@explorer02/cfm-survey-sdk 0.3.9 → 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.
- package/dist/cli/index.js +35 -35
- package/dist/cli/index.mjs +35 -35
- package/package.json +1 -1
- package/templates/docs/templates/CsatMatrixScale.tsx +36 -23
- package/templates/docs/templates/CustomSliderTrack.tsx +2 -2
- package/templates/docs/templates/Header.tsx +14 -14
- package/templates/docs/templates/LikertMatrixScale.tsx +2 -2
- package/templates/docs/templates/Question.tsx +30 -23
- package/templates/docs/templates/RankOrderScale.tsx +10 -2
- package/templates/docs/templates/SliderMatrixScale.tsx +1 -1
- package/templates/docs/templates/uiConfig.ts +85 -0
- package/templates/preview-harness/preview-bridge.inline.js +55 -17
- package/templates/preview-harness/previewPages.js +2 -2
- package/templates/preview-harness/previewPages.ts +1 -1
- package/templates/preview-harness/vite-app/src/PreviewConfigContext.tsx +8 -1
- package/templates/preview-harness/vite-app/src/QuestionPreview.tsx +17 -96
- package/templates/preview-harness/vite-app/src/SurveyPagePreview.tsx +32 -8
- package/templates/preview-harness/vite-app/src/fixtures/questions.ts +47 -16
- package/templates/preview-harness/vite-app/src/globals.css +6 -0
- package/templates/preview-harness/vite-app/src/mergePreviewQuestion.ts +173 -0
- package/templates/preview-harness/vite-app/src/preview-live-overrides.css +22 -0
- package/templates/previewBridge.ts +61 -18
- package/templates/survey-theme.css +3 -0
- package/templates/wizard-dist/assets/{PreviewMock-CS4WqhDB.js → PreviewMock-BlOAvUCN.js} +1 -1
- package/templates/wizard-dist/assets/TypePanel-DcbMaw1b.js +1 -0
- package/templates/wizard-dist/assets/index-2Bi_7Fdh.js +34 -0
- package/templates/wizard-dist/index.html +1 -1
- package/templates/wizard-dist/assets/TypePanel-BgTW2c74.js +0 -1
- package/templates/wizard-dist/assets/index-hjm-fxWO.js +0 -34
|
@@ -2,6 +2,7 @@ import { useMemo, useState } from 'react';
|
|
|
2
2
|
import type { AnswerValue, SurveyQuestion } from '@explorer02/cfm-survey-sdk';
|
|
3
3
|
import Question from '@/components/Question';
|
|
4
4
|
import { PreviewConfigProvider, usePreviewConfig } from './PreviewConfigContext';
|
|
5
|
+
import { mergePreviewQuestion } from './mergePreviewQuestion';
|
|
5
6
|
|
|
6
7
|
type QuestionPreviewProps = {
|
|
7
8
|
question: SurveyQuestion;
|
|
@@ -27,103 +28,14 @@ function resolveNumberLabelColor(index: number): string {
|
|
|
27
28
|
return '';
|
|
28
29
|
}
|
|
29
30
|
|
|
30
|
-
function applyMatrixRowCount<T extends SurveyQuestion & { statementRows?: { id: string; statementText: string }[] }>(
|
|
31
|
-
q: T,
|
|
32
|
-
multiStatement: boolean | undefined,
|
|
33
|
-
): T {
|
|
34
|
-
if (!('statementRows' in q) || !Array.isArray(q.statementRows)) return q;
|
|
35
|
-
const rows = q.statementRows;
|
|
36
|
-
if (multiStatement) {
|
|
37
|
-
if (rows.length >= 2) return q;
|
|
38
|
-
return {
|
|
39
|
-
...q,
|
|
40
|
-
statementRows: [
|
|
41
|
-
...rows,
|
|
42
|
-
{ id: 'r2', statementText: 'Statement Item 2' },
|
|
43
|
-
...(rows.length === 1 ? [{ id: 'r3', statementText: 'Statement Item 3' }] : []),
|
|
44
|
-
],
|
|
45
|
-
} as T;
|
|
46
|
-
}
|
|
47
|
-
return { ...q, statementRows: rows.slice(0, 1) } as T;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
31
|
function QuestionPreviewInner({ question, initialValue }: QuestionPreviewProps) {
|
|
51
32
|
const { previewState, configRevision } = usePreviewConfig();
|
|
52
33
|
const [value, setValue] = useState<AnswerValue | undefined>(initialValue);
|
|
53
34
|
|
|
54
|
-
const mergedQuestion = useMemo(
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
if (previewState.textfieldPlaceholder && q.type === 'TEXTFIELD') {
|
|
59
|
-
q = { ...q, placeholder: previewState.textfieldPlaceholder };
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
if (previewState.npsButtonStyle && q.type === 'NPS_SCALE') {
|
|
63
|
-
const style = previewState.npsButtonStyle;
|
|
64
|
-
const buttonStyle =
|
|
65
|
-
style === 'numbered'
|
|
66
|
-
? 'numbered'
|
|
67
|
-
: style === 'pill'
|
|
68
|
-
? 'pill'
|
|
69
|
-
: style === 'emoji'
|
|
70
|
-
? 'emoji'
|
|
71
|
-
: 'standard';
|
|
72
|
-
q = { ...q, buttonStyle };
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
if (previewState.hintMinText !== undefined || previewState.hintMaxText !== undefined) {
|
|
76
|
-
if (q.type === 'NPS_SCALE') {
|
|
77
|
-
q = {
|
|
78
|
-
...q,
|
|
79
|
-
minLabel: previewState.hintMinText ?? q.minLabel,
|
|
80
|
-
maxLabel: previewState.hintMaxText ?? q.maxLabel,
|
|
81
|
-
};
|
|
82
|
-
}
|
|
83
|
-
if (q.type === 'SLIDER_MATRIX') {
|
|
84
|
-
q = {
|
|
85
|
-
...q,
|
|
86
|
-
scaleAnchorLabels: [
|
|
87
|
-
previewState.hintMinText ?? q.scaleAnchorLabels?.[0] ?? '',
|
|
88
|
-
previewState.hintMaxText ?? q.scaleAnchorLabels?.[1] ?? '',
|
|
89
|
-
],
|
|
90
|
-
};
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
if (q.type === 'CFM_MATRIX') {
|
|
95
|
-
if (previewKey === 'CFM_bipolar') {
|
|
96
|
-
q = {
|
|
97
|
-
...q,
|
|
98
|
-
statementLayout: 'bipolar',
|
|
99
|
-
showColumnHeaders: false,
|
|
100
|
-
gridLayout: 'standard',
|
|
101
|
-
transposeTable: false,
|
|
102
|
-
};
|
|
103
|
-
} else if (previewKey === 'CFM_transpose') {
|
|
104
|
-
q = { ...q, statementLayout: 'likert', transposeTable: true, gridLayout: 'standard' };
|
|
105
|
-
} else if (previewKey === 'CFM_carousel') {
|
|
106
|
-
q = { ...q, statementLayout: 'likert', gridLayout: 'carousel', transposeTable: false };
|
|
107
|
-
} else if (previewKey === 'CFM_dropdown') {
|
|
108
|
-
q = { ...q, statementLayout: 'likert', gridLayout: 'dropdown', transposeTable: false };
|
|
109
|
-
} else {
|
|
110
|
-
q = { ...q, statementLayout: 'likert', gridLayout: 'standard', transposeTable: false };
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
if (q.type === 'RANK_ORDER') {
|
|
115
|
-
if (previewState.rankVariant === 'drag' || previewKey === 'RANK_drag') {
|
|
116
|
-
q = { ...q, interactionMode: 'dragAndDrop', optionDisplay: 'textAndImage' };
|
|
117
|
-
}
|
|
118
|
-
if (previewState.rankVariant === 'dropdown' || previewKey === 'RANK_dropdown') {
|
|
119
|
-
q = { ...q, interactionMode: 'dropdown', optionDisplay: 'textAndImage' };
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
q = applyMatrixRowCount(q, previewState.multiStatement);
|
|
124
|
-
|
|
125
|
-
return q;
|
|
126
|
-
}, [question, previewState]);
|
|
35
|
+
const mergedQuestion = useMemo(
|
|
36
|
+
() => mergePreviewQuestion(question, previewState),
|
|
37
|
+
[question, previewState],
|
|
38
|
+
);
|
|
127
39
|
|
|
128
40
|
const heatmapInitial =
|
|
129
41
|
previewState.heatmapPreviewPin && mergedQuestion.type === 'HEATMAP'
|
|
@@ -136,16 +48,25 @@ function QuestionPreviewInner({ question, initialValue }: QuestionPreviewProps)
|
|
|
136
48
|
]
|
|
137
49
|
: initialValue;
|
|
138
50
|
|
|
51
|
+
const rankDropdownInitial =
|
|
52
|
+
mergedQuestion.type === 'RANK_ORDER' &&
|
|
53
|
+
(mergedQuestion as { interactionMode?: string }).interactionMode === 'dropdown' &&
|
|
54
|
+
value === undefined
|
|
55
|
+
? { o1: 1 }
|
|
56
|
+
: undefined;
|
|
57
|
+
|
|
58
|
+
const resolvedValue = heatmapInitial ?? rankDropdownInitial ?? value;
|
|
59
|
+
|
|
139
60
|
return (
|
|
140
61
|
<div
|
|
141
|
-
className="mx-auto max-w-
|
|
142
|
-
style={{ fontFamily: 'var(--cfm-font-family)' }}
|
|
62
|
+
className="mx-auto w-full min-w-0 max-w-full overflow-x-auto px-4 py-6 box-border"
|
|
63
|
+
style={{ fontFamily: 'var(--cfm-font-family)', maxWidth: 'min(100%, var(--cfm-max-width, 48rem))' }}
|
|
143
64
|
data-cfm-question-area
|
|
144
65
|
>
|
|
145
66
|
<Question
|
|
146
67
|
key={configRevision}
|
|
147
68
|
question={mergedQuestion}
|
|
148
|
-
selectedValue={
|
|
69
|
+
selectedValue={resolvedValue}
|
|
149
70
|
allAnswers={{}}
|
|
150
71
|
allQuestions={[mergedQuestion]}
|
|
151
72
|
onSelect={setValue}
|
|
@@ -1,26 +1,43 @@
|
|
|
1
|
-
import { useState } from 'react';
|
|
2
|
-
import type { AnswerValue } from '@explorer02/cfm-survey-sdk';
|
|
1
|
+
import { useMemo, useState } from 'react';
|
|
2
|
+
import type { AnswerValue, MatrixRowAnswers } from '@explorer02/cfm-survey-sdk';
|
|
3
3
|
import SurveyStickyChrome from '@/components/SurveyStickyChrome';
|
|
4
4
|
import LanguageSelector from '@/components/LanguageSelector';
|
|
5
5
|
import Question from '@/components/Question';
|
|
6
6
|
import Footer from '@/components/Footer';
|
|
7
7
|
import { PreviewConfigProvider, usePreviewConfig } from './PreviewConfigContext';
|
|
8
|
+
import { mergeSurveyPageQuestion } from './mergePreviewQuestion';
|
|
8
9
|
import {
|
|
9
10
|
FIXTURE_SURVEY_PAGE_MCQ,
|
|
11
|
+
FIXTURE_SURVEY_PAGE_CFM_LIKERT,
|
|
10
12
|
FIXTURE_SURVEY_PAGE_NPS,
|
|
11
13
|
PREVIEW_LANGUAGES,
|
|
12
14
|
} from './fixtures/questions';
|
|
13
15
|
|
|
14
16
|
function SurveyPagePreviewInner() {
|
|
15
|
-
const { configRevision } = usePreviewConfig();
|
|
17
|
+
const { previewState, configRevision } = usePreviewConfig();
|
|
16
18
|
const [lang, setLang] = useState('en');
|
|
17
19
|
const [mcqAnswer, setMcqAnswer] = useState<AnswerValue>('a');
|
|
20
|
+
const [matrixAnswer, setMatrixAnswer] = useState<MatrixRowAnswers>({});
|
|
18
21
|
const [npsAnswer, setNpsAnswer] = useState<AnswerValue>(8);
|
|
19
22
|
|
|
20
|
-
const
|
|
23
|
+
const mcqQuestion = useMemo(
|
|
24
|
+
() => mergeSurveyPageQuestion(FIXTURE_SURVEY_PAGE_MCQ, previewState),
|
|
25
|
+
[previewState],
|
|
26
|
+
);
|
|
27
|
+
const matrixQuestion = useMemo(
|
|
28
|
+
() => mergeSurveyPageQuestion(FIXTURE_SURVEY_PAGE_CFM_LIKERT, previewState),
|
|
29
|
+
[previewState],
|
|
30
|
+
);
|
|
31
|
+
const npsQuestion = useMemo(
|
|
32
|
+
() => mergeSurveyPageQuestion(FIXTURE_SURVEY_PAGE_NPS, previewState),
|
|
33
|
+
[previewState],
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
const allQuestions = [mcqQuestion, matrixQuestion, npsQuestion];
|
|
21
37
|
const allAnswers = {
|
|
22
|
-
[
|
|
23
|
-
[
|
|
38
|
+
[mcqQuestion.id]: mcqAnswer,
|
|
39
|
+
[matrixQuestion.id]: matrixAnswer,
|
|
40
|
+
[npsQuestion.id]: npsAnswer,
|
|
24
41
|
};
|
|
25
42
|
|
|
26
43
|
return (
|
|
@@ -45,14 +62,21 @@ function SurveyPagePreviewInner() {
|
|
|
45
62
|
|
|
46
63
|
<div className="space-y-8" key={configRevision}>
|
|
47
64
|
<Question
|
|
48
|
-
question={
|
|
65
|
+
question={mcqQuestion}
|
|
49
66
|
selectedValue={mcqAnswer}
|
|
50
67
|
allAnswers={allAnswers}
|
|
51
68
|
allQuestions={allQuestions}
|
|
52
69
|
onSelect={setMcqAnswer}
|
|
53
70
|
/>
|
|
54
71
|
<Question
|
|
55
|
-
question={
|
|
72
|
+
question={matrixQuestion}
|
|
73
|
+
selectedValue={matrixAnswer}
|
|
74
|
+
allAnswers={allAnswers}
|
|
75
|
+
allQuestions={allQuestions}
|
|
76
|
+
onSelect={setMatrixAnswer}
|
|
77
|
+
/>
|
|
78
|
+
<Question
|
|
79
|
+
question={npsQuestion}
|
|
56
80
|
selectedValue={npsAnswer}
|
|
57
81
|
allAnswers={allAnswers}
|
|
58
82
|
allQuestions={allQuestions}
|
|
@@ -19,14 +19,37 @@ const LIKERT_COLUMNS = [
|
|
|
19
19
|
{ id: 'c5', label: 'Strongly Agree' },
|
|
20
20
|
];
|
|
21
21
|
|
|
22
|
-
const MATRIX_ROW_SINGLE = [{ id: 'r1', statementText: 'Statement Item 1' }];
|
|
22
|
+
export const MATRIX_ROW_SINGLE = [{ id: 'r1', statementText: 'Statement Item 1' }];
|
|
23
23
|
|
|
24
|
-
const MATRIX_ROWS_MULTI = [
|
|
24
|
+
export const MATRIX_ROWS_MULTI = [
|
|
25
25
|
{ id: 'r1', statementText: 'Statement Item 1' },
|
|
26
26
|
{ id: 'r2', statementText: 'Statement Item 2' },
|
|
27
27
|
{ id: 'r3', statementText: 'Statement Item 3' },
|
|
28
28
|
];
|
|
29
29
|
|
|
30
|
+
/** Bipolar matrix preview rows — left statement + matching right opposite per row. */
|
|
31
|
+
export const BIPOLAR_ROWS_SINGLE = [
|
|
32
|
+
{ id: 'r1', statementText: 'Statement 1', oppositeStatementText: 'Opposite 1' },
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
export const BIPOLAR_ROWS_MULTI = [
|
|
36
|
+
{ id: 'r1', statementText: 'Statement 1', oppositeStatementText: 'Opposite 1' },
|
|
37
|
+
{ id: 'r2', statementText: 'Statement 2', oppositeStatementText: 'Opposite 2' },
|
|
38
|
+
{ id: 'r3', statementText: 'Statement 3', oppositeStatementText: 'Opposite 3' },
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
const SLIDER_ROW_DEFAULTS = { min: 0, max: 10, step: 1 };
|
|
42
|
+
|
|
43
|
+
export const SLIDER_ROWS_SINGLE = [
|
|
44
|
+
{ id: 'r1', statementText: 'Statement Item 1', ...SLIDER_ROW_DEFAULTS },
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
export const SLIDER_ROWS_MULTI = [
|
|
48
|
+
{ id: 'r1', statementText: 'Statement Item 1', ...SLIDER_ROW_DEFAULTS },
|
|
49
|
+
{ id: 'r2', statementText: 'Statement Item 2', ...SLIDER_ROW_DEFAULTS },
|
|
50
|
+
{ id: 'r3', statementText: 'Statement Item 3', ...SLIDER_ROW_DEFAULTS },
|
|
51
|
+
];
|
|
52
|
+
|
|
30
53
|
const SLIDER_TICKS = Array.from({ length: 11 }, (_, i) => ({
|
|
31
54
|
tickPosition: i,
|
|
32
55
|
tickLabel: String(i),
|
|
@@ -79,7 +102,7 @@ export const FIXTURE_NPS = base({
|
|
|
79
102
|
type: 'NPS_SCALE',
|
|
80
103
|
questionText:
|
|
81
104
|
'Based on your experience, how likely are you to recommend [Company/Product/Service] to your friends and family?',
|
|
82
|
-
buttonStyle: '
|
|
105
|
+
buttonStyle: 'numbered',
|
|
83
106
|
minLabel: 'Not Likely At All',
|
|
84
107
|
maxLabel: 'Extremely Likely',
|
|
85
108
|
options: Array.from({ length: 11 }, (_, i) => ({
|
|
@@ -104,8 +127,8 @@ export const FIXTURE_SURVEY_PAGE_MCQ = base({
|
|
|
104
127
|
export const FIXTURE_SURVEY_PAGE_NPS = base({
|
|
105
128
|
id: 'preview-survey-nps',
|
|
106
129
|
type: 'NPS_SCALE',
|
|
107
|
-
questionNumber:
|
|
108
|
-
buttonStyle: '
|
|
130
|
+
questionNumber: 3,
|
|
131
|
+
buttonStyle: 'numbered',
|
|
109
132
|
questionText:
|
|
110
133
|
'Based on your experience, how likely are you to recommend [Company/Product/Service] to your friends and family?',
|
|
111
134
|
minLabel: 'Not Likely At All',
|
|
@@ -116,6 +139,23 @@ export const FIXTURE_SURVEY_PAGE_NPS = base({
|
|
|
116
139
|
})),
|
|
117
140
|
});
|
|
118
141
|
|
|
142
|
+
/** Likert matrix with 3 statements — used on survey_page preview for zebra row styling. */
|
|
143
|
+
export const FIXTURE_SURVEY_PAGE_CFM_LIKERT = base({
|
|
144
|
+
id: 'preview-survey-cfm-likert',
|
|
145
|
+
type: 'CFM_MATRIX',
|
|
146
|
+
questionNumber: 2,
|
|
147
|
+
questionText: 'Please rate each statement below.',
|
|
148
|
+
statementLayout: 'likert',
|
|
149
|
+
gridLayout: 'standard',
|
|
150
|
+
transposeTable: false,
|
|
151
|
+
selectionMode: 'single',
|
|
152
|
+
showColumnHeaders: true,
|
|
153
|
+
repeatColumnHeaders: false,
|
|
154
|
+
hasNotApplicableOption: false,
|
|
155
|
+
statementRows: MATRIX_ROWS_MULTI,
|
|
156
|
+
scaleColumns: LIKERT_COLUMNS,
|
|
157
|
+
});
|
|
158
|
+
|
|
119
159
|
export const FIXTURE_TEXT_SHORT = base({
|
|
120
160
|
type: 'TEXTFIELD',
|
|
121
161
|
questionText: 'Question',
|
|
@@ -154,13 +194,7 @@ export const FIXTURE_CFM_BIPOLAR = base({
|
|
|
154
194
|
showColumnHeaders: false,
|
|
155
195
|
repeatColumnHeaders: false,
|
|
156
196
|
hasNotApplicableOption: false,
|
|
157
|
-
statementRows:
|
|
158
|
-
{
|
|
159
|
-
id: 'r1',
|
|
160
|
-
statementText: 'Difficult',
|
|
161
|
-
oppositeStatementText: 'Easy',
|
|
162
|
-
},
|
|
163
|
-
],
|
|
197
|
+
statementRows: BIPOLAR_ROWS_SINGLE,
|
|
164
198
|
scaleColumns: Array.from({ length: 5 }, (_, i) => ({ id: `c${i}`, label: '' })),
|
|
165
199
|
scaleAnchorLabels: ['Hard', '', '', '', 'Easy'],
|
|
166
200
|
});
|
|
@@ -333,10 +367,7 @@ export const FIXTURE_RATING_SLIDER = base({
|
|
|
333
367
|
|
|
334
368
|
export const FIXTURE_SLIDER = base({
|
|
335
369
|
type: 'SLIDER_MATRIX',
|
|
336
|
-
statementRows:
|
|
337
|
-
{ id: 'r1', statementText: 'Statement Item 1', min: 0, max: 10, step: 1 },
|
|
338
|
-
{ id: 'r2', statementText: 'Statement Item 2', min: 0, max: 10, step: 1 },
|
|
339
|
-
],
|
|
370
|
+
statementRows: SLIDER_ROWS_MULTI.slice(0, 2),
|
|
340
371
|
scaleAnchorLabels: ['Label 1', 'Label 2'],
|
|
341
372
|
ticks: {
|
|
342
373
|
count: 11,
|
|
@@ -9,8 +9,14 @@ body {
|
|
|
9
9
|
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
10
10
|
color: #262626;
|
|
11
11
|
background-color: #ffffff;
|
|
12
|
+
max-width: 100%;
|
|
13
|
+
overflow-x: hidden;
|
|
14
|
+
box-sizing: border-box;
|
|
12
15
|
}
|
|
13
16
|
|
|
14
17
|
#root {
|
|
15
18
|
min-height: 100vh;
|
|
19
|
+
max-width: 100%;
|
|
20
|
+
overflow-x: hidden;
|
|
21
|
+
box-sizing: border-box;
|
|
16
22
|
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import type { SurveyQuestion } from '@explorer02/cfm-survey-sdk';
|
|
2
|
+
import type { PreviewStatePatch } from './PreviewConfigContext';
|
|
3
|
+
import {
|
|
4
|
+
BIPOLAR_ROWS_MULTI,
|
|
5
|
+
BIPOLAR_ROWS_SINGLE,
|
|
6
|
+
MATRIX_ROWS_MULTI,
|
|
7
|
+
MATRIX_ROW_SINGLE,
|
|
8
|
+
SLIDER_ROWS_MULTI,
|
|
9
|
+
SLIDER_ROWS_SINGLE,
|
|
10
|
+
} from './fixtures/questions';
|
|
11
|
+
|
|
12
|
+
function mapNpsButtonStyle(style: string | undefined): SurveyQuestion['buttonStyle'] {
|
|
13
|
+
if (style === 'numbered' || style === 'pill' || style === 'emoji' || style === 'standard') {
|
|
14
|
+
return style;
|
|
15
|
+
}
|
|
16
|
+
return 'numbered';
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Survey page always shows multi-row matrix for zebra styling. */
|
|
20
|
+
export function normalizeSurveyPagePreviewState(previewState: PreviewStatePatch): PreviewStatePatch {
|
|
21
|
+
return {
|
|
22
|
+
...previewState,
|
|
23
|
+
activePreviewKey: 'survey_page',
|
|
24
|
+
multiStatement: true,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
type MatrixRowCapable = SurveyQuestion & {
|
|
29
|
+
statementRows?: { id: string; statementText: string; oppositeStatementText?: string }[];
|
|
30
|
+
statementLayout?: string;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const MULTI_STATEMENT_MATRIX_TYPES = new Set([
|
|
34
|
+
'CFM_MATRIX',
|
|
35
|
+
'CSAT_MATRIX',
|
|
36
|
+
'RATING_MATRIX',
|
|
37
|
+
'SLIDER_MATRIX',
|
|
38
|
+
]);
|
|
39
|
+
|
|
40
|
+
export function applyMatrixRowCount<T extends MatrixRowCapable>(
|
|
41
|
+
q: T,
|
|
42
|
+
multiStatement: boolean | undefined,
|
|
43
|
+
options?: { preserveSurveyPageRows?: boolean },
|
|
44
|
+
): T {
|
|
45
|
+
if (!MULTI_STATEMENT_MATRIX_TYPES.has(q.type)) return q;
|
|
46
|
+
if (!('statementRows' in q) || !Array.isArray(q.statementRows)) return q;
|
|
47
|
+
|
|
48
|
+
const isBipolar = q.type === 'CFM_MATRIX' && q.statementLayout === 'bipolar';
|
|
49
|
+
const isSlider = q.type === 'SLIDER_MATRIX';
|
|
50
|
+
|
|
51
|
+
if (options?.preserveSurveyPageRows && q.statementRows.length >= 2) return q;
|
|
52
|
+
|
|
53
|
+
if (multiStatement === true) {
|
|
54
|
+
if (isBipolar) {
|
|
55
|
+
return { ...q, statementRows: BIPOLAR_ROWS_MULTI } as T;
|
|
56
|
+
}
|
|
57
|
+
if (isSlider) {
|
|
58
|
+
return { ...q, statementRows: SLIDER_ROWS_MULTI } as T;
|
|
59
|
+
}
|
|
60
|
+
if (q.statementRows.length >= 3) return q;
|
|
61
|
+
return { ...q, statementRows: MATRIX_ROWS_MULTI } as T;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (multiStatement === false) {
|
|
65
|
+
if (isBipolar) {
|
|
66
|
+
return { ...q, statementRows: BIPOLAR_ROWS_SINGLE } as T;
|
|
67
|
+
}
|
|
68
|
+
if (isSlider) {
|
|
69
|
+
return { ...q, statementRows: SLIDER_ROWS_SINGLE } as T;
|
|
70
|
+
}
|
|
71
|
+
return { ...q, statementRows: [...MATRIX_ROW_SINGLE] } as T;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Before bridge snapshot — keep fixture row count (single-row fixtures stay single).
|
|
75
|
+
return q;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function applyCfmMatrixLayout(
|
|
79
|
+
q: SurveyQuestion & {
|
|
80
|
+
statementLayout?: string;
|
|
81
|
+
gridLayout?: string;
|
|
82
|
+
transposeTable?: boolean;
|
|
83
|
+
showColumnHeaders?: boolean;
|
|
84
|
+
},
|
|
85
|
+
previewKey: string,
|
|
86
|
+
): SurveyQuestion {
|
|
87
|
+
if (previewKey === 'survey_page' || previewKey === 'CFM_likert' || !previewKey.startsWith('CFM_')) {
|
|
88
|
+
return {
|
|
89
|
+
...q,
|
|
90
|
+
statementLayout: 'likert',
|
|
91
|
+
gridLayout: 'standard',
|
|
92
|
+
transposeTable: false,
|
|
93
|
+
showColumnHeaders: true,
|
|
94
|
+
} as SurveyQuestion;
|
|
95
|
+
}
|
|
96
|
+
if (previewKey === 'CFM_bipolar') {
|
|
97
|
+
return {
|
|
98
|
+
...q,
|
|
99
|
+
statementLayout: 'bipolar',
|
|
100
|
+
showColumnHeaders: false,
|
|
101
|
+
gridLayout: 'standard',
|
|
102
|
+
transposeTable: false,
|
|
103
|
+
} as SurveyQuestion;
|
|
104
|
+
}
|
|
105
|
+
if (previewKey === 'CFM_transpose') {
|
|
106
|
+
return { ...q, statementLayout: 'likert', transposeTable: true, gridLayout: 'standard' } as SurveyQuestion;
|
|
107
|
+
}
|
|
108
|
+
if (previewKey === 'CFM_carousel') {
|
|
109
|
+
return { ...q, statementLayout: 'likert', gridLayout: 'carousel', transposeTable: false } as SurveyQuestion;
|
|
110
|
+
}
|
|
111
|
+
if (previewKey === 'CFM_dropdown') {
|
|
112
|
+
return { ...q, statementLayout: 'likert', gridLayout: 'dropdown', transposeTable: false } as SurveyQuestion;
|
|
113
|
+
}
|
|
114
|
+
return { ...q, statementLayout: 'likert', gridLayout: 'standard', transposeTable: false } as SurveyQuestion;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Apply wizard bridge previewState onto a fixture question. */
|
|
118
|
+
export function mergePreviewQuestion(
|
|
119
|
+
question: SurveyQuestion,
|
|
120
|
+
previewState: PreviewStatePatch,
|
|
121
|
+
): SurveyQuestion {
|
|
122
|
+
let q = { ...question } as SurveyQuestion;
|
|
123
|
+
const previewKey = previewState.activePreviewKey ?? '';
|
|
124
|
+
const isSurveyPage = previewKey === 'survey_page';
|
|
125
|
+
|
|
126
|
+
if (previewState.textfieldPlaceholder && q.type === 'TEXTFIELD') {
|
|
127
|
+
q = { ...q, placeholder: previewState.textfieldPlaceholder };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (q.type === 'NPS_SCALE') {
|
|
131
|
+
q = {
|
|
132
|
+
...q,
|
|
133
|
+
buttonStyle: mapNpsButtonStyle(previewState.npsButtonStyle ?? q.buttonStyle ?? 'numbered'),
|
|
134
|
+
minLabel: previewState.hintMinText ?? q.minLabel,
|
|
135
|
+
maxLabel: previewState.hintMaxText ?? q.maxLabel,
|
|
136
|
+
};
|
|
137
|
+
} else if (previewState.hintMinText !== undefined || previewState.hintMaxText !== undefined) {
|
|
138
|
+
if (q.type === 'SLIDER_MATRIX') {
|
|
139
|
+
q = {
|
|
140
|
+
...q,
|
|
141
|
+
scaleAnchorLabels: [
|
|
142
|
+
previewState.hintMinText ?? q.scaleAnchorLabels?.[0] ?? '',
|
|
143
|
+
previewState.hintMaxText ?? q.scaleAnchorLabels?.[1] ?? '',
|
|
144
|
+
],
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (q.type === 'CFM_MATRIX') {
|
|
150
|
+
q = applyCfmMatrixLayout(q, isSurveyPage ? 'survey_page' : previewKey);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (q.type === 'RANK_ORDER') {
|
|
154
|
+
if (previewState.rankVariant === 'drag' || previewKey === 'RANK_drag') {
|
|
155
|
+
q = { ...q, interactionMode: 'dragAndDrop', optionDisplay: 'textAndImage' };
|
|
156
|
+
}
|
|
157
|
+
if (previewState.rankVariant === 'dropdown' || previewKey === 'RANK_dropdown') {
|
|
158
|
+
q = { ...q, interactionMode: 'dropdown', optionDisplay: 'textAndImage' };
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return applyMatrixRowCount(q, previewState.multiStatement, {
|
|
163
|
+
preserveSurveyPageRows: isSurveyPage,
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Survey page layout — each question reflects its type-tab customization mapping. */
|
|
168
|
+
export function mergeSurveyPageQuestion(
|
|
169
|
+
question: SurveyQuestion,
|
|
170
|
+
previewState: PreviewStatePatch,
|
|
171
|
+
): SurveyQuestion {
|
|
172
|
+
return mergePreviewQuestion(question, normalizeSurveyPagePreviewState(previewState));
|
|
173
|
+
}
|
|
@@ -106,3 +106,25 @@ html.cfm-live-edit input[data-cfm-textfield]::placeholder {
|
|
|
106
106
|
color: var(--cfm-input-placeholder, #9ca3af) !important;
|
|
107
107
|
opacity: 1;
|
|
108
108
|
}
|
|
109
|
+
|
|
110
|
+
/* Keep every question-type preview inside the wizard iframe without horizontal bleed */
|
|
111
|
+
html.cfm-live-edit,
|
|
112
|
+
html.cfm-live-edit body,
|
|
113
|
+
html.cfm-live-edit #root {
|
|
114
|
+
max-width: 100%;
|
|
115
|
+
overflow-x: hidden;
|
|
116
|
+
box-sizing: border-box;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
html.cfm-live-edit [data-cfm-question-area],
|
|
120
|
+
html.cfm-live-edit .cfm-question-card {
|
|
121
|
+
max-width: 100%;
|
|
122
|
+
min-width: 0;
|
|
123
|
+
overflow-x: auto;
|
|
124
|
+
box-sizing: border-box;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
html.cfm-live-edit [data-cfm-nps-area] {
|
|
128
|
+
max-width: 100%;
|
|
129
|
+
overflow-x: auto;
|
|
130
|
+
}
|
|
@@ -43,6 +43,8 @@ function flushCssVars(): void {
|
|
|
43
43
|
delete pendingVars[key];
|
|
44
44
|
}
|
|
45
45
|
applyThemeSideEffects();
|
|
46
|
+
syncCompanyNameVisibility();
|
|
47
|
+
syncQuestionChromeVisibility();
|
|
46
48
|
}
|
|
47
49
|
|
|
48
50
|
function queueCssVars(vars: Record<string, string>): void {
|
|
@@ -58,6 +60,8 @@ function applyCssVarsImmediate(vars: Record<string, string>): void {
|
|
|
58
60
|
root.style.setProperty(key, value);
|
|
59
61
|
}
|
|
60
62
|
applyThemeSideEffects();
|
|
63
|
+
syncCompanyNameVisibility();
|
|
64
|
+
syncQuestionChromeVisibility();
|
|
61
65
|
}
|
|
62
66
|
|
|
63
67
|
function setDisplay(selector: string, visible: boolean): void {
|
|
@@ -66,6 +70,58 @@ function setDisplay(selector: string, visible: boolean): void {
|
|
|
66
70
|
});
|
|
67
71
|
}
|
|
68
72
|
|
|
73
|
+
function readCssVar(name: string, fallback: string): string {
|
|
74
|
+
return getComputedStyle(document.documentElement).getPropertyValue(name).trim() || fallback;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function isHeaderLogoVisible(): boolean {
|
|
78
|
+
const logoImg = document.querySelector(
|
|
79
|
+
'header [data-cfm-logo], .cfm-header [data-cfm-logo]',
|
|
80
|
+
) as HTMLImageElement | null;
|
|
81
|
+
return Boolean(
|
|
82
|
+
logoImg &&
|
|
83
|
+
logoImg.style.display !== 'none' &&
|
|
84
|
+
Boolean(logoImg.getAttribute('src')),
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Keep [data-cfm-company] beside logo when toggle, name, and logo are all present. */
|
|
89
|
+
function syncCompanyNameVisibility(layoutShowCompany?: boolean): void {
|
|
90
|
+
const showFromCss = readCssVar('--cfm-header-show-company', '1') !== '0';
|
|
91
|
+
const showToggle =
|
|
92
|
+
layoutShowCompany !== undefined ? Boolean(layoutShowCompany) : showFromCss;
|
|
93
|
+
|
|
94
|
+
document.querySelectorAll('[data-cfm-company]').forEach((el) => {
|
|
95
|
+
const htmlEl = el as HTMLElement;
|
|
96
|
+
const hasName = Boolean(htmlEl.textContent?.trim());
|
|
97
|
+
const show = showToggle && hasName && isHeaderLogoVisible();
|
|
98
|
+
htmlEl.style.display = show ? '' : 'none';
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function syncQuestionChromeVisibility(
|
|
103
|
+
layoutShowNumbers?: boolean,
|
|
104
|
+
layoutShowRequired?: boolean,
|
|
105
|
+
): void {
|
|
106
|
+
const showNumbers =
|
|
107
|
+
layoutShowNumbers !== undefined
|
|
108
|
+
? Boolean(layoutShowNumbers)
|
|
109
|
+
: readCssVar('--cfm-show-question-numbers', '1') !== '0';
|
|
110
|
+
const showRequired =
|
|
111
|
+
layoutShowRequired !== undefined
|
|
112
|
+
? Boolean(layoutShowRequired)
|
|
113
|
+
: readCssVar('--cfm-show-required-asterisk', '1') !== '0';
|
|
114
|
+
|
|
115
|
+
document.querySelectorAll('[data-cfm-question-number]').forEach((el) => {
|
|
116
|
+
const htmlEl = el as HTMLElement;
|
|
117
|
+
const hasNumber = Boolean(htmlEl.textContent?.trim());
|
|
118
|
+
htmlEl.style.display = showNumbers && hasNumber ? '' : 'none';
|
|
119
|
+
});
|
|
120
|
+
document.querySelectorAll('[data-cfm-required]').forEach((el) => {
|
|
121
|
+
(el as HTMLElement).style.display = showRequired ? '' : 'none';
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
69
125
|
function applyContentPatch(patch: ContentPatch): void {
|
|
70
126
|
if (patch.logoUrl !== undefined) {
|
|
71
127
|
document.querySelectorAll('[data-cfm-logo], header img, footer img').forEach((el) => {
|
|
@@ -88,23 +144,12 @@ function applyContentPatch(patch: ContentPatch): void {
|
|
|
88
144
|
htmlEl.classList.remove('hidden');
|
|
89
145
|
}
|
|
90
146
|
});
|
|
91
|
-
document.querySelectorAll('[data-cfm-company]').forEach((el) => {
|
|
92
|
-
const htmlEl = el as HTMLElement;
|
|
93
|
-
if (patch.logoUrl) {
|
|
94
|
-
if (!htmlEl.textContent?.trim()) {
|
|
95
|
-
htmlEl.style.display = 'none';
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
});
|
|
99
147
|
}
|
|
100
148
|
|
|
101
149
|
if (patch.companyName !== undefined) {
|
|
102
150
|
const name = patch.companyName ?? '';
|
|
103
151
|
document.querySelectorAll('[data-cfm-company]').forEach((el) => {
|
|
104
152
|
(el as HTMLElement).textContent = name;
|
|
105
|
-
if (!name) {
|
|
106
|
-
(el as HTMLElement).style.display = 'none';
|
|
107
|
-
}
|
|
108
153
|
});
|
|
109
154
|
document.querySelectorAll('[data-cfm-logo-text]').forEach((el) => {
|
|
110
155
|
const logoImg = document.querySelector('[data-cfm-logo]') as HTMLImageElement | null;
|
|
@@ -167,7 +212,7 @@ function applyContentPatch(patch: ContentPatch): void {
|
|
|
167
212
|
if (patch.layoutFlags) {
|
|
168
213
|
const flags = patch.layoutFlags;
|
|
169
214
|
if (flags.showCompanyName !== undefined) {
|
|
170
|
-
|
|
215
|
+
syncCompanyNameVisibility(Boolean(flags.showCompanyName));
|
|
171
216
|
}
|
|
172
217
|
if (flags.showHeader !== undefined) {
|
|
173
218
|
setDisplay('.cfm-header, header.cfm-header', Boolean(flags.showHeader));
|
|
@@ -188,10 +233,10 @@ function applyContentPatch(patch: ContentPatch): void {
|
|
|
188
233
|
setDisplay('[data-cfm-btn-back]', Boolean(flags.showBackButton));
|
|
189
234
|
}
|
|
190
235
|
if (flags.showQuestionNumbers !== undefined) {
|
|
191
|
-
|
|
236
|
+
syncQuestionChromeVisibility(Boolean(flags.showQuestionNumbers), undefined);
|
|
192
237
|
}
|
|
193
238
|
if (flags.showRequiredAsterisk !== undefined) {
|
|
194
|
-
|
|
239
|
+
syncQuestionChromeVisibility(undefined, Boolean(flags.showRequiredAsterisk));
|
|
195
240
|
}
|
|
196
241
|
if (flags.showFooterLogo !== undefined) {
|
|
197
242
|
setDisplay('[data-cfm-footer-logo]', Boolean(flags.showFooterLogo));
|
|
@@ -229,15 +274,13 @@ function applyContentPatch(patch: ContentPatch): void {
|
|
|
229
274
|
(el as HTMLElement).style.display = show ? '' : 'none';
|
|
230
275
|
});
|
|
231
276
|
}
|
|
277
|
+
syncCompanyNameVisibility();
|
|
278
|
+
syncQuestionChromeVisibility();
|
|
232
279
|
applyThemeSideEffects();
|
|
233
280
|
}
|
|
234
281
|
|
|
235
282
|
const TRAFFIC_BADGE = ['#ef4444', '#f97316', '#eab308', '#84cc16', '#22c55e', '#14b8a6', '#06b6d4', '#3b82f6', '#6366f1', '#8b5cf6', '#a855f7'];
|
|
236
283
|
|
|
237
|
-
function readCssVar(name: string, fallback: string): string {
|
|
238
|
-
return getComputedStyle(document.documentElement).getPropertyValue(name).trim() || fallback;
|
|
239
|
-
}
|
|
240
|
-
|
|
241
284
|
function applyThemeSideEffects(): void {
|
|
242
285
|
const mode = readCssVar('--cfm-number-label-mode', 'traffic');
|
|
243
286
|
const mono = readCssVar('--cfm-number-mono-color', '#9ca3af');
|