@getquick/content 0.3.1 → 0.4.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.
@@ -0,0 +1,205 @@
1
+ //#region frontend/src/forms/descriptor.ts
2
+ /** The Site path the forms handler answers under: `/gq/forms/<formId>`. */
3
+ const FORMS_PATH = "/gq/forms";
4
+ /** The descriptor's wire version; another version doesn't parse. */
5
+ const FORM_DESCRIPTOR_SCHEMA = 1;
6
+ /** The Gravity Forms block a Site's dispatcher routes to the renderer. */
7
+ const GRAVITY_FORMS_BLOCK = "gravityforms/form";
8
+ /** Field types this release renders and submits. */
9
+ const SUPPORTED_FIELD_TYPES = [
10
+ "text",
11
+ "textarea",
12
+ "email",
13
+ "phone",
14
+ "number",
15
+ "website",
16
+ "select",
17
+ "radio",
18
+ "checkbox",
19
+ "consent",
20
+ "name",
21
+ "hidden",
22
+ "html",
23
+ "section"
24
+ ];
25
+ function isRecord(value) {
26
+ return typeof value === "object" && value !== null && !Array.isArray(value);
27
+ }
28
+ const text = (value) => typeof value === "string" ? value : "";
29
+ const optionalText = (value) => typeof value === "string" && value !== "" ? value : void 0;
30
+ const finite = (value) => typeof value === "number" && Number.isFinite(value) ? value : void 0;
31
+ const INPUT_NAME = /^input_\d+(?:_\d+)?$/;
32
+ const DIAGNOSTIC_CODES = /* @__PURE__ */ new Set([
33
+ "unavailable",
34
+ "unsupported-captcha",
35
+ "unsupported-file-upload",
36
+ "unsupported-payment",
37
+ "unsupported-multi-page",
38
+ "unsupported-field",
39
+ "unsupported-conditional-logic",
40
+ "requires-login"
41
+ ]);
42
+ function parseInput(value) {
43
+ if (!isRecord(value)) return null;
44
+ const id = text(value.id);
45
+ const name = text(value.name);
46
+ if (!/^\d+(?:\.\d+)?$/.test(id) || !INPUT_NAME.test(name)) return null;
47
+ return {
48
+ id,
49
+ name,
50
+ label: text(value.label),
51
+ ...typeof value.value === "string" ? { value: value.value } : {},
52
+ ...optionalText(value.placeholder) ? { placeholder: text(value.placeholder) } : {},
53
+ ...optionalText(value.autocomplete) ? { autocomplete: text(value.autocomplete) } : {}
54
+ };
55
+ }
56
+ function parseField(value) {
57
+ if (!isRecord(value)) return null;
58
+ const id = value.id;
59
+ const type = value.type;
60
+ if (typeof id !== "number" || !Number.isInteger(id) || id < 1) return null;
61
+ if (!SUPPORTED_FIELD_TYPES.includes(type)) return null;
62
+ const inputs = Array.isArray(value.inputs) ? value.inputs.map(parseInput).filter((input) => input !== null) : [];
63
+ const field = {
64
+ id,
65
+ type,
66
+ label: text(value.label),
67
+ description: text(value.description),
68
+ required: value.required === true,
69
+ visibility: value.visibility === "hidden" ? "hidden" : "visible",
70
+ inputs
71
+ };
72
+ if (optionalText(value.placeholder)) field.placeholder = text(value.placeholder);
73
+ const maxLength = finite(value.maxLength);
74
+ if (maxLength !== void 0 && maxLength > 0) field.maxLength = Math.floor(maxLength);
75
+ if (typeof value.defaultValue === "string") field.defaultValue = value.defaultValue;
76
+ if (optionalText(value.cssClass)) field.cssClass = text(value.cssClass);
77
+ if (Array.isArray(value.choices)) field.choices = value.choices.filter(isRecord).map((choice) => ({
78
+ text: text(choice.text),
79
+ value: text(choice.value),
80
+ selected: choice.selected === true
81
+ }));
82
+ if (isRecord(value.consent)) field.consent = {
83
+ checkboxLabel: text(value.consent.checkboxLabel),
84
+ description: text(value.consent.description),
85
+ revisionId: text(value.consent.revisionId)
86
+ };
87
+ if (typeof value.content === "string") field.content = value.content;
88
+ if (isRecord(value.range)) {
89
+ const min = finite(value.range.min);
90
+ const max = finite(value.range.max);
91
+ field.range = {
92
+ ...min !== void 0 ? { min } : {},
93
+ ...max !== void 0 ? { max } : {}
94
+ };
95
+ }
96
+ return field;
97
+ }
98
+ function parseConfirmation(value) {
99
+ if (isRecord(value) && value.type === "redirect" && typeof value.url === "string") try {
100
+ const url = new URL(value.url);
101
+ if (url.protocol === "https:" || url.protocol === "http:") return {
102
+ type: "redirect",
103
+ url: url.href
104
+ };
105
+ } catch {}
106
+ return {
107
+ type: "message",
108
+ message: isRecord(value) ? text(value.message) : ""
109
+ };
110
+ }
111
+ /**
112
+ * A descriptor as the CMS delivered it, keeping only the public fields above,
113
+ * or null when it isn't one. Fields of a type this release doesn't support
114
+ * are left out; the CMS reports them as diagnostics.
115
+ */
116
+ function parseFormDescriptor(value) {
117
+ if (!isRecord(value) || value.schema !== 1) return null;
118
+ const id = value.id;
119
+ if (typeof id !== "number" || !Number.isInteger(id) || id < 1) return null;
120
+ const fields = Array.isArray(value.fields) ? value.fields.map(parseField).filter((field) => field !== null) : [];
121
+ const honeypotName = isRecord(value.honeypot) ? text(value.honeypot.name) : "";
122
+ const diagnostics = Array.isArray(value.diagnostics) ? value.diagnostics.filter(isRecord).flatMap((diagnostic) => {
123
+ const code = diagnostic.code;
124
+ if (!DIAGNOSTIC_CODES.has(code)) return [];
125
+ const fieldId = finite(diagnostic.fieldId);
126
+ return [{
127
+ code,
128
+ ...fieldId !== void 0 ? { fieldId } : {},
129
+ ...optionalText(diagnostic.type) ? { type: text(diagnostic.type) } : {},
130
+ message: text(diagnostic.message)
131
+ }];
132
+ }) : [];
133
+ return {
134
+ schema: 1,
135
+ id,
136
+ available: value.available === true,
137
+ title: text(value.title),
138
+ description: text(value.description),
139
+ submitLabel: text(value.submitLabel),
140
+ fields,
141
+ honeypot: INPUT_NAME.test(honeypotName) ? { name: honeypotName } : null,
142
+ confirmation: parseConfirmation(value.confirmation),
143
+ diagnostics
144
+ };
145
+ }
146
+ /**
147
+ * The descriptors a publication carries (the CMS's `getquickForms`, as JSON
148
+ * text or data), dropping any that don't parse. Undefined for no value.
149
+ */
150
+ function parseFormDescriptors(value) {
151
+ let parsed = value;
152
+ if (typeof parsed === "string") try {
153
+ parsed = JSON.parse(parsed);
154
+ } catch {
155
+ return;
156
+ }
157
+ if (!Array.isArray(parsed)) return void 0;
158
+ return parsed.map(parseFormDescriptor).filter((descriptor) => descriptor !== null);
159
+ }
160
+ /** The form a `gravityforms/form` block shows, from its attributes (`formId`). */
161
+ function blockFormId(attributes) {
162
+ const raw = attributes?.formId;
163
+ const id = typeof raw === "string" ? Number(raw) : raw;
164
+ return typeof id === "number" && Number.isInteger(id) && id > 0 ? id : null;
165
+ }
166
+ /** A form's descriptor among a publication's, or null. */
167
+ function findForm(forms, formId) {
168
+ return forms?.find((form) => form.id === formId) ?? null;
169
+ }
170
+ /** The path a form posts to on the Site's own origin. */
171
+ const formAction = (formId) => `${FORMS_PATH}/${formId}`;
172
+ /** Everything readiness must fail for: an unavailable form, and each unsupported field or setting. */
173
+ function formDiagnostics(descriptor) {
174
+ const diagnostics = [...descriptor.diagnostics];
175
+ if (!descriptor.available && !diagnostics.some((each) => each.code === "unavailable")) diagnostics.unshift({
176
+ code: "unavailable",
177
+ message: `Form ${descriptor.id} is missing, inactive or in the trash.`
178
+ });
179
+ return diagnostics;
180
+ }
181
+ /**
182
+ * Readiness over the forms a Site's published content references: fails for
183
+ * any diagnostic. `spam` is the Site's declared controls; declaring the
184
+ * honeypot for a form that doesn't enable it is reported as a warning.
185
+ */
186
+ function formsReadiness(descriptors, options = {}) {
187
+ const forms = [...new Map(descriptors.map((descriptor) => [descriptor.id, descriptor])).values()].map((descriptor) => {
188
+ const diagnostics = formDiagnostics(descriptor);
189
+ return {
190
+ id: descriptor.id,
191
+ title: descriptor.title,
192
+ ok: diagnostics.length === 0,
193
+ diagnostics,
194
+ honeypot: descriptor.honeypot !== null
195
+ };
196
+ });
197
+ const warnings = options.spam?.includes("honeypot") ? forms.filter((form) => !form.honeypot).map((form) => `Form ${form.id} doesn't enable Gravity Forms' anti-spam honeypot.`) : [];
198
+ return {
199
+ ok: forms.every((form) => form.ok),
200
+ forms,
201
+ warnings
202
+ };
203
+ }
204
+ //#endregion
205
+ export { blockFormId as a, formDiagnostics as c, parseFormDescriptors as d, SUPPORTED_FIELD_TYPES as i, formsReadiness as l, FORM_DESCRIPTOR_SCHEMA as n, findForm as o, GRAVITY_FORMS_BLOCK as r, formAction as s, FORMS_PATH as t, parseFormDescriptor as u };
@@ -0,0 +1,19 @@
1
+ //#region frontend/src/carousel-client.d.ts
2
+ /** Mount each Carousel once. Sites invoke on page load and call cleanup before
3
+ * client navigation replaces the page. DOM APIs are read only on invocation. */
4
+ export declare function mountDesignCarousels(root?: ParentNode): () => void;
5
+ //#endregion
6
+ /** Enhance only decorative media; nested content keeps its native interaction. */
7
+ export declare function mountVideoHero(hero: HTMLElement): () => void;
8
+ //#endregion
9
+ //#region frontend/src/navigation-client.d.ts
10
+ /** Mount the adapter's navigation markup once; cleanup restores server state.
11
+ * Native links retain ordinary Tab order (this is navigation, not an ARIA menu).
12
+ * Install navigationStyleSheet alongside the canonical presentation stylesheet. */
13
+ export declare function mountDesignNavigation(root?: ParentNode): () => void;
14
+ //#endregion
15
+ //#region frontend/src/cover-video-client.d.ts
16
+ /** Mount decorative native Cover backgrounds in neutral, saved or delivered HTML.
17
+ * No DOM access occurs until invocation. The Site owns mounting and cleanup. */
18
+ export declare function mountDesignCoverVideos(root?: ParentNode): () => void;
19
+ //#endregion