@questify/core 1.0.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,322 @@
1
+ // src/vue/index.ts
2
+ import { ref, computed, onUnmounted } from "vue";
3
+
4
+ // src/core/index.ts
5
+ function evaluateSimple(condition, responses) {
6
+ var _a;
7
+ const response = responses[condition.questionId];
8
+ if (response === void 0 || response === null) return false;
9
+ const op = (_a = condition.operator) != null ? _a : "eq";
10
+ const val = condition.value;
11
+ switch (op) {
12
+ case "eq":
13
+ return response === val;
14
+ case "neq":
15
+ return response !== val;
16
+ case "gt":
17
+ return typeof response === "number" && typeof val === "number" && response > val;
18
+ case "lt":
19
+ return typeof response === "number" && typeof val === "number" && response < val;
20
+ case "gte":
21
+ return typeof response === "number" && typeof val === "number" && response >= val;
22
+ case "lte":
23
+ return typeof response === "number" && typeof val === "number" && response <= val;
24
+ case "includes":
25
+ return Array.isArray(response) ? response.includes(val) : String(response).includes(String(val));
26
+ default:
27
+ return response === val;
28
+ }
29
+ }
30
+ var MAX_CONDITION_DEPTH = 20;
31
+ function evaluateRule(rule, responses, depth = 0) {
32
+ if (depth > MAX_CONDITION_DEPTH) {
33
+ console.warn("[questify] showIf condition tree exceeds maximum depth. Defaulting to visible.");
34
+ return true;
35
+ }
36
+ if ("and" in rule && rule.and) {
37
+ if (rule.and.length === 0) return true;
38
+ return rule.and.every((r) => evaluateRule(r, responses, depth + 1));
39
+ }
40
+ if ("or" in rule && rule.or) {
41
+ if (rule.or.length === 0) return false;
42
+ return rule.or.some((r) => evaluateRule(r, responses, depth + 1));
43
+ }
44
+ return evaluateSimple(rule, responses);
45
+ }
46
+ function getVisibleQuestions(questions, responses) {
47
+ return questions.filter((q) => q.showIf ? evaluateRule(q.showIf, responses) : true);
48
+ }
49
+ function isEmpty(value) {
50
+ if (value === void 0 || value === null) return true;
51
+ if (typeof value === "string" && value.trim() === "") return true;
52
+ if (Array.isArray(value) && value.length === 0) return true;
53
+ return false;
54
+ }
55
+ function validateAnswer(question, value) {
56
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i;
57
+ if (question.required && isEmpty(value)) {
58
+ return (_b = (_a = question.validation) == null ? void 0 : _a.message) != null ? _b : "This field is required.";
59
+ }
60
+ if (isEmpty(value)) return null;
61
+ const v = question.validation;
62
+ if (question.type === "number" || question.type === "rating") {
63
+ const num = Number(value);
64
+ if (isNaN(num) || !isFinite(num)) return "Must be a valid number.";
65
+ if ((v == null ? void 0 : v.min) !== void 0 && num < v.min) return (_c = v.message) != null ? _c : `Minimum value is ${v.min}.`;
66
+ if ((v == null ? void 0 : v.max) !== void 0 && num > v.max) return (_d = v.message) != null ? _d : `Maximum value is ${v.max}.`;
67
+ }
68
+ if (question.type === "text") {
69
+ const str = String(value).trim();
70
+ if ((v == null ? void 0 : v.minLength) !== void 0 && str.length < v.minLength)
71
+ return (_e = v.message) != null ? _e : `Minimum ${v.minLength} characters required.`;
72
+ if ((v == null ? void 0 : v.maxLength) !== void 0 && str.length > v.maxLength)
73
+ return (_f = v.message) != null ? _f : `Maximum ${v.maxLength} characters allowed.`;
74
+ if (v == null ? void 0 : v.regex) {
75
+ try {
76
+ if (!new RegExp(v.regex).test(str)) return (_g = v.message) != null ? _g : "Invalid format.";
77
+ } catch {
78
+ }
79
+ }
80
+ }
81
+ if (question.type === "email") {
82
+ const str = String(value).trim();
83
+ if ((v == null ? void 0 : v.minLength) !== void 0 && str.length < v.minLength)
84
+ return (_h = v.message) != null ? _h : `Minimum ${v.minLength} characters required.`;
85
+ if ((v == null ? void 0 : v.maxLength) !== void 0 && str.length > v.maxLength)
86
+ return (_i = v.message) != null ? _i : `Maximum ${v.maxLength} characters allowed.`;
87
+ if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(str))
88
+ return "Please enter a valid email address.";
89
+ }
90
+ if (question.type === "date") {
91
+ const str = String(value);
92
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(str)) return "Please enter a valid date (YYYY-MM-DD).";
93
+ const d = /* @__PURE__ */ new Date(str + "T00:00:00");
94
+ if (isNaN(d.getTime())) return "Please enter a valid date.";
95
+ }
96
+ return null;
97
+ }
98
+ function computeProgress(visibleQuestions, responses) {
99
+ if (visibleQuestions.length === 0) return 0;
100
+ const answered = visibleQuestions.filter((q) => !isEmpty(responses[q.id])).length;
101
+ return answered / visibleQuestions.length;
102
+ }
103
+ function checkComplete(visibleQuestions, responses) {
104
+ return visibleQuestions.filter((q) => q.required).every((q) => !isEmpty(responses[q.id]) && validateAnswer(q, responses[q.id]) === null);
105
+ }
106
+ function buildState(config, responses, questionIndex, errors) {
107
+ var _a;
108
+ const visibleQuestions = getVisibleQuestions(config.questions, responses);
109
+ const safeIndex = Math.min(questionIndex, Math.max(0, visibleQuestions.length - 1));
110
+ const question = (_a = visibleQuestions[safeIndex]) != null ? _a : null;
111
+ const visibleIds = new Set(visibleQuestions.map((q) => q.id));
112
+ const visibleErrors = Object.fromEntries(
113
+ Object.entries(errors).filter(([id]) => visibleIds.has(id))
114
+ );
115
+ return {
116
+ question,
117
+ visibleQuestions,
118
+ questionIndex: safeIndex,
119
+ totalQuestions: visibleQuestions.length,
120
+ progress: computeProgress(visibleQuestions, responses),
121
+ // [H-2] Defensive copy — prevents external mutation of internal state
122
+ responses: { ...responses },
123
+ isComplete: checkComplete(visibleQuestions, responses),
124
+ errors: visibleErrors,
125
+ canGoBack: safeIndex > 0,
126
+ canGoNext: safeIndex < visibleQuestions.length - 1
127
+ };
128
+ }
129
+ var UNSAFE_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
130
+ var Questionnaire = class {
131
+ constructor(config) {
132
+ this.subscribers = [];
133
+ var _a;
134
+ const ids = config.questions.map((q) => q.id);
135
+ const dupes = ids.filter((id, i) => ids.indexOf(id) !== i);
136
+ if (dupes.length > 0) {
137
+ console.warn(
138
+ `[questify] Duplicate question IDs detected: ${[...new Set(dupes)].join(", ")}. Each question must have a unique id.`
139
+ );
140
+ }
141
+ this.config = { mode: "step", ...config };
142
+ this.responses = { ...(_a = config.initialResponses) != null ? _a : {} };
143
+ this.questionIndex = 0;
144
+ this.errors = {};
145
+ this.cachedState = buildState(this.config, this.responses, 0, {});
146
+ }
147
+ recompute() {
148
+ this.cachedState = buildState(
149
+ this.config,
150
+ this.responses,
151
+ this.questionIndex,
152
+ this.errors
153
+ );
154
+ }
155
+ notify() {
156
+ const snapshot = this.subscribers.slice();
157
+ snapshot.forEach((s) => s(this.cachedState));
158
+ }
159
+ // ── Read ──────────────────────────────────────────────────────────────────
160
+ getState() {
161
+ return this.cachedState;
162
+ }
163
+ /**
164
+ * Returns only responses for currently visible questions.
165
+ * Use this for form submission — avoids sending hidden-branch data to the server.
166
+ *
167
+ * `state.responses` retains all answers for back-navigation; this method prunes them.
168
+ */
169
+ getSubmittableResponses() {
170
+ const { visibleQuestions } = this.cachedState;
171
+ const visibleIds = new Set(visibleQuestions.map((q) => q.id));
172
+ return Object.fromEntries(
173
+ Object.entries(this.responses).filter(([id]) => visibleIds.has(id))
174
+ );
175
+ }
176
+ subscribe(callback) {
177
+ this.subscribers.push(callback);
178
+ callback(this.cachedState);
179
+ return () => {
180
+ this.subscribers = this.subscribers.filter((s) => s !== callback);
181
+ };
182
+ }
183
+ // ── Write ─────────────────────────────────────────────────────────────────
184
+ /**
185
+ * Answer the current question (step mode).
186
+ * In `mode: "all"` prefer `answerById()`.
187
+ */
188
+ answer(value) {
189
+ const { visibleQuestions, questionIndex } = this.cachedState;
190
+ const q = visibleQuestions[questionIndex];
191
+ if (!q) return;
192
+ this._setAnswer(q, value);
193
+ }
194
+ /**
195
+ * Answer any visible question by its id — the right API for `mode: "all"`.
196
+ * Safe to call in step mode too (allows answering any step by id).
197
+ */
198
+ answerById(questionId, value) {
199
+ const q = this.cachedState.visibleQuestions.find((q2) => q2.id === questionId);
200
+ if (!q) return;
201
+ this._setAnswer(q, value);
202
+ }
203
+ _setAnswer(question, value) {
204
+ if (UNSAFE_KEYS.has(question.id)) return;
205
+ this.responses = { ...this.responses, [question.id]: value };
206
+ const error = validateAnswer(question, value);
207
+ if (error) {
208
+ this.errors = { ...this.errors, [question.id]: error };
209
+ } else {
210
+ const { [question.id]: _dropped, ...rest } = this.errors;
211
+ this.errors = rest;
212
+ }
213
+ this.recompute();
214
+ this.notify();
215
+ }
216
+ /**
217
+ * Advance to the next step (step mode).
218
+ * Validates the current required question first; populates `errors` if invalid.
219
+ */
220
+ next() {
221
+ const { visibleQuestions, questionIndex } = this.cachedState;
222
+ const q = visibleQuestions[questionIndex];
223
+ if (q) {
224
+ const error = validateAnswer(q, this.responses[q.id]);
225
+ if (error) {
226
+ this.errors = { ...this.errors, [q.id]: error };
227
+ this.recompute();
228
+ this.notify();
229
+ return;
230
+ }
231
+ }
232
+ if (questionIndex < visibleQuestions.length - 1) {
233
+ this.questionIndex = questionIndex + 1;
234
+ this.recompute();
235
+ this.notify();
236
+ }
237
+ }
238
+ /** Go to the previous step. No-op on step 0. */
239
+ back() {
240
+ if (this.questionIndex > 0) {
241
+ this.questionIndex -= 1;
242
+ this.recompute();
243
+ this.notify();
244
+ }
245
+ }
246
+ /**
247
+ * Jump to a specific step index (0-based, within visible questions).
248
+ * Out-of-range indices are silently ignored.
249
+ */
250
+ jumpTo(index) {
251
+ const { visibleQuestions } = this.cachedState;
252
+ if (index >= 0 && index < visibleQuestions.length) {
253
+ this.questionIndex = index;
254
+ this.recompute();
255
+ this.notify();
256
+ }
257
+ }
258
+ /**
259
+ * Validate all visible required questions and return a map of errors.
260
+ * Useful for "submit" gates in `mode: "all"`.
261
+ */
262
+ validate() {
263
+ const newErrors = {};
264
+ for (const q of this.cachedState.visibleQuestions) {
265
+ const error = validateAnswer(q, this.responses[q.id]);
266
+ if (error) newErrors[q.id] = error;
267
+ }
268
+ if (Object.keys(newErrors).length > 0) {
269
+ this.errors = { ...this.errors, ...newErrors };
270
+ this.recompute();
271
+ this.notify();
272
+ }
273
+ return newErrors;
274
+ }
275
+ /** Reset all responses and errors, restart from step 0. */
276
+ reset() {
277
+ var _a;
278
+ this.responses = { ...(_a = this.config.initialResponses) != null ? _a : {} };
279
+ this.questionIndex = 0;
280
+ this.errors = {};
281
+ this.recompute();
282
+ this.notify();
283
+ }
284
+ };
285
+
286
+ // src/vue/index.ts
287
+ function useQuestionnaire(config) {
288
+ const q = new Questionnaire(config);
289
+ const state = ref(q.getState());
290
+ const unsubscribe = q.subscribe((newState) => {
291
+ state.value = newState;
292
+ });
293
+ onUnmounted(() => unsubscribe());
294
+ return {
295
+ question: computed(() => state.value.question),
296
+ visibleQuestions: computed(() => state.value.visibleQuestions),
297
+ questionIndex: computed(() => state.value.questionIndex),
298
+ totalQuestions: computed(() => state.value.totalQuestions),
299
+ progress: computed(() => state.value.progress),
300
+ responses: computed(() => state.value.responses),
301
+ isComplete: computed(() => state.value.isComplete),
302
+ errors: computed(() => state.value.errors),
303
+ canGoBack: computed(() => state.value.canGoBack),
304
+ canGoNext: computed(() => state.value.canGoNext),
305
+ /** Answer the current step question (step mode). In all-mode, use `answerById`. */
306
+ answer: (value) => q.answer(value),
307
+ /** Answer any visible question by id — correct API for mode:"all". */
308
+ answerById: (questionId, value) => q.answerById(questionId, value),
309
+ next: () => q.next(),
310
+ back: () => q.back(),
311
+ jumpTo: (index) => q.jumpTo(index),
312
+ /** Validate all visible required questions. Returns id→error map. */
313
+ validate: () => q.validate(),
314
+ reset: () => q.reset(),
315
+ /** Returns only responses for currently visible questions. Use for submission. */
316
+ getSubmittableResponses: () => q.getSubmittableResponses()
317
+ };
318
+ }
319
+ export {
320
+ useQuestionnaire
321
+ };
322
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/vue/index.ts","../../src/core/index.ts"],"sourcesContent":["import { ref, computed, onUnmounted } from \"vue\";\nimport { Questionnaire } from \"../core\";\nimport type { QuestionnaireConfig, QuestionnaireState } from \"../core/types\";\n\n/**\n * Vue 3 composable for questify.\n *\n * All state values are `computed` refs — fully reactive.\n * The Questionnaire instance is created once per composable call and cleaned\n * up via `onUnmounted`. Do not call this outside of `setup()`.\n *\n * If the parent component passes different config on re-render, the composable\n * does NOT reinitialise. Use a `key` attribute on the component to force remount.\n */\nexport function useQuestionnaire(config: QuestionnaireConfig) {\n const q = new Questionnaire(config);\n const state = ref<QuestionnaireState>(q.getState());\n\n const unsubscribe = q.subscribe((newState) => {\n state.value = newState;\n });\n\n onUnmounted(() => unsubscribe());\n\n return {\n question: computed(() => state.value.question),\n visibleQuestions: computed(() => state.value.visibleQuestions),\n questionIndex: computed(() => state.value.questionIndex),\n totalQuestions: computed(() => state.value.totalQuestions),\n progress: computed(() => state.value.progress),\n responses: computed(() => state.value.responses),\n isComplete: computed(() => state.value.isComplete),\n errors: computed(() => state.value.errors),\n canGoBack: computed(() => state.value.canGoBack),\n canGoNext: computed(() => state.value.canGoNext),\n\n /** Answer the current step question (step mode). In all-mode, use `answerById`. */\n answer: (value: unknown) => q.answer(value),\n /** Answer any visible question by id — correct API for mode:\"all\". */\n answerById: (questionId: string, value: unknown) => q.answerById(questionId, value),\n next: () => q.next(),\n back: () => q.back(),\n jumpTo: (index: number) => q.jumpTo(index),\n /** Validate all visible required questions. Returns id→error map. */\n validate: () => q.validate(),\n reset: () => q.reset(),\n /** Returns only responses for currently visible questions. Use for submission. */\n getSubmittableResponses: () => q.getSubmittableResponses(),\n };\n}\n","import type {\n Question,\n QuestionnaireConfig,\n QuestionnaireState,\n ShowIfCondition,\n ShowIfRule,\n} from \"./types\";\n\nexport * from \"./types\";\n\n// ─── Conditional visibility ───────────────────────────────────────────────────\n\nfunction evaluateSimple(\n condition: ShowIfCondition,\n responses: Record<string, unknown>\n): boolean {\n const response = responses[condition.questionId];\n if (response === undefined || response === null) return false;\n\n const op = condition.operator ?? \"eq\";\n const val = condition.value;\n\n switch (op) {\n case \"eq\": return response === val;\n case \"neq\": return response !== val;\n case \"gt\":\n return typeof response === \"number\" && typeof val === \"number\" && response > val;\n case \"lt\":\n return typeof response === \"number\" && typeof val === \"number\" && response < val;\n case \"gte\":\n return typeof response === \"number\" && typeof val === \"number\" && response >= val;\n case \"lte\":\n return typeof response === \"number\" && typeof val === \"number\" && response <= val;\n case \"includes\":\n return Array.isArray(response)\n ? response.includes(val)\n : String(response).includes(String(val));\n default: return response === val;\n }\n}\n\n// Maximum nesting depth for showIf condition trees.\n// Guards against stack overflows from accidentally deep or circular configs.\nconst MAX_CONDITION_DEPTH = 20;\n\nfunction evaluateRule(\n rule: ShowIfRule,\n responses: Record<string, unknown>,\n depth = 0\n): boolean {\n if (depth > MAX_CONDITION_DEPTH) {\n // Tree too deep — default to visible so questions don't silently disappear.\n console.warn(\"[questify] showIf condition tree exceeds maximum depth. Defaulting to visible.\");\n return true;\n }\n if (\"and\" in rule && rule.and) {\n if (rule.and.length === 0) return true;\n return rule.and.every((r) => evaluateRule(r, responses, depth + 1));\n }\n if (\"or\" in rule && rule.or) {\n if (rule.or.length === 0) return false;\n return rule.or.some((r) => evaluateRule(r, responses, depth + 1));\n }\n return evaluateSimple(rule as ShowIfCondition, responses);\n}\n\nfunction getVisibleQuestions(\n questions: Question[],\n responses: Record<string, unknown>\n): Question[] {\n return questions.filter((q) => (q.showIf ? evaluateRule(q.showIf, responses) : true));\n}\n\n// ─── Validation ───────────────────────────────────────────────────────────────\n\nfunction isEmpty(value: unknown): boolean {\n if (value === undefined || value === null) return true;\n if (typeof value === \"string\" && value.trim() === \"\") return true;\n if (Array.isArray(value) && value.length === 0) return true;\n return false;\n}\n\nexport function validateAnswer(question: Question, value: unknown): string | null {\n // Required check — uses isEmpty which trims whitespace\n if (question.required && isEmpty(value)) {\n return question.validation?.message ?? \"This field is required.\";\n }\n\n // No further validation on empty optional fields\n if (isEmpty(value)) return null;\n\n const v = question.validation;\n\n // Number / rating\n if (question.type === \"number\" || question.type === \"rating\") {\n const num = Number(value);\n if (isNaN(num) || !isFinite(num)) return \"Must be a valid number.\";\n if (v?.min !== undefined && num < v.min) return v.message ?? `Minimum value is ${v.min}.`;\n if (v?.max !== undefined && num > v.max) return v.message ?? `Maximum value is ${v.max}.`;\n }\n\n // Text (length + regex)\n if (question.type === \"text\") {\n const str = String(value).trim();\n if (v?.minLength !== undefined && str.length < v.minLength)\n return v.message ?? `Minimum ${v.minLength} characters required.`;\n if (v?.maxLength !== undefined && str.length > v.maxLength)\n return v.message ?? `Maximum ${v.maxLength} characters allowed.`;\n if (v?.regex) {\n try {\n if (!new RegExp(v.regex).test(str)) return v.message ?? \"Invalid format.\";\n } catch {\n // Malformed regex in config — skip silently in production\n }\n }\n }\n\n // Email — format check always runs, regardless of whether a validation object exists\n if (question.type === \"email\") {\n const str = String(value).trim();\n if (v?.minLength !== undefined && str.length < v.minLength)\n return v.message ?? `Minimum ${v.minLength} characters required.`;\n if (v?.maxLength !== undefined && str.length > v.maxLength)\n return v.message ?? `Maximum ${v.maxLength} characters allowed.`;\n if (!/^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(str))\n return \"Please enter a valid email address.\";\n }\n\n // Date — always validate YYYY-MM-DD format to avoid timezone surprises\n if (question.type === \"date\") {\n const str = String(value);\n if (!/^\\d{4}-\\d{2}-\\d{2}$/.test(str)) return \"Please enter a valid date (YYYY-MM-DD).\";\n const d = new Date(str + \"T00:00:00\");\n if (isNaN(d.getTime())) return \"Please enter a valid date.\";\n }\n\n return null;\n}\n\n// ─── Progress + completion ────────────────────────────────────────────────────\n\nfunction computeProgress(\n visibleQuestions: Question[],\n responses: Record<string, unknown>\n): number {\n if (visibleQuestions.length === 0) return 0;\n const answered = visibleQuestions.filter((q) => !isEmpty(responses[q.id])).length;\n return answered / visibleQuestions.length;\n}\n\nfunction checkComplete(\n visibleQuestions: Question[],\n responses: Record<string, unknown>\n): boolean {\n return visibleQuestions\n .filter((q) => q.required)\n .every((q) => !isEmpty(responses[q.id]) && validateAnswer(q, responses[q.id]) === null);\n}\n\n// ─── State builder ────────────────────────────────────────────────────────────\n\nfunction buildState(\n config: QuestionnaireConfig,\n responses: Record<string, unknown>,\n questionIndex: number,\n errors: Record<string, string>\n): QuestionnaireState {\n const visibleQuestions = getVisibleQuestions(config.questions, responses);\n const safeIndex = Math.min(questionIndex, Math.max(0, visibleQuestions.length - 1));\n const question = visibleQuestions[safeIndex] ?? null;\n\n // [H-1] Filter stale errors for hidden questions\n const visibleIds = new Set(visibleQuestions.map((q) => q.id));\n const visibleErrors = Object.fromEntries(\n Object.entries(errors).filter(([id]) => visibleIds.has(id))\n );\n\n return {\n question,\n visibleQuestions,\n questionIndex: safeIndex,\n totalQuestions: visibleQuestions.length,\n progress: computeProgress(visibleQuestions, responses),\n // [H-2] Defensive copy — prevents external mutation of internal state\n responses: { ...responses },\n isComplete: checkComplete(visibleQuestions, responses),\n errors: visibleErrors,\n canGoBack: safeIndex > 0,\n canGoNext: safeIndex < visibleQuestions.length - 1,\n };\n}\n\n// ─── Questionnaire class ──────────────────────────────────────────────────────\n\n// Keys that must never become property names on a plain object.\n// Guards against prototype pollution via developer-supplied question IDs.\nconst UNSAFE_KEYS = new Set([\"__proto__\", \"constructor\", \"prototype\"]);\n\ntype Subscriber = (state: QuestionnaireState) => void;\n\nexport class Questionnaire {\n private config: QuestionnaireConfig;\n private responses: Record<string, unknown>;\n private questionIndex: number;\n private errors: Record<string, string>;\n private subscribers: Subscriber[] = [];\n private cachedState: QuestionnaireState;\n\n constructor(config: QuestionnaireConfig) {\n // [M-1] Warn on duplicate IDs — always a config bug, regardless of environment\n const ids = config.questions.map((q) => q.id);\n const dupes = ids.filter((id, i) => ids.indexOf(id) !== i);\n if (dupes.length > 0) {\n console.warn(\n `[questify] Duplicate question IDs detected: ${[...new Set(dupes)].join(\", \")}. ` +\n `Each question must have a unique id.`\n );\n }\n\n this.config = { mode: \"step\", ...config };\n this.responses = { ...(config.initialResponses ?? {}) };\n this.questionIndex = 0;\n this.errors = {};\n this.cachedState = buildState(this.config, this.responses, 0, {});\n }\n\n private recompute(): void {\n this.cachedState = buildState(\n this.config,\n this.responses,\n this.questionIndex,\n this.errors\n );\n }\n\n private notify(): void {\n // [M-5] Snapshot before iterating — safe against subscribe/unsubscribe during emit\n const snapshot = this.subscribers.slice();\n snapshot.forEach((s) => s(this.cachedState));\n }\n\n // ── Read ──────────────────────────────────────────────────────────────────\n\n getState(): QuestionnaireState {\n return this.cachedState;\n }\n\n /**\n * Returns only responses for currently visible questions.\n * Use this for form submission — avoids sending hidden-branch data to the server.\n *\n * `state.responses` retains all answers for back-navigation; this method prunes them.\n */\n getSubmittableResponses(): Record<string, unknown> {\n const { visibleQuestions } = this.cachedState;\n const visibleIds = new Set(visibleQuestions.map((q) => q.id));\n return Object.fromEntries(\n Object.entries(this.responses).filter(([id]) => visibleIds.has(id))\n );\n }\n\n subscribe(callback: Subscriber): () => void {\n this.subscribers.push(callback);\n callback(this.cachedState);\n return () => {\n this.subscribers = this.subscribers.filter((s) => s !== callback);\n };\n }\n\n // ── Write ─────────────────────────────────────────────────────────────────\n\n /**\n * Answer the current question (step mode).\n * In `mode: \"all\"` prefer `answerById()`.\n */\n answer(value: unknown): void {\n const { visibleQuestions, questionIndex } = this.cachedState;\n const q = visibleQuestions[questionIndex];\n if (!q) return;\n this._setAnswer(q, value);\n }\n\n /**\n * Answer any visible question by its id — the right API for `mode: \"all\"`.\n * Safe to call in step mode too (allows answering any step by id).\n */\n answerById(questionId: string, value: unknown): void {\n const q = this.cachedState.visibleQuestions.find((q) => q.id === questionId);\n if (!q) return;\n this._setAnswer(q, value);\n }\n\n private _setAnswer(question: Question, value: unknown): void {\n // Guard against prototype pollution — unsafe IDs are silently ignored.\n if (UNSAFE_KEYS.has(question.id)) return;\n this.responses = { ...this.responses, [question.id]: value };\n\n const error = validateAnswer(question, value);\n if (error) {\n this.errors = { ...this.errors, [question.id]: error };\n } else {\n const { [question.id]: _dropped, ...rest } = this.errors;\n this.errors = rest;\n }\n\n this.recompute();\n this.notify();\n }\n\n /**\n * Advance to the next step (step mode).\n * Validates the current required question first; populates `errors` if invalid.\n */\n next(): void {\n const { visibleQuestions, questionIndex } = this.cachedState;\n const q = visibleQuestions[questionIndex];\n\n if (q) {\n const error = validateAnswer(q, this.responses[q.id]);\n if (error) {\n this.errors = { ...this.errors, [q.id]: error };\n this.recompute();\n this.notify();\n return;\n }\n }\n\n if (questionIndex < visibleQuestions.length - 1) {\n this.questionIndex = questionIndex + 1;\n this.recompute();\n this.notify();\n }\n }\n\n /** Go to the previous step. No-op on step 0. */\n back(): void {\n if (this.questionIndex > 0) {\n this.questionIndex -= 1;\n this.recompute();\n this.notify();\n }\n }\n\n /**\n * Jump to a specific step index (0-based, within visible questions).\n * Out-of-range indices are silently ignored.\n */\n jumpTo(index: number): void {\n const { visibleQuestions } = this.cachedState;\n if (index >= 0 && index < visibleQuestions.length) {\n this.questionIndex = index;\n this.recompute();\n this.notify();\n }\n }\n\n /**\n * Validate all visible required questions and return a map of errors.\n * Useful for \"submit\" gates in `mode: \"all\"`.\n */\n validate(): Record<string, string> {\n const newErrors: Record<string, string> = {};\n for (const q of this.cachedState.visibleQuestions) {\n const error = validateAnswer(q, this.responses[q.id]);\n if (error) newErrors[q.id] = error;\n }\n if (Object.keys(newErrors).length > 0) {\n this.errors = { ...this.errors, ...newErrors };\n this.recompute();\n this.notify();\n }\n return newErrors;\n }\n\n /** Reset all responses and errors, restart from step 0. */\n reset(): void {\n this.responses = { ...(this.config.initialResponses ?? {}) };\n this.questionIndex = 0;\n this.errors = {};\n this.recompute();\n this.notify();\n }\n}\n"],"mappings":"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","names":["q"]}
package/package.json ADDED
@@ -0,0 +1,71 @@
1
+ {
2
+ "name": "@questify/core",
3
+ "version": "1.0.0",
4
+ "description": "Headless, zero-dependency questionnaire engine. Works with React, Vue, Svelte, or vanilla JS.",
5
+ "keywords": [
6
+ "questionnaire",
7
+ "survey",
8
+ "quiz",
9
+ "form",
10
+ "headless",
11
+ "react",
12
+ "vue",
13
+ "typescript",
14
+ "conditional-logic",
15
+ "step-form",
16
+ "multi-step"
17
+ ],
18
+ "author": "Ashish Kumar",
19
+ "license": "MIT",
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "git+https://github.com/ashishcumar/questify.git"
23
+ },
24
+ "main": "./dist/index.js",
25
+ "module": "./dist/index.mjs",
26
+ "types": "./dist/index.d.ts",
27
+ "exports": {
28
+ ".": {
29
+ "types": "./dist/index.d.ts",
30
+ "import": "./dist/index.mjs",
31
+ "require": "./dist/index.js"
32
+ },
33
+ "./react": {
34
+ "types": "./dist/react/index.d.ts",
35
+ "import": "./dist/react/index.mjs",
36
+ "require": "./dist/react/index.js"
37
+ },
38
+ "./vue": {
39
+ "types": "./dist/vue/index.d.ts",
40
+ "import": "./dist/vue/index.mjs",
41
+ "require": "./dist/vue/index.js"
42
+ }
43
+ },
44
+ "files": [
45
+ "dist"
46
+ ],
47
+ "scripts": {
48
+ "build": "tsup",
49
+ "dev": "tsup --watch",
50
+ "lint": "tsc --noEmit"
51
+ },
52
+ "devDependencies": {
53
+ "tsup": "^8.0.0",
54
+ "typescript": "^5.0.0",
55
+ "@types/react": "^19.0.0",
56
+ "react": "^19.0.0",
57
+ "vue": "^3.0.0"
58
+ },
59
+ "peerDependencies": {
60
+ "react": ">=17.0.0",
61
+ "vue": ">=3.0.0"
62
+ },
63
+ "peerDependenciesMeta": {
64
+ "react": {
65
+ "optional": true
66
+ },
67
+ "vue": {
68
+ "optional": true
69
+ }
70
+ }
71
+ }