@leed-ai/cli 0.0.0-stage → 6.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +57 -0
- package/README.md +125 -2
- package/dist/LICENSE.md +57 -0
- package/dist/acorn-DAgDT1om.js +5087 -0
- package/dist/angular-l0_4PK8N.js +3598 -0
- package/dist/authShared-Dray38Ls.js +1161 -0
- package/dist/babel-CJeRQd5u.js +10038 -0
- package/dist/buildResults-tiq1nugP.js +21180 -0
- package/dist/chunk-1VJ9lAw7.js +42 -0
- package/dist/cliAuth-38JjD6hw.js +15 -0
- package/dist/env-B0bHiXJJ.js +29 -0
- package/dist/errors-CqKamGCu.js +481 -0
- package/dist/estree-E8WG8UHj.js +6930 -0
- package/dist/flow-6xaRIbPQ.js +52255 -0
- package/dist/glimmer-aJPjsfbT.js +7272 -0
- package/dist/graphql-mcgtPEiZ.js +1901 -0
- package/dist/html--eeea3Kl.js +6553 -0
- package/dist/leed.eleventy.mjs +6525 -0
- package/dist/leed.mjs +88952 -0
- package/dist/leed.sources.json +798 -0
- package/dist/leedLogger-Zkada5aO.js +78 -0
- package/dist/markdown-D73jNKwx.js +6836 -0
- package/dist/meriyah-CMp1nDDW.js +8932 -0
- package/dist/package.json +101 -0
- package/dist/postcss-h4fBr2Fn.js +6760 -0
- package/dist/prettier-CwFSKxfN.js +16821 -0
- package/dist/static/customer-site-worker/package.json +9 -0
- package/dist/static/customer-site-worker/src/assetsHydration.ts +55 -0
- package/dist/static/customer-site-worker/src/constants.ts +20 -0
- package/dist/static/customer-site-worker/src/cors.ts +120 -0
- package/dist/static/customer-site-worker/src/index.ts +242 -0
- package/dist/static/customer-site-worker/src/mcp-proxy.ts +98 -0
- package/dist/static/customer-site-worker/src/proxy.ts +92 -0
- package/dist/static/customer-site-worker/src/proxyHeaders.ts +47 -0
- package/dist/static/customer-site-worker/src/tier.ts +40 -0
- package/dist/static/customer-site-worker/src/types.ts +82 -0
- package/dist/static/customer-site-worker/test/functions-mock.worker.js +50 -0
- package/dist/static/customer-site-worker/tsconfig.json +19 -0
- package/dist/static/customer-site-worker/vitest.workers.config.ts +61 -0
- package/dist/static/install/husky/colors.mjs +5 -0
- package/dist/static/install/husky/hooks/commit-msg +1 -0
- package/dist/static/install/husky/hooks/pre-commit +1 -0
- package/dist/static/install/husky/hooks/pre-push +1 -0
- package/dist/static/install/husky/valid-command.mjs +29 -0
- package/dist/static/install/husky/valid-commit-msg.mjs +20 -0
- package/dist/static/install/package.json +10 -0
- package/dist/static/install/packages.txt +4 -0
- package/dist/static/install/tasks.json +111 -0
- package/dist/static/javascript/detectIncognito.js +195 -0
- package/dist/static/javascript/documentation.js +2781 -0
- package/dist/static/javascript/fp/fp.esm.js +2896 -0
- package/dist/static/javascript/fp/fp.js +3067 -0
- package/dist/static/javascript/live-search.js +986 -0
- package/dist/static/javascript/lunr-search.js +843 -0
- package/dist/static/javascript/timeme.js +451 -0
- package/dist/static/javascript/tracker/webmcp.js +67 -0
- package/dist/static/javascript/tracker/whisper.js +1 -0
- package/dist/static/javascript/utilities.js +819 -0
- package/dist/static/javascript/versions.json +74 -0
- package/dist/static/javascript/zoomable.js +488 -0
- package/dist/static/site/_data/eleventyComputed.js +228 -0
- package/dist/static/site/_data/leed-form-data.json +253 -0
- package/dist/static/site/_includes/leed/api/bits/array-type.hbs +24 -0
- package/dist/static/site/_includes/leed/api/bits/badges.hbs +9 -0
- package/dist/static/site/_includes/leed/api/bits/description.hbs +9 -0
- package/dist/static/site/_includes/leed/api/bits/examples.hbs +42 -0
- package/dist/static/site/_includes/leed/api/bits/scopes.hbs +28 -0
- package/dist/static/site/_includes/leed/api/object.hbs +110 -0
- package/dist/static/site/_includes/leed/api/parameters.hbs +11 -0
- package/dist/static/site/_includes/leed/api/property.hbs +153 -0
- package/dist/static/site/_includes/leed/api/request-body.hbs +51 -0
- package/dist/static/site/_includes/leed/api/responses.hbs +89 -0
- package/dist/static/site/_includes/leed/api/security-constraints.hbs +59 -0
- package/dist/static/site/_includes/leed/api/security.hbs +75 -0
- package/dist/static/site/_includes/leed/components/pagination.hbs +45 -0
- package/dist/static/site/_includes/leed/components/scroll-status-indicator-horizontal.hbs +22 -0
- package/dist/static/site/_includes/leed/docs/alpha/api.hbs +6 -0
- package/dist/static/site/_includes/leed/docs/alpha/base.hbs +26 -0
- package/dist/static/site/_includes/leed/docs/alpha/documentation.hbs +7 -0
- package/dist/static/site/_includes/leed/docs/api-article.hbs +75 -0
- package/dist/static/site/_includes/leed/docs/bravo/api.hbs +5 -0
- package/dist/static/site/_includes/leed/docs/bravo/base.hbs +31 -0
- package/dist/static/site/_includes/leed/docs/bravo/documentation.hbs +7 -0
- package/dist/static/site/_includes/leed/docs/charlie/api.hbs +6 -0
- package/dist/static/site/_includes/leed/docs/charlie/base.hbs +27 -0
- package/dist/static/site/_includes/leed/docs/charlie/documentation.hbs +10 -0
- package/dist/static/site/_includes/leed/docs/doc-article.hbs +40 -0
- package/dist/static/site/_includes/leed/docs/docs-footer.hbs +35 -0
- package/dist/static/site/_includes/leed/docs/docs-header.hbs +59 -0
- package/dist/static/site/_includes/leed/docs/document-search-modal.hbs +36 -0
- package/dist/static/site/_includes/leed/docs/document-search.hbs +23 -0
- package/dist/static/site/_includes/leed/docs/footer.hbs +12 -0
- package/dist/static/site/_includes/leed/docs/header/brand.hbs +19 -0
- package/dist/static/site/_includes/leed/docs/header/chrome.hbs +33 -0
- package/dist/static/site/_includes/leed/docs/header/compact.hbs +19 -0
- package/dist/static/site/_includes/leed/docs/header/cta.hbs +15 -0
- package/dist/static/site/_includes/leed/docs/header/nav.hbs +18 -0
- package/dist/static/site/_includes/leed/docs/header/top-search.hbs +15 -0
- package/dist/static/site/_includes/leed/docs/mini-bar.hbs +53 -0
- package/dist/static/site/_includes/leed/docs/pagination.hbs +19 -0
- package/dist/static/site/_includes/leed/docs/shell.hbs +56 -0
- package/dist/static/site/_includes/leed/docs/sidebar.hbs +52 -0
- package/dist/static/site/_includes/leed/docs/social-icons.hbs +81 -0
- package/dist/static/site/_includes/leed/docs/style.hbs +17 -0
- package/dist/static/site/_includes/leed/docs/toc.hbs +7 -0
- package/dist/static/site/_includes/leed/form.hbs +266 -0
- package/dist/static/site/_includes/leed/head.hbs +67 -0
- package/dist/static/site/_includes/leed/menu/builder.hbs +35 -0
- package/dist/static/site/_includes/leed/menu/item.hbs +67 -0
- package/dist/static/site/_includes/leed/menu/link.hbs +59 -0
- package/dist/static/site/_includes/leed/metadata/ogCard.hbs +40 -0
- package/dist/static/site/_includes/leed/metadata/siteMetadata.hbs +24 -0
- package/dist/static/site/_includes/leed/metadata/twitterCard.hbs +34 -0
- package/dist/static/site/_includes/leed/popup-video.hbs +21 -0
- package/dist/static/site/_includes/leed/stub-template.hbs +17 -0
- package/dist/static/site/_includes/leed/unsubscribe-form.hbs +30 -0
- package/dist/static/site/_layouts/leed-documentation.hbs +12 -0
- package/dist/static/site/leed-feed-atom.hbs +42 -0
- package/dist/static/site/leed-feed-json.hbs +56 -0
- package/dist/static/site/leed-feed-rss.hbs +39 -0
- package/dist/static/site/leed-headers.hbs +38 -0
- package/dist/static/site/leed-redirects.hbs +10 -0
- package/dist/static/site/leed-sitemap-pageType.hbs +28 -0
- package/dist/static/site/leed-sitemap.hbs +19 -0
- package/dist/static/site/leed-unsubscribe.hbs +44 -0
- package/dist/static/site/leed-unsubscribed.hbs +40 -0
- package/dist/static/tailwind/components/alerts.css +132 -0
- package/dist/static/tailwind/components/api-methods.css +39 -0
- package/dist/static/tailwind/components/breadcrumbs.css +31 -0
- package/dist/static/tailwind/components/code.css +88 -0
- package/dist/static/tailwind/components/collapsible.css +38 -0
- package/dist/static/tailwind/components/document-search.css +222 -0
- package/dist/static/tailwind/components/form.css +185 -0
- package/dist/static/tailwind/components/kbd.css +7 -0
- package/dist/static/tailwind/components/pagination.css +81 -0
- package/dist/static/tailwind/components/scroll-status-indicator.css +5 -0
- package/dist/static/tailwind/components/tab-groups.css +117 -0
- package/dist/static/tailwind/components/tables.css +0 -0
- package/dist/static/tailwind/components/toc.css +60 -0
- package/dist/static/tailwind/components.css +248 -0
- package/dist/static/tailwind/defaults.css +260 -0
- package/dist/static/tailwind/docs/api.css +701 -0
- package/dist/static/tailwind/docs/components/blockquote.css +5 -0
- package/dist/static/tailwind/docs/components/button.css +11 -0
- package/dist/static/tailwind/docs/components/docs-pagination.css +71 -0
- package/dist/static/tailwind/docs/components/mobile-bar.css +83 -0
- package/dist/static/tailwind/docs/components/navigation.css +279 -0
- package/dist/static/tailwind/docs/components/table.css +54 -0
- package/dist/static/tailwind/docs/components.css +6 -0
- package/dist/static/tailwind/docs/docs.css +427 -0
- package/dist/static/tailwind/docs/layouts/alpha.css +108 -0
- package/dist/static/tailwind/docs/layouts/bravo.css +218 -0
- package/dist/static/tailwind/docs/layouts/charlie.css +105 -0
- package/dist/static/tailwind/docs/layouts.css +3 -0
- package/dist/static/tailwind/docs/themes/amber.css +113 -0
- package/dist/static/tailwind/docs/themes/blue.css +94 -0
- package/dist/static/tailwind/docs/themes/cyan.css +111 -0
- package/dist/static/tailwind/docs/themes/emerald.css +111 -0
- package/dist/static/tailwind/docs/themes/fuchsia.css +111 -0
- package/dist/static/tailwind/docs/themes/green.css +95 -0
- package/dist/static/tailwind/docs/themes/indigo.css +111 -0
- package/dist/static/tailwind/docs/themes/lime.css +111 -0
- package/dist/static/tailwind/docs/themes/orange.css +111 -0
- package/dist/static/tailwind/docs/themes/pink.css +111 -0
- package/dist/static/tailwind/docs/themes/purple.css +111 -0
- package/dist/static/tailwind/docs/themes/red.css +111 -0
- package/dist/static/tailwind/docs/themes/rose.css +111 -0
- package/dist/static/tailwind/docs/themes/sky.css +111 -0
- package/dist/static/tailwind/docs/themes/teal.css +111 -0
- package/dist/static/tailwind/docs/themes/violet.css +111 -0
- package/dist/static/tailwind/docs/themes/yellow.css +111 -0
- package/dist/static/tailwind/docs/themes.css +84 -0
- package/dist/static/tailwind/font-awesome/free-7.3.1/brands.css +2455 -0
- package/dist/static/tailwind/font-awesome/free-7.3.1/fontawesome.css +8997 -0
- package/dist/static/tailwind/font-awesome/free-7.3.1/fonts.css +45 -0
- package/dist/static/tailwind/font-awesome/free-7.3.1/icons.json +2608 -0
- package/dist/static/tailwind/font-awesome/free-7.3.1.css +3 -0
- package/dist/static/tailwind/font-awesome/pro-7.0.0/brands.css +2215 -0
- package/dist/static/tailwind/font-awesome/pro-7.0.0/fontawesome.css +19281 -0
- package/dist/static/tailwind/font-awesome/pro-7.0.0/fonts.css +445 -0
- package/dist/static/tailwind/font-awesome/pro-7.0.0.css +3 -0
- package/dist/static/tailwind/font-awesome/pro-7.3.1/brands.css +2455 -0
- package/dist/static/tailwind/font-awesome/pro-7.3.1/fontawesome.css +20365 -0
- package/dist/static/tailwind/font-awesome/pro-7.3.1/fonts.css +589 -0
- package/dist/static/tailwind/font-awesome/pro-7.3.1.css +3 -0
- package/dist/static/tailwind/fonts/all.css +6 -0
- package/dist/static/tailwind/fonts/config.jsonc +31 -0
- package/dist/static/tailwind/fonts/geist-mono.css +142 -0
- package/dist/static/tailwind/fonts/geist.css +142 -0
- package/dist/static/tailwind/fonts/inter.css +215 -0
- package/dist/static/tailwind/fonts/jetbrains-mono.css +126 -0
- package/dist/static/tailwind/fonts/noto-sans.css +134 -0
- package/dist/static/tailwind/fonts/open-sans.css +89 -0
- package/dist/static/tailwind/highlighters.css +14 -0
- package/dist/static/tailwind/hljs/README.md +53 -0
- package/dist/static/tailwind/hljs/a11y.css +96 -0
- package/dist/static/tailwind/hljs/atom-fruit.css +90 -0
- package/dist/static/tailwind/hljs/atom-one.css +164 -0
- package/dist/static/tailwind/hljs/default.css +164 -0
- package/dist/static/tailwind/hljs/github.css +101 -0
- package/dist/static/tailwind/hljs/google-code.css +112 -0
- package/dist/static/tailwind/hljs/grayscale.css +115 -0
- package/dist/static/tailwind/hljs/gruvbox-hard.css +115 -0
- package/dist/static/tailwind/hljs/gruvbox-medium.css +115 -0
- package/dist/static/tailwind/hljs/gruvbox-soft.css +115 -0
- package/dist/static/tailwind/hljs/papercolor.css +120 -0
- package/dist/static/tailwind/hljs/qt-creator.css +91 -0
- package/dist/static/tailwind/hljs/summerfruit.css +115 -0
- package/dist/static/tailwind/hljs/tomorrow.css +115 -0
- package/dist/static/tailwind/leed.css +170 -0
- package/dist/static/tailwind/site-tokens.reference.css +105 -0
- package/dist/static/tailwind/utils/icons.css +102 -0
- package/dist/static/tailwind/utils/utilities.css +105 -0
- package/dist/static/templates/pages/PAGE.hbs +27 -0
- package/dist/static/templates/pages/PAGE_LAYOUT.hbs +19 -0
- package/dist/static/templates/pages/PAGE_TYPE_INDEX.hbs +89 -0
- package/dist/static/templates/pages/PAGINATED_PAGE_TYPE_ITEM.hbs +52 -0
- package/dist/static/templates/pages/PAGINATED_PAGE_TYPE_LIST.hbs +39 -0
- package/dist/static/templates/pages/PAGINATED_PAGE_TYPE_PAGINATION.hbs +11 -0
- package/dist/static/templates/raw-content/.ci/build.sh +58 -0
- package/dist/static/templates/raw-content/README.md +179 -0
- package/dist/static/templates/raw-content/_gitignore +50 -0
- package/dist/static/templates/raw-content/src/404.hbs +21 -0
- package/dist/static/templates/raw-content/src/_data/autolink.json +1 -0
- package/dist/static/templates/raw-content/src/_data/ctaList.json +1 -0
- package/dist/static/templates/raw-content/src/_data/leedForms.json +1 -0
- package/dist/static/templates/raw-content/src/_data/menu.json +1 -0
- package/dist/static/templates/raw-content/src/_includes/footer.hbs +18 -0
- package/dist/static/templates/raw-content/src/_includes/header-includes.hbs +25 -0
- package/dist/static/templates/raw-content/src/_includes/header.hbs +5 -0
- package/dist/static/templates/raw-content/src/_includes/site-template.hbs +23 -0
- package/dist/static/templates/raw-content/src/_includes/unsubscribe-DISABLED.hbs +17 -0
- package/dist/static/templates/raw-content/src/_includes/unsubscribed-DISABLED.hbs +13 -0
- package/dist/static/templates/raw-content/src/_layouts/empty.hbs +2 -0
- package/dist/static/templates/raw-content/src/index.hbs +21 -0
- package/dist/static/templates/raw-content/src/robots.hbs +9 -0
- package/dist/static/templates/raw-content/tailwind/site/base.css +44 -0
- package/dist/static/templates/raw-content/tailwind/site.config.css +3 -0
- package/dist/static/templates/sandbox/_gitignore +50 -0
- package/dist/static/templates/sandbox/identity.json +1 -0
- package/dist/static/templates/sandbox/src/404.hbs +21 -0
- package/dist/static/templates/sandbox/src/_data/autolink.json +1 -0
- package/dist/static/templates/sandbox/src/_data/ctaList.json +1 -0
- package/dist/static/templates/sandbox/src/_data/labelList.json +9 -0
- package/dist/static/templates/sandbox/src/_data/leedForms.json +20 -0
- package/dist/static/templates/sandbox/src/_data/menu.json +19 -0
- package/dist/static/templates/sandbox/src/_data/pageTypeList.json +14 -0
- package/dist/static/templates/sandbox/src/_data/searchIndexList.json +1 -0
- package/dist/static/templates/sandbox/src/_data/userList.json +11 -0
- package/dist/static/templates/sandbox/src/_includes/footer.hbs +18 -0
- package/dist/static/templates/sandbox/src/_includes/header-includes.hbs +25 -0
- package/dist/static/templates/sandbox/src/_includes/header.hbs +5 -0
- package/dist/static/templates/sandbox/src/_includes/site-template.hbs +23 -0
- package/dist/static/templates/sandbox/src/_layouts/empty.hbs +2 -0
- package/dist/static/templates/sandbox/src/index.hbs +20 -0
- package/dist/static/templates/sandbox/src/robots.hbs +9 -0
- package/dist/static/templates/sandbox/src/src.11tydata.json +39 -0
- package/dist/static/templates/sandbox/tailwind/site/base.css +44 -0
- package/dist/static/templates/sandbox/tailwind/site.config.css +3 -0
- package/dist/thrown-sukVkp_1.js +113 -0
- package/dist/typescript-D4T0V_Hn.js +23004 -0
- package/dist/yaml-9wR8GxF_.js +5236 -0
- package/package.json +157 -4
|
@@ -0,0 +1,819 @@
|
|
|
1
|
+
window.KNOWN_USER_CHECK = undefined;
|
|
2
|
+
const downloadState = "downloading";
|
|
3
|
+
const thanksState = "thanks";
|
|
4
|
+
const errorState = "error";
|
|
5
|
+
const emailSentState = "email-sent";
|
|
6
|
+
const sendingState = "sending";
|
|
7
|
+
const submitState = "submit";
|
|
8
|
+
|
|
9
|
+
// duplicated from data-types/src/util/index
|
|
10
|
+
function cleanRecord(record) {
|
|
11
|
+
const result = {};
|
|
12
|
+
Object.assign(result, record);
|
|
13
|
+
Object.keys(result).forEach((key) => {
|
|
14
|
+
if (result[key] == null || result[key] === "" || result[key] === "null") {
|
|
15
|
+
delete result[key];
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
return result;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function activeFormElement(formId, elementName) {
|
|
22
|
+
const stateSubmit = document.getElementById(`${formId}-button-${submitState}`);
|
|
23
|
+
const stateSending = document.getElementById(`${formId}-button-${sendingState}`);
|
|
24
|
+
const stateEmailSent = document.getElementById(`${formId}-button-${emailSentState}`);
|
|
25
|
+
const stateError = document.getElementById(`${formId}-button-${errorState}`);
|
|
26
|
+
const stateThanks = document.getElementById(`${formId}-button-${thanksState}`);
|
|
27
|
+
const stateDownloading = document.getElementById(`${formId}-button-${downloadState}`);
|
|
28
|
+
const states = [stateSubmit, stateSending, stateEmailSent, stateError, stateThanks, stateDownloading];
|
|
29
|
+
|
|
30
|
+
let activeElement;
|
|
31
|
+
states.forEach((element) => {
|
|
32
|
+
element.style.display = "none";
|
|
33
|
+
if (element.id.endsWith(elementName)) {
|
|
34
|
+
activeElement = element;
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
stateDownloading.style.setProperty("display", "none", "important");
|
|
38
|
+
|
|
39
|
+
activeElement.style.display = "block";
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function leedEnableFormButton(event) {
|
|
43
|
+
const formId = event.currentTarget.id;
|
|
44
|
+
document.getElementById(`${formId}-button`).disabled = false;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function emailChangeListener() {
|
|
48
|
+
const formId = this.form.id;
|
|
49
|
+
activeFormElement(formId, submitState);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function downloadFile(url, filename) {
|
|
53
|
+
await fetch(url)
|
|
54
|
+
.then((response) => response.blob())
|
|
55
|
+
.then((blob) => {
|
|
56
|
+
const a = document.createElement("a");
|
|
57
|
+
a.href = URL.createObjectURL(blob);
|
|
58
|
+
a.download = filename;
|
|
59
|
+
document.body.appendChild(a);
|
|
60
|
+
a.click();
|
|
61
|
+
document.body.removeChild(a);
|
|
62
|
+
})
|
|
63
|
+
.catch((error) => {
|
|
64
|
+
console.error("Error downloading file:", error);
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function leedFormSubmit(event) {
|
|
69
|
+
event.preventDefault();
|
|
70
|
+
// this is a shared promise. needs to finish before we call services.
|
|
71
|
+
await leedSessionReady("form_fill");
|
|
72
|
+
|
|
73
|
+
const formId = event.srcElement.id;
|
|
74
|
+
const formButton = document.getElementById(`${formId}-button`);
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* We disable the button to prevent double/triple clicks.
|
|
78
|
+
* If successful we leave the button disabled, and only
|
|
79
|
+
* reenable on change to the form for resubmission.
|
|
80
|
+
*/
|
|
81
|
+
formButton.disabled = true;
|
|
82
|
+
activeFormElement(formId, sendingState);
|
|
83
|
+
|
|
84
|
+
const fd = new FormData(event.srcElement);
|
|
85
|
+
const data = cleanRecord(Object.fromEntries(fd.entries()));
|
|
86
|
+
let timezone;
|
|
87
|
+
try {
|
|
88
|
+
timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
89
|
+
} catch (_e) {
|
|
90
|
+
// Older browsers may not support Intl.DateTimeFormat — omit timezone.
|
|
91
|
+
}
|
|
92
|
+
Object.assign(data, {
|
|
93
|
+
knownUser: window.KNOWN_USER_CHECK?.known || false,
|
|
94
|
+
formId,
|
|
95
|
+
pageId: pid, // declared in each page
|
|
96
|
+
pageTypeId: pt, // declared in each page
|
|
97
|
+
timezone,
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
const response = await fetch("/api/clerk", {
|
|
101
|
+
method: "POST",
|
|
102
|
+
body: JSON.stringify(data),
|
|
103
|
+
headers: { "content-type": "application/json" },
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const s = event.srcElement.dataset.successcallback;
|
|
107
|
+
const f = event.srcElement.dataset.failurecallback;
|
|
108
|
+
if (response.status === 204) {
|
|
109
|
+
activeFormElement(formId, thanksState);
|
|
110
|
+
} else if (response.status === 200) {
|
|
111
|
+
if (response.body) {
|
|
112
|
+
const result = await response.json();
|
|
113
|
+
if (result?.url && result.filename) {
|
|
114
|
+
activeFormElement(formId, downloadState);
|
|
115
|
+
await downloadFile(result.url, result.filename);
|
|
116
|
+
activeFormElement(formId, thanksState);
|
|
117
|
+
} else if (result?.message && result.message === "sent") {
|
|
118
|
+
activeFormElement(formId, emailSentState);
|
|
119
|
+
} else {
|
|
120
|
+
activeFormElement(formId, thanksState);
|
|
121
|
+
}
|
|
122
|
+
} else {
|
|
123
|
+
activeFormElement(formId, thanksState);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (s && window[s] && typeof window[s] === "function") {
|
|
127
|
+
console.log("Success callback...");
|
|
128
|
+
window[s].apply(null, [formId]);
|
|
129
|
+
}
|
|
130
|
+
} else if (response.status === 422) {
|
|
131
|
+
activeFormElement(formId, errorState);
|
|
132
|
+
|
|
133
|
+
if (f && window[f] && typeof window[f] === "function") {
|
|
134
|
+
console.log("Turnstile failure...");
|
|
135
|
+
window[f].apply(null, [formId]);
|
|
136
|
+
}
|
|
137
|
+
formButton.disabled = false;
|
|
138
|
+
} else {
|
|
139
|
+
activeFormElement(formId, errorState);
|
|
140
|
+
|
|
141
|
+
if (f && window[f] && typeof window[f] === "function") {
|
|
142
|
+
console.log("Failure callback...");
|
|
143
|
+
window[f].apply(null, [formId]);
|
|
144
|
+
}
|
|
145
|
+
formButton.disabled = false;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async function leedAutofill(formId) {
|
|
150
|
+
if (!window.KNOWN_USER_CHECK) {
|
|
151
|
+
// this is a shared promise. needs to finish before we call services.
|
|
152
|
+
await leedSessionReady("pageview");
|
|
153
|
+
|
|
154
|
+
// console.log("Checking if this is a known user...");
|
|
155
|
+
try {
|
|
156
|
+
const resp = await fetch(`/api/poirot`);
|
|
157
|
+
window.KNOWN_USER_CHECK = await resp.json();
|
|
158
|
+
} catch (e) {
|
|
159
|
+
window.KNOWN_USER_CHECK = { known: false };
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (window?.KNOWN_USER_CHECK?.known) {
|
|
164
|
+
const record = window?.KNOWN_USER_CHECK;
|
|
165
|
+
const form = document.getElementById(formId);
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* If fields are configured to be hidden, we hide them based on how the user is known.
|
|
169
|
+
*/
|
|
170
|
+
if (record.details.source != null && record.details.source !== "form") {
|
|
171
|
+
const hideable = form.querySelectorAll("div.known-valid-hideable");
|
|
172
|
+
for (let i = 0; i < hideable.length; i++) {
|
|
173
|
+
const div = hideable[i];
|
|
174
|
+
const all = div.querySelectorAll("*");
|
|
175
|
+
Array.from(all).forEach((element) => {
|
|
176
|
+
if (element.hasAttribute("required")) {
|
|
177
|
+
element.required = false;
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
div.style.display = "none";
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const details = Object.keys(record.details).reduce((acc, key) => {
|
|
185
|
+
acc[key.toLowerCase()] = record.details[key];
|
|
186
|
+
return acc;
|
|
187
|
+
}, {});
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Iterate over all the fields in the form to grab any fillable values.
|
|
191
|
+
*/
|
|
192
|
+
const wbDiv = document.getElementById(`${formId}-wb`);
|
|
193
|
+
let message = wbDiv.innerHTML;
|
|
194
|
+
const formElements = form.elements;
|
|
195
|
+
for (let i = 0; i < formElements.length; i++) {
|
|
196
|
+
const formElement = formElements[i];
|
|
197
|
+
const key = formElement.name;
|
|
198
|
+
if (key) {
|
|
199
|
+
/**
|
|
200
|
+
* We only populate the fields if they were known by a non-validated
|
|
201
|
+
* form fill, because we still want them to update fields.
|
|
202
|
+
*/
|
|
203
|
+
if (record.details.source === "form" && details[key]) {
|
|
204
|
+
formElement.value = details[key];
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Build the welcome back message. If a value is empty
|
|
209
|
+
* it will just get replaced with the empty value so the
|
|
210
|
+
* string should still format well.
|
|
211
|
+
*/
|
|
212
|
+
const tK = `{{${key}}}`;
|
|
213
|
+
const newValue = details[key] ? details[key] : "";
|
|
214
|
+
message = message.replace(tK, newValue);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
if (message !== "") {
|
|
218
|
+
wbDiv.innerHTML = message;
|
|
219
|
+
wbDiv.style.display = "block";
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Wrap this in onload to ensure that it happens after the page has loaded.
|
|
226
|
+
*/
|
|
227
|
+
function setupLeedForms() {
|
|
228
|
+
const forms = document.getElementsByClassName("LeedForm");
|
|
229
|
+
for (let i = 0; i < forms.length; i++) {
|
|
230
|
+
const form = forms.item(i);
|
|
231
|
+
form.onsubmit = leedFormSubmit;
|
|
232
|
+
form.oninput = leedEnableFormButton;
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Find the email field, add reset functionality in case they
|
|
236
|
+
* want to change their email address and resubmit the form.
|
|
237
|
+
*/
|
|
238
|
+
const emailField = document.getElementById(`${form.id}-email`);
|
|
239
|
+
if (emailField) {
|
|
240
|
+
emailField.addEventListener("input", emailChangeListener, false);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
window.addEventListener("load", setupLeedForms);
|
|
246
|
+
|
|
247
|
+
function submitLeadUnsubscribe() {
|
|
248
|
+
const form = document.getElementById("unsubscribeForm");
|
|
249
|
+
const reason = form.elements.reason.value;
|
|
250
|
+
|
|
251
|
+
let params = `?reason=${reason}`;
|
|
252
|
+
const doNotContact = form.elements.donotcontact.checked;
|
|
253
|
+
if (doNotContact) {
|
|
254
|
+
params += "&donotcontact=true";
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// This is the unsubscribe page, and we want to submit to the out page.
|
|
258
|
+
const url = window.location.pathname.replace(".unsubscribe", ".out") + params;
|
|
259
|
+
|
|
260
|
+
window.location.href = url;
|
|
261
|
+
return false;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
async function fetchJson(endpointName) {
|
|
265
|
+
const resp = await fetch(`/api/${endpointName}/v2/${pid}`); // pid declared in each page
|
|
266
|
+
if (resp.status !== 200) return null;
|
|
267
|
+
try {
|
|
268
|
+
return await resp.json();
|
|
269
|
+
} catch {
|
|
270
|
+
return null;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Stamp the click-attribution reason onto every anchor in `container`, replacing
|
|
276
|
+
* the build-time `addAnchorReason` shortcode. This has to stay client-side:
|
|
277
|
+
* utm_content derives from each anchor's rendered innerText, which HTMLRewriter
|
|
278
|
+
* cannot see at the point it could still set an attribute.
|
|
279
|
+
*
|
|
280
|
+
* Pass the wrapper or the cloned DocumentFragment, never the stub anchor itself
|
|
281
|
+
* — `element.querySelectorAll("a")` does not match the element it is called on,
|
|
282
|
+
* and a stub template's root IS the `<a>`. A fragment's does include it.
|
|
283
|
+
*
|
|
284
|
+
* The reason string is parsed server-side as a URL query (`addUrchinParams`), so
|
|
285
|
+
* the link text is encoded — an unencoded `&` or `=` in a title like
|
|
286
|
+
* "Q&A: Getting Started" would otherwise split into bogus params. It is also
|
|
287
|
+
* collapsed and capped: a stub's anchor wraps the WHOLE card, so its text is the
|
|
288
|
+
* title, date, summary, labels and authors run together.
|
|
289
|
+
*/
|
|
290
|
+
const UTM_CONTENT_MAX = 100;
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Cap the link text and percent-encode it for the reason query string.
|
|
294
|
+
*
|
|
295
|
+
* Array.from iterates a string by CODE POINT, so the cap can never land between
|
|
296
|
+
* the halves of a surrogate pair. `.slice()` counts UTF-16 code units and can:
|
|
297
|
+
* an emoji straddling the cap leaves a lone surrogate, encodeURIComponent
|
|
298
|
+
* throws URIError on one, and applyReason runs synchronously inside the
|
|
299
|
+
* per-item loop — so a single emoji would drop every remaining card. The catch
|
|
300
|
+
* covers link text that arrived unpaired to begin with: lose utm_content, never
|
|
301
|
+
* the card.
|
|
302
|
+
*/
|
|
303
|
+
function utmContent(text) {
|
|
304
|
+
const capped = Array.from(text).slice(0, UTM_CONTENT_MAX).join("");
|
|
305
|
+
try {
|
|
306
|
+
return encodeURIComponent(capped);
|
|
307
|
+
} catch (_e) {
|
|
308
|
+
return "";
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function applyReason(container, medium, term) {
|
|
313
|
+
const base = `utm_campaign=leed&utm_source=internal&utm_medium=${medium}&utm_term=${term}`;
|
|
314
|
+
container.querySelectorAll("a").forEach((a) => {
|
|
315
|
+
const content = utmContent((a.innerText || "").replace(/\s+/g, " ").trim());
|
|
316
|
+
a.dataset.reason = content ? `${base}&utm_content=${content}` : base;
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Render a date the way the build-time `{{#date "default"}}` shortcode did.
|
|
322
|
+
*
|
|
323
|
+
* The stub used to be rendered by Eleventy, which resolved the company/page-type
|
|
324
|
+
* dateFormat in the site's timezone via dayjs. Moving the card to the client
|
|
325
|
+
* moved that off the server, and `toLocaleDateString()` would give a different
|
|
326
|
+
* string per visitor locale and timezone — visibly unlike every other date on
|
|
327
|
+
* the page. The `<template>` is emitted at build time, so it carries the site's
|
|
328
|
+
* `dateFormat`/`timezone` and these format from those instead. Defaults match
|
|
329
|
+
* `plugins/dates.ts` exactly.
|
|
330
|
+
*
|
|
331
|
+
* Tokens are the dayjs date/time set. Anything unrecognized passes through, and
|
|
332
|
+
* `[...]` escapes a literal, both as dayjs does.
|
|
333
|
+
*/
|
|
334
|
+
const DATE_MONTHS = [
|
|
335
|
+
"January",
|
|
336
|
+
"February",
|
|
337
|
+
"March",
|
|
338
|
+
"April",
|
|
339
|
+
"May",
|
|
340
|
+
"June",
|
|
341
|
+
"July",
|
|
342
|
+
"August",
|
|
343
|
+
"September",
|
|
344
|
+
"October",
|
|
345
|
+
"November",
|
|
346
|
+
"December",
|
|
347
|
+
];
|
|
348
|
+
const DATE_TOKENS = /\[([^\]]*)\]|YYYY|YY|MMMM|MMM|MM|M|dddd|ddd|dd|DD|D|HH|H|hh|h|mm|m|ss|s|A|a/g;
|
|
349
|
+
|
|
350
|
+
function zonedParts(when, timezone) {
|
|
351
|
+
const options = {
|
|
352
|
+
year: "numeric",
|
|
353
|
+
month: "2-digit",
|
|
354
|
+
day: "2-digit",
|
|
355
|
+
hour: "2-digit",
|
|
356
|
+
minute: "2-digit",
|
|
357
|
+
second: "2-digit",
|
|
358
|
+
weekday: "long",
|
|
359
|
+
hourCycle: "h23",
|
|
360
|
+
};
|
|
361
|
+
let formatted;
|
|
362
|
+
try {
|
|
363
|
+
formatted = new Intl.DateTimeFormat("en-US", { ...options, timeZone: timezone }).formatToParts(when);
|
|
364
|
+
} catch (_e) {
|
|
365
|
+
// Intl throws RangeError on an unknown IANA zone; the build-time helper
|
|
366
|
+
// defaults to UTC, so match it rather than dropping the date.
|
|
367
|
+
formatted = new Intl.DateTimeFormat("en-US", { ...options, timeZone: "UTC" }).formatToParts(when);
|
|
368
|
+
}
|
|
369
|
+
const parts = {};
|
|
370
|
+
formatted.forEach((part) => {
|
|
371
|
+
parts[part.type] = part.value;
|
|
372
|
+
});
|
|
373
|
+
return parts;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function formatCardDate(when, dateFormat, timezone) {
|
|
377
|
+
const parts = zonedParts(when, timezone || "UTC");
|
|
378
|
+
const month = DATE_MONTHS[Number(parts.month) - 1];
|
|
379
|
+
const hour24 = Number(parts.hour);
|
|
380
|
+
const hour12 = hour24 % 12 || 12;
|
|
381
|
+
const values = {
|
|
382
|
+
YYYY: parts.year,
|
|
383
|
+
YY: parts.year.slice(-2),
|
|
384
|
+
MMMM: month,
|
|
385
|
+
MMM: month.slice(0, 3),
|
|
386
|
+
MM: parts.month,
|
|
387
|
+
M: String(Number(parts.month)),
|
|
388
|
+
dddd: parts.weekday,
|
|
389
|
+
ddd: parts.weekday.slice(0, 3),
|
|
390
|
+
dd: parts.weekday.slice(0, 2),
|
|
391
|
+
DD: parts.day,
|
|
392
|
+
D: String(Number(parts.day)),
|
|
393
|
+
HH: parts.hour,
|
|
394
|
+
H: String(hour24),
|
|
395
|
+
hh: String(hour12).padStart(2, "0"),
|
|
396
|
+
h: String(hour12),
|
|
397
|
+
mm: parts.minute,
|
|
398
|
+
m: String(Number(parts.minute)),
|
|
399
|
+
ss: parts.second,
|
|
400
|
+
s: String(Number(parts.second)),
|
|
401
|
+
A: hour24 < 12 ? "AM" : "PM",
|
|
402
|
+
a: hour24 < 12 ? "am" : "pm",
|
|
403
|
+
};
|
|
404
|
+
return (dateFormat || "YYYY-MM-DD").replace(DATE_TOKENS, (token, literal) =>
|
|
405
|
+
literal === undefined ? values[token] : literal,
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Reading time in whole minutes, the client-side twin of the `readingTime`
|
|
411
|
+
* shortcode — the stub used to render it at build time and would otherwise lose
|
|
412
|
+
* it. The arithmetic is ported verbatim, including the 200 wpm fallback, which
|
|
413
|
+
* is deliberately NOT the config default of 250 (`defaults.ts`): the shortcode
|
|
414
|
+
* only falls back when `readingWpm` is null or 0, and collapsing the two
|
|
415
|
+
* numbers would silently change displayed reading times on live sites.
|
|
416
|
+
*
|
|
417
|
+
* Returns "" for a page with no word count, so the falsy-slot rule removes the
|
|
418
|
+
* element the way the old `{{#if list.data.wordCount}}` guard did. Never zero —
|
|
419
|
+
* a countable page always reads as at least one minute.
|
|
420
|
+
*/
|
|
421
|
+
function readingMinutes(wordCount, readingWpm) {
|
|
422
|
+
if (!wordCount) return "";
|
|
423
|
+
const wpm = readingWpm != null && readingWpm !== 0 ? readingWpm : 200;
|
|
424
|
+
const t = Math.floor(wordCount / wpm);
|
|
425
|
+
return String(t > 0 ? t : 1);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Cloudflare Images serves a URL as `/cdn-cgi/imagedelivery/<hash>/<id>/<variant>`,
|
|
430
|
+
* where the variant is the LAST path segment. Recommendation payloads carry the
|
|
431
|
+
* page's `featureImage` at whatever variant was indexed -- in practice
|
|
432
|
+
* `original`, which is up to 5120px for a card that renders a few hundred wide.
|
|
433
|
+
*
|
|
434
|
+
* The variant is swapped on the segment rather than by replacing the string
|
|
435
|
+
* "original" anywhere in the URL, which would corrupt any id that happens to
|
|
436
|
+
* contain it. Any query or hash is preserved.
|
|
437
|
+
*
|
|
438
|
+
* A template opts in with `data-leed-variant`; `medium` is used when it says
|
|
439
|
+
* nothing, so a site that ejected `stub-template.hbs` before this existed still
|
|
440
|
+
* stops shipping full-size images.
|
|
441
|
+
*/
|
|
442
|
+
const CDN_IMAGE_PATH = "/cdn-cgi/imagedelivery/";
|
|
443
|
+
const DEFAULT_CARD_VARIANT = "medium";
|
|
444
|
+
|
|
445
|
+
function cardImageUrl(url, variant) {
|
|
446
|
+
const name = variant || DEFAULT_CARD_VARIANT;
|
|
447
|
+
// Only a Cloudflare Images URL has a variant segment to swap.
|
|
448
|
+
if (typeof url !== "string" || !url.includes(CDN_IMAGE_PATH)) return url;
|
|
449
|
+
|
|
450
|
+
const cut = url.search(/[?#]/);
|
|
451
|
+
const base = cut === -1 ? url : url.slice(0, cut);
|
|
452
|
+
const suffix = cut === -1 ? "" : url.slice(cut);
|
|
453
|
+
|
|
454
|
+
const lastSlash = base.lastIndexOf("/");
|
|
455
|
+
// No segment after the id means there is no variant to replace.
|
|
456
|
+
if (lastSlash === -1 || lastSlash === base.length - 1) return url;
|
|
457
|
+
|
|
458
|
+
return `${base.slice(0, lastSlash + 1)}${name}${suffix}`;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* Fill one cloned stub template from a recommendation object.
|
|
463
|
+
*
|
|
464
|
+
* `[data-leed-each="key"]` on a nested <template> repeats its content once per
|
|
465
|
+
* array element; `[data-leed-slot="key"]` receives a single value. A falsy value
|
|
466
|
+
* removes its slot element, so a page with no featureImage renders no empty
|
|
467
|
+
* <img>. Values are always written with textContent, never innerHTML — the
|
|
468
|
+
* payload is assembled from Vectorize metadata and D1/KV records and is
|
|
469
|
+
* untrusted at the DOM boundary.
|
|
470
|
+
*
|
|
471
|
+
* `dates` carries the site's `{ dateFormat, timezone }` off the build-time
|
|
472
|
+
* <template>, so a <time> slot renders the same string the rest of the page
|
|
473
|
+
* does rather than the visitor's locale default.
|
|
474
|
+
*/
|
|
475
|
+
function fillSlots(root, item, dates) {
|
|
476
|
+
// Slots FIRST, repeats second. querySelectorAll does not descend into a
|
|
477
|
+
// <template>'s content, so while the repeated rows are still inside their
|
|
478
|
+
// <template> host this pass cannot see their slots — which is exactly what we
|
|
479
|
+
// want, because those belong to a row object, not to `item`. Run the repeat
|
|
480
|
+
// pass first and this pass would find the row slots after they were spliced
|
|
481
|
+
// in, look them up against `item`, miss, and delete every one of them.
|
|
482
|
+
root.querySelectorAll("[data-leed-slot]").forEach((el) => {
|
|
483
|
+
const value = item[el.dataset.leedSlot];
|
|
484
|
+
if (!value) {
|
|
485
|
+
el.remove();
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
if (el.tagName === "IMG") {
|
|
489
|
+
el.src = cardImageUrl(value, el.dataset.leedVariant);
|
|
490
|
+
if (!el.alt) el.alt = item.title || "";
|
|
491
|
+
} else if (el.tagName === "A") {
|
|
492
|
+
el.href = value;
|
|
493
|
+
} else if (el.tagName === "TIME" && typeof value === "number") {
|
|
494
|
+
// The payload's only instant is `publishedAt`, an epoch number — so a
|
|
495
|
+
// NUMBER in a <time> is a timestamp and gets datetime + a formatted date.
|
|
496
|
+
// A <time> fed anything else (reading time, say) falls through to text,
|
|
497
|
+
// which is exactly what the build-time stub rendered into it.
|
|
498
|
+
const when = new Date(value);
|
|
499
|
+
// An out-of-range value would make toISOString() throw, rejecting
|
|
500
|
+
// recommendations() mid-loop so the remaining cards never render.
|
|
501
|
+
if (Number.isNaN(when.getTime())) {
|
|
502
|
+
el.remove();
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
el.dateTime = when.toISOString();
|
|
506
|
+
// A template can pin its own format with `data-leed-date-format` — a card
|
|
507
|
+
// is a compact surface, and the site-wide `dateFormat` is often authored
|
|
508
|
+
// for a full article header (a time, a timezone) that reads as noise next
|
|
509
|
+
// to "5 min read". Falls back to the site format when unset, so a template
|
|
510
|
+
// that predates this keeps rendering exactly what it did.
|
|
511
|
+
el.textContent = formatCardDate(when, el.dataset.leedDateFormat || dates?.dateFormat, dates?.timezone);
|
|
512
|
+
} else {
|
|
513
|
+
el.textContent = value;
|
|
514
|
+
}
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
root.querySelectorAll("[data-leed-each]").forEach((host) => {
|
|
518
|
+
const rows = item[host.dataset.leedEach];
|
|
519
|
+
if (!Array.isArray(rows) || rows.length === 0) {
|
|
520
|
+
host.remove();
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
const filled = document.createDocumentFragment();
|
|
524
|
+
rows.forEach((row) => {
|
|
525
|
+
const clone = host.content.cloneNode(true);
|
|
526
|
+
fillSlots(clone, row, dates);
|
|
527
|
+
filled.appendChild(clone);
|
|
528
|
+
});
|
|
529
|
+
host.replaceWith(filled);
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* Hide the widget when there is nothing to recommend.
|
|
535
|
+
*
|
|
536
|
+
* The `<id>-container` wrapper is the long-standing convention of the id-based
|
|
537
|
+
* API — it holds the heading as well as the cards, so hiding it is what stops a
|
|
538
|
+
* bare "Related reading" heading sitting over an empty box. A declarative host
|
|
539
|
+
* has no such convention, so the host itself is hidden.
|
|
540
|
+
*/
|
|
541
|
+
function hideEmptyRecommendations(target) {
|
|
542
|
+
const container = (target.id && document.getElementById(`${target.id}-container`)) || target;
|
|
543
|
+
// setProperty with "important" so the hide beats an `!important` display class
|
|
544
|
+
// on the container (e.g. Tailwind's `block!`), which a plain inline style
|
|
545
|
+
// cannot override.
|
|
546
|
+
container.style.setProperty("display", "none", "important");
|
|
547
|
+
container.appendChild(document.createComment("No recommendations located - container was hidden"));
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/** Render the recommendation cards into `target`. */
|
|
551
|
+
async function renderRecommendations(target) {
|
|
552
|
+
// this is a shared promise. needs to finish before we call services.
|
|
553
|
+
await leedSessionReady("pageview");
|
|
554
|
+
const items = await fetchJson("abby");
|
|
555
|
+
const template = document.getElementById("leed-stub-template");
|
|
556
|
+
|
|
557
|
+
if (!Array.isArray(items) || items.length === 0 || !template) {
|
|
558
|
+
hideEmptyRecommendations(target);
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// Stamped onto the <template> at build time by the stub-template transform,
|
|
563
|
+
// so card dates and reading times match what the rest of the site renders.
|
|
564
|
+
// `|| 0` normalizes a missing or non-numeric attribute to the "unset" value
|
|
565
|
+
// readingMinutes falls back from.
|
|
566
|
+
const dates = { dateFormat: template.dataset.dateFormat, timezone: template.dataset.timezone };
|
|
567
|
+
const readingWpm = Number(template.dataset.readingWpm) || 0;
|
|
568
|
+
items.forEach((item) => {
|
|
569
|
+
const stub = template.content.cloneNode(true);
|
|
570
|
+
// readingTime is derived, not carried on the payload — wordCount is.
|
|
571
|
+
fillSlots(stub, { ...item, readingTime: readingMinutes(item.wordCount, readingWpm) }, dates);
|
|
572
|
+
// On the fragment, so the template's root anchor is included.
|
|
573
|
+
applyReason(stub, "recommendation", item.pageId);
|
|
574
|
+
// Captured before the append empties the fragment. Inert while stubs carry
|
|
575
|
+
// no data-id — giving them one would change ClickTracker's attribution for
|
|
576
|
+
// stub clicks, which is out of scope.
|
|
577
|
+
const root = stub.firstElementChild;
|
|
578
|
+
target.appendChild(stub);
|
|
579
|
+
window.leedObserveElement?.(root);
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
/**
|
|
584
|
+
* Deprecated id-based entry point, kept working for sites that call it directly.
|
|
585
|
+
* The declarative `data-leed-recommendations` attribute is the supported API.
|
|
586
|
+
*/
|
|
587
|
+
async function recommendations(targetElementId) {
|
|
588
|
+
const target = document.getElementById(targetElementId);
|
|
589
|
+
if (target) await renderRecommendations(target);
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/**
|
|
593
|
+
* Find every recommendation widget on the page and fill it.
|
|
594
|
+
*
|
|
595
|
+
* Two ways in, both declarative — no inline script, so a site can ship a strict
|
|
596
|
+
* CSP with no `unsafe-inline`:
|
|
597
|
+
* - `data-leed-recommendations` on the element that receives the cards.
|
|
598
|
+
* - `data-leed-recommendations-target="id"` on the hidden marker the deprecated
|
|
599
|
+
* `{{{ Recommendations "id" }}}` shortcode emits, which renders into the
|
|
600
|
+
* element with that id — the shortcode's original semantic, preserved.
|
|
601
|
+
*
|
|
602
|
+
* A page with neither runs nothing and makes no request.
|
|
603
|
+
*/
|
|
604
|
+
function initRecommendations() {
|
|
605
|
+
const targets = [...document.querySelectorAll("[data-leed-recommendations]")];
|
|
606
|
+
document.querySelectorAll("[data-leed-recommendations-target]").forEach((marker) => {
|
|
607
|
+
const target = document.getElementById(marker.dataset.leedRecommendationsTarget);
|
|
608
|
+
if (target) {
|
|
609
|
+
if (!targets.includes(target)) targets.push(target);
|
|
610
|
+
} else {
|
|
611
|
+
console.warn(`(H) recommendations: no element with id "${marker.dataset.leedRecommendationsTarget}"`);
|
|
612
|
+
}
|
|
613
|
+
});
|
|
614
|
+
// Fired, not awaited — widgets fill independently, and one slow fetch must not
|
|
615
|
+
// hold up the others.
|
|
616
|
+
targets.forEach((target) => {
|
|
617
|
+
renderRecommendations(target);
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
/** Build and insert the CTA into `target`. Renders nothing when there is no CTA. */
|
|
622
|
+
async function renderCallToAction(target) {
|
|
623
|
+
// this is a shared promise. needs to finish before we call services.
|
|
624
|
+
await leedSessionReady("pageview");
|
|
625
|
+
const cta = await fetchJson("sway");
|
|
626
|
+
if (!cta?.ctaId || !cta.content) return;
|
|
627
|
+
|
|
628
|
+
const el = document.createElement("div");
|
|
629
|
+
// Both attributes are load-bearing: ClickTracker.buildClickDetails walks up
|
|
630
|
+
// from the clicked anchor to the nearest [data-id], and ElementViewTracker's
|
|
631
|
+
// selector is `body div[name="cta"][data-id]`.
|
|
632
|
+
el.setAttribute("name", "cta");
|
|
633
|
+
el.dataset.id = `cta-${cta.ctaId}`;
|
|
634
|
+
// innerHTML is correct HERE ONLY — content is the same CMS-authored HTML the
|
|
635
|
+
// pre-rendered stub used to inline.
|
|
636
|
+
el.innerHTML = cta.content;
|
|
637
|
+
applyReason(el, "cta", cta.ctaId);
|
|
638
|
+
target.appendChild(el);
|
|
639
|
+
window.leedObserveElement?.(el);
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
/**
|
|
643
|
+
* Deprecated id-based entry point, kept working for sites that call it directly.
|
|
644
|
+
* The declarative `data-leed-cta` attribute is the supported API.
|
|
645
|
+
*/
|
|
646
|
+
async function callToAction(targetElementId) {
|
|
647
|
+
const target = document.getElementById(targetElementId);
|
|
648
|
+
if (target) await renderCallToAction(target);
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
/**
|
|
652
|
+
* Find every CTA slot on the page and fill it. Same two declarative ways in as
|
|
653
|
+
* recommendations, and the same guarantee: a page with neither runs nothing and
|
|
654
|
+
* makes no request.
|
|
655
|
+
*
|
|
656
|
+
* Unlike recommendations there is no empty-state hide — a page with no CTA has
|
|
657
|
+
* always simply rendered nothing there.
|
|
658
|
+
*/
|
|
659
|
+
function initCallToAction() {
|
|
660
|
+
const targets = [...document.querySelectorAll("[data-leed-cta]")];
|
|
661
|
+
document.querySelectorAll("[data-leed-cta-target]").forEach((marker) => {
|
|
662
|
+
const target = document.getElementById(marker.dataset.leedCtaTarget);
|
|
663
|
+
if (target) {
|
|
664
|
+
if (!targets.includes(target)) targets.push(target);
|
|
665
|
+
} else {
|
|
666
|
+
console.warn(`(H) cta: no element with id "${marker.dataset.leedCtaTarget}"`);
|
|
667
|
+
}
|
|
668
|
+
});
|
|
669
|
+
// Fired, not awaited — slots fill independently.
|
|
670
|
+
targets.forEach((target) => {
|
|
671
|
+
renderCallToAction(target);
|
|
672
|
+
});
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
/**
|
|
676
|
+
* Find the nearest scrollable parent element, or return null if window is the scrollable container
|
|
677
|
+
*/
|
|
678
|
+
function findScrollableParent(element) {
|
|
679
|
+
if (!element || element === document.body || element === document.documentElement) {
|
|
680
|
+
return null; // Window is scrollable
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
let parent = element.parentElement;
|
|
684
|
+
while (parent && parent !== document.body && parent !== document.documentElement) {
|
|
685
|
+
const style = window.getComputedStyle(parent);
|
|
686
|
+
const hasOverflow =
|
|
687
|
+
style.overflow === "auto" ||
|
|
688
|
+
style.overflow === "scroll" ||
|
|
689
|
+
style.overflowY === "auto" ||
|
|
690
|
+
style.overflowY === "scroll";
|
|
691
|
+
const isScrollable = parent.scrollHeight > parent.clientHeight;
|
|
692
|
+
|
|
693
|
+
if (hasOverflow && isScrollable) {
|
|
694
|
+
return parent;
|
|
695
|
+
}
|
|
696
|
+
parent = parent.parentElement;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
return null; // No scrollable parent found, window is scrollable
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
/**
|
|
703
|
+
* Scroll status indicator and related scroll functionality
|
|
704
|
+
*/
|
|
705
|
+
function scrollStatusIndicatorHorizontalHandler(elementId = null) {
|
|
706
|
+
return {
|
|
707
|
+
percent: 0,
|
|
708
|
+
contentElement: null,
|
|
709
|
+
scrollContainer: null,
|
|
710
|
+
windowScrolls: false,
|
|
711
|
+
|
|
712
|
+
calculatePercentage() {
|
|
713
|
+
if (this.windowScrolls) {
|
|
714
|
+
// Window is scrollable - track content element's position through viewport
|
|
715
|
+
const rect = this.contentElement.getBoundingClientRect();
|
|
716
|
+
const elementTop = rect.top;
|
|
717
|
+
const elementBottom = rect.bottom;
|
|
718
|
+
const elementHeight = rect.height;
|
|
719
|
+
const viewportHeight = window.innerHeight;
|
|
720
|
+
|
|
721
|
+
// 0% when top of element reaches top of viewport, 100% when bottom reaches bottom
|
|
722
|
+
const totalDistance = viewportHeight - elementHeight;
|
|
723
|
+
const currentProgress = -elementTop;
|
|
724
|
+
|
|
725
|
+
let ratio = 0;
|
|
726
|
+
if (totalDistance > 0) {
|
|
727
|
+
ratio = (currentProgress / totalDistance) * 100;
|
|
728
|
+
} else if (totalDistance === 0) {
|
|
729
|
+
ratio = elementTop <= 0 && elementBottom >= viewportHeight ? 100 : 0;
|
|
730
|
+
} else {
|
|
731
|
+
const maxScroll = elementHeight - viewportHeight;
|
|
732
|
+
const currentScroll = -elementTop;
|
|
733
|
+
ratio = maxScroll > 0 ? (currentScroll / maxScroll) * 100 : 0;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
this.percent = Math.min(100, Math.max(0, ratio));
|
|
737
|
+
// console.log("percent (window scroll)", this.percent);
|
|
738
|
+
} else {
|
|
739
|
+
// Element container is scrollable - calculate based on contentElement's position within container
|
|
740
|
+
const containerRect = this.scrollContainer.getBoundingClientRect();
|
|
741
|
+
const contentRect = this.contentElement.getBoundingClientRect();
|
|
742
|
+
|
|
743
|
+
const containerTop = containerRect.top;
|
|
744
|
+
const containerHeight = containerRect.height;
|
|
745
|
+
const contentTop = contentRect.top;
|
|
746
|
+
const contentBottom = contentRect.bottom;
|
|
747
|
+
const contentHeight = contentRect.height;
|
|
748
|
+
|
|
749
|
+
// Positions relative to container's viewport
|
|
750
|
+
const contentTopRelative = contentTop - containerTop;
|
|
751
|
+
const contentBottomRelative = contentBottom - containerTop;
|
|
752
|
+
|
|
753
|
+
// 0% when top reaches container top, 100% when bottom reaches container bottom
|
|
754
|
+
const totalDistance = containerHeight - contentHeight;
|
|
755
|
+
const currentProgress = -contentTopRelative;
|
|
756
|
+
|
|
757
|
+
let ratio = 0;
|
|
758
|
+
if (totalDistance > 0) {
|
|
759
|
+
ratio = (currentProgress / totalDistance) * 100;
|
|
760
|
+
} else if (totalDistance === 0) {
|
|
761
|
+
ratio = contentTopRelative <= 0 && contentBottomRelative >= containerHeight ? 100 : 0;
|
|
762
|
+
} else {
|
|
763
|
+
const maxScroll = contentHeight - containerHeight;
|
|
764
|
+
const currentScroll = -contentTopRelative;
|
|
765
|
+
ratio = maxScroll > 0 ? (currentScroll / maxScroll) * 100 : 0;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
this.percent = Math.min(100, Math.max(0, ratio));
|
|
769
|
+
// console.log("percent (element container)", this.percent);
|
|
770
|
+
}
|
|
771
|
+
},
|
|
772
|
+
|
|
773
|
+
init() {
|
|
774
|
+
if (!elementId) {
|
|
775
|
+
console.warn("(H) scrollStatusIndicator: no elementId provided");
|
|
776
|
+
return;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
this.contentElement = document.getElementById(elementId);
|
|
780
|
+
if (!this.contentElement) {
|
|
781
|
+
console.warn(`(H) scrollStatusIndicator: elementId "${elementId}" not found`);
|
|
782
|
+
return;
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
// Find the nearest scrollable parent
|
|
786
|
+
const foundContainer = findScrollableParent(this.contentElement);
|
|
787
|
+
this.scrollContainer = foundContainer || window;
|
|
788
|
+
this.windowScrolls = this.scrollContainer === window;
|
|
789
|
+
|
|
790
|
+
if (!this.scrollContainer) {
|
|
791
|
+
console.warn(
|
|
792
|
+
`(H) scrollStatusIndicator: could not located a scrollable container for elementId "${elementId}"`,
|
|
793
|
+
);
|
|
794
|
+
return;
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
const scrollHandler = () => {
|
|
798
|
+
requestAnimationFrame(() => this.calculatePercentage());
|
|
799
|
+
};
|
|
800
|
+
|
|
801
|
+
this.scrollContainer.addEventListener("scroll", scrollHandler, { passive: true });
|
|
802
|
+
this.calculatePercentage();
|
|
803
|
+
},
|
|
804
|
+
};
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
// Recommendation and CTA widgets are declarative, so nothing on the page calls
|
|
808
|
+
// in — find them as soon as the DOM is walkable, whether or not this script was
|
|
809
|
+
// deferred.
|
|
810
|
+
function initLeedWidgets() {
|
|
811
|
+
initRecommendations();
|
|
812
|
+
initCallToAction();
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
if (document.readyState === "loading") {
|
|
816
|
+
document.addEventListener("DOMContentLoaded", initLeedWidgets);
|
|
817
|
+
} else {
|
|
818
|
+
initLeedWidgets();
|
|
819
|
+
}
|