@media-quest/builder 0.0.1 → 0.0.2

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.
Files changed (49) hide show
  1. package/dist/public-api.d.mts +634 -0
  2. package/dist/public-api.d.ts +634 -0
  3. package/dist/public-api.js +2161 -0
  4. package/dist/public-api.mjs +2129 -0
  5. package/package.json +12 -3
  6. package/src/Builder-option.ts +67 -0
  7. package/src/Builder-page.spec.ts +313 -0
  8. package/src/Builder-page.ts +249 -0
  9. package/src/Builder-question.spec.ts +68 -0
  10. package/src/Builder-question.ts +101 -0
  11. package/src/Builder-schema.spec.ts +302 -0
  12. package/src/Builder-schema.ts +250 -0
  13. package/src/Builder-text.spec.ts +24 -0
  14. package/src/Builder-text.ts +57 -0
  15. package/src/BuilderMainImageDto.ts +7 -0
  16. package/src/BuilderMainText.ts +81 -0
  17. package/src/BuilderMainVideoDto.ts +10 -0
  18. package/src/BuilderObject.ts +69 -0
  19. package/src/BuilderTag.ts +97 -0
  20. package/src/media-files.ts +28 -0
  21. package/src/public-api.ts +10 -6
  22. package/src/rulebuilder/Builder-condition-group.spec.ts +47 -0
  23. package/src/rulebuilder/Builder-condition-group.ts +109 -0
  24. package/src/rulebuilder/Builder-condition.spec.ts +169 -0
  25. package/src/rulebuilder/Builder-condition.ts +186 -0
  26. package/src/rulebuilder/Builder-operator.spec.ts +9 -0
  27. package/src/rulebuilder/Builder-operator.ts +31 -0
  28. package/src/rulebuilder/Builder-rule.spec.ts +207 -0
  29. package/src/rulebuilder/Builder-rule.ts +165 -0
  30. package/src/rulebuilder/RuleAction.ts +20 -0
  31. package/src/rulebuilder/RuleBuilder-test-utils.ts +254 -0
  32. package/src/rulebuilder/RuleInput.ts +44 -0
  33. package/src/rulebuilder/RuleVariable.ts +39 -0
  34. package/src/rulebuilder/SingleSelectItem.ts +135 -0
  35. package/src/rulebuilder/index.ts +22 -0
  36. package/src/rulebuilder/jump-to-action-manager.ts +33 -0
  37. package/src/rulebuilder/multi-select-item.ts +70 -0
  38. package/src/rulebuilder/page-action-manager.ts +20 -0
  39. package/src/rulebuilder/tag-action-manager.spec.ts +44 -0
  40. package/src/rulebuilder/tag-action-manager.ts +18 -0
  41. package/src/theme/AbstractThemeCompiler.ts +7 -0
  42. package/src/theme/IDefaultTheme.ts +178 -0
  43. package/src/theme/css-theme.ts +7 -0
  44. package/src/theme/default-theme-compiler.ts +395 -0
  45. package/src/theme/icon-urls.ts +29 -0
  46. package/src/theme/standard-props.ts +113 -0
  47. package/src/theme/theme-utils.ts +110 -0
  48. package/src/theme/theme1.spec.ts +52 -0
  49. package/tsconfig.json +0 -2
package/package.json CHANGED
@@ -1,10 +1,19 @@
1
1
  {
2
2
  "name": "@media-quest/builder",
3
- "version": "0.0.1",
3
+ "version": "0.0.2",
4
4
  "description": "Builder library for Media-quest schemas",
5
- "main": "./src/public-api.ts",
5
+ "main": "dist/public-api.js",
6
+ "module": "dist/public-api.mjs",
7
+ "types": "dist/public-api.d.ts",
6
8
  "license": "MIT",
9
+ "scripts": {
10
+ "check": "tsc --watch --noEmit",
11
+ "clean": "rimraf dist",
12
+ "build": "npm run clean && tsup src/public-api.ts --format cjs,esm --dts",
13
+ "prepublishOnly": "npm run build",
14
+ "postpublish": "npm run clean"
15
+ },
7
16
  "dependencies": {
8
- "@media-quest/engine": "0.0.1"
17
+ "@media-quest/engine": "0.0.2"
9
18
  }
10
19
  }
@@ -0,0 +1,67 @@
1
+ import { BuilderObjectId, BuilderObject } from "./BuilderObject";
2
+ import type { BuilderOptionTheme } from "./theme/IDefaultTheme";
3
+ import { DefaultTheme } from "./theme/IDefaultTheme";
4
+ import { AudioFile } from "./media-files";
5
+
6
+ export interface BuilderOptionDto {
7
+ readonly id: BuilderObjectId.QuestionOptionID;
8
+ readonly value: number;
9
+ readonly label: string;
10
+ readonly labelAudio?: AudioFile;
11
+ // readonly theme: BuilderPageTheme['nextButtonTheme'];
12
+ }
13
+
14
+ export class BuilderOption extends BuilderObject<"builder-question-option", BuilderOptionDto> {
15
+ readonly objectType = "builder-question-option";
16
+ theme: BuilderOptionTheme = DefaultTheme.responseButtons;
17
+
18
+ id: BuilderObjectId.QuestionOptionID;
19
+ value: number;
20
+ label = "";
21
+ private _labelAudioFile: AudioFile | false = false;
22
+ get labelAudioFile() {
23
+ return this._labelAudioFile;
24
+ }
25
+ set labelAudioFile(audioFile: AudioFile | false) {
26
+ this._labelAudioFile = audioFile;
27
+ }
28
+
29
+ private constructor(dto: BuilderOptionDto) {
30
+ super(dto);
31
+ this.id = dto.id;
32
+ this.value = dto.value;
33
+ this.label = dto.label;
34
+ // this.theme = dto.theme;
35
+ }
36
+ public static create(value: number, label: string) {
37
+ const id = BuilderObjectId.questionOptionId();
38
+ const dto: BuilderOptionDto = {
39
+ id,
40
+ value,
41
+ label,
42
+ };
43
+ const instance = new BuilderOption(dto);
44
+ return instance;
45
+ }
46
+
47
+ public static fromJson(dto: BuilderOptionDto) {
48
+ const instance = new BuilderOption(dto);
49
+ return instance;
50
+ }
51
+
52
+ toJson(): BuilderOptionDto {
53
+ const dto: BuilderOptionDto = {
54
+ id: this.id,
55
+ value: this.value,
56
+ label: this.label,
57
+ };
58
+ return dto;
59
+ }
60
+
61
+ clone(): BuilderOptionDto {
62
+ const cloneId = BuilderObjectId.questionOptionId();
63
+ const dto = this.toJson();
64
+ const cloneDto: BuilderOptionDto = { ...dto, id: cloneId };
65
+ return cloneDto;
66
+ }
67
+ }
@@ -0,0 +1,313 @@
1
+ import { BuilderPage } from "./Builder-page";
2
+ import type { BuilderPageDto } from "./Builder-page";
3
+ import type { BuilderQuestionDto } from "./Builder-question";
4
+ import { BuilderQuestion } from "./Builder-question";
5
+ import type { BuilderObjectId } from "./BuilderObject";
6
+ import { DUtil } from "@media-quest/engine";
7
+
8
+ const U = DUtil;
9
+ const deleteIdsFromPage = (page: BuilderPageDto) => {
10
+ const deleteIdsFromQuestion = (q: BuilderQuestionDto) => {
11
+ U.deleteProp(q, "id");
12
+ q.options.forEach((o) => {
13
+ U.deleteProp(o, "id");
14
+ });
15
+ };
16
+ U.deleteProp(page, "id");
17
+ page.questions.forEach(deleteIdsFromQuestion);
18
+ deleteIdsFromQuestion(page.defaultQuestion);
19
+ return page;
20
+ };
21
+
22
+ const questionPageDto: BuilderPageDto = {
23
+ _type: "question",
24
+ autoplaySequence: [],
25
+ mainText: {
26
+ text: "Velkommen til denne undersøkelsen.",
27
+ audioFile: false,
28
+ autoplayDelay: 0,
29
+ autoplay: false,
30
+ },
31
+ tags: [],
32
+
33
+ nextButton: {
34
+ id: "next-button-id-5" as BuilderObjectId.QuestionOptionID,
35
+ label: "Neste",
36
+ value: -1,
37
+ },
38
+ defaultQuestion: {
39
+ id: "q1" as BuilderObjectId.QuestionID,
40
+ prefix: "q1",
41
+ text: "sadf",
42
+ options: [
43
+ {
44
+ id: "q1-opt1" as BuilderObjectId.QuestionOptionID,
45
+ value: 0,
46
+ label: "Nei",
47
+ },
48
+ {
49
+ id: "q1-opt2" as BuilderObjectId.QuestionOptionID,
50
+ value: 1,
51
+ label: "Ja",
52
+ },
53
+ {
54
+ id: "q1-opt3" as BuilderObjectId.QuestionOptionID,
55
+ value: 9,
56
+ label: "Vet ikke",
57
+ },
58
+ ],
59
+ _type: "select-one",
60
+ },
61
+ id: "p1" as BuilderObjectId.PageID,
62
+ prefix: "",
63
+ questions: [],
64
+ };
65
+
66
+ const multiQuestionPageDto: BuilderPageDto = {
67
+ _type: "multi-select",
68
+ mainText: {
69
+ text: "Velkommen til denne undersøkelsen.",
70
+ audioFile: false,
71
+ autoplay: false,
72
+ autoplayDelay: 0,
73
+ },
74
+ autoplaySequence: [],
75
+ defaultQuestion: {
76
+ id: "q1" as BuilderObjectId.QuestionID,
77
+ prefix: "",
78
+ text: "",
79
+ options: [],
80
+ _type: "select-one",
81
+ },
82
+ tags: [],
83
+ nextButton: {
84
+ id: "next-button-id-3" as BuilderObjectId.QuestionOptionID,
85
+ label: "Neste",
86
+ value: -1,
87
+ },
88
+
89
+ id: "p2" as BuilderObjectId.PageID,
90
+ prefix: "",
91
+ questions: [
92
+ {
93
+ id: "q3" as BuilderObjectId.QuestionID,
94
+ prefix: "a",
95
+ text: "har du..",
96
+ _type: "select-one",
97
+ // options: []
98
+ options: [
99
+ {
100
+ id: "q3-opt1" as BuilderObjectId.QuestionOptionID,
101
+ value: 0,
102
+ label: "Nei",
103
+ },
104
+ {
105
+ id: "q3-opt2" as BuilderObjectId.QuestionOptionID,
106
+ value: 1,
107
+ label: "Ja",
108
+ },
109
+ {
110
+ id: "q3-opt3" as BuilderObjectId.QuestionOptionID,
111
+ value: 9,
112
+ label: "Vet ikke",
113
+ },
114
+ ],
115
+ },
116
+ {
117
+ id: "q4" as BuilderObjectId.QuestionID,
118
+ prefix: "a",
119
+ text: "har du..",
120
+ _type: "select-one",
121
+ // options: []
122
+ options: [
123
+ {
124
+ id: "q4-opt1" as BuilderObjectId.QuestionOptionID,
125
+ value: 0,
126
+ label: "Nei",
127
+ },
128
+ {
129
+ id: "q4-opt2" as BuilderObjectId.QuestionOptionID,
130
+ value: 1,
131
+ label: "Ja",
132
+ },
133
+ {
134
+ id: "q4-opt3" as BuilderObjectId.QuestionOptionID,
135
+ value: 9,
136
+ label: "Vet ikke",
137
+ },
138
+ ],
139
+ },
140
+ ],
141
+ };
142
+
143
+ let page = BuilderPage.create("info-page", "a");
144
+
145
+ beforeEach(() => {
146
+ page = BuilderPage.create("info-page", "a");
147
+ });
148
+
149
+ describe("Builder Page", () => {
150
+ test("Can create page, and convert to json, and back", () => {
151
+ const page1 = BuilderPage.fromJson(questionPageDto);
152
+ const asJson = page1.toJson();
153
+ expect(questionPageDto).toStrictEqual(asJson);
154
+ const page2 = BuilderPage.fromJson(multiQuestionPageDto);
155
+ const asJson2 = page2.toJson();
156
+ expect(multiQuestionPageDto).toStrictEqual(asJson2);
157
+ // expect(page1.prefix).toBe(pageDto.prefix);
158
+ });
159
+ test("Can change question type", () => {
160
+ expect(page.questions.length).toBe(0);
161
+ page.addQuestion("select-one");
162
+ page.addQuestion("select-one");
163
+ expect(page.questions.length).toBe(2);
164
+ page.pageType = "question";
165
+ expect(page.questions.length).toBe(0);
166
+ });
167
+ test("Can add and delete questions", () => {
168
+ page.pageType = "multi-select";
169
+ expect(page.questions.length).toBe(1);
170
+ const q1 = page.addQuestion("select-one");
171
+ const q2 = page.addQuestion("select-one");
172
+ expect(page.questions.length).toBe(3);
173
+ page.deleteQuestion(q1);
174
+ expect(page.questions.length).toBe(2);
175
+ page.deleteQuestion(q2);
176
+ expect(page.questions.length).toBe(1);
177
+ // page.pageType = 'question';
178
+ // expect(page.questions.length).toBe(0);
179
+ });
180
+ test("Can add question at concrete index. ", () => {
181
+ page.pageType = "multi-select";
182
+ expect(page.questions.length).toBe(1);
183
+ const q1 = page.addQuestion("select-one", 0);
184
+ expect(page.questions[0].id).toBe(q1.id);
185
+ const q2 = page.addQuestion("select-one", 1);
186
+ expect(page.questions[1].id).toBe(q2.id);
187
+ expect(page.questions.length).toBe(3);
188
+ const q3 = page.addQuestion("email", 2);
189
+ expect(page.questions[2]).toBe(q3);
190
+ expect(q3.type).toBe("email");
191
+ });
192
+ test("Can clone a page and everything except id is equal ", () => {
193
+ const page1 = BuilderPage.create("info-page", "as");
194
+ const clone1 = BuilderPage.fromJson(page1.clone());
195
+ const page1Json = deleteIdsFromPage(page1.clone());
196
+ const clone1Json = deleteIdsFromPage(page1.clone());
197
+ expect(page1Json).toStrictEqual(clone1Json);
198
+
199
+ const page2 = BuilderPage.fromJson(questionPageDto);
200
+ const clone2 = BuilderPage.fromJson(page2.clone());
201
+ const page2Json = deleteIdsFromPage(page2.toJson());
202
+ const clone2Json = deleteIdsFromPage(clone2.toJson());
203
+ expect(page2Json).toStrictEqual(clone2Json);
204
+
205
+ const page3 = BuilderPage.fromJson(multiQuestionPageDto);
206
+ const clone3 = BuilderPage.fromJson(page3.clone());
207
+ const page3Json = deleteIdsFromPage(page3.toJson());
208
+ const clone3Json = deleteIdsFromPage(clone3.toJson());
209
+ expect(page3Json).toStrictEqual(clone3Json);
210
+ });
211
+ test("Can clone a page and no id is equal", () => {
212
+ const page1 = BuilderPage.create("info-page", "as");
213
+ const clone1 = BuilderPage.fromJson(page1.clone());
214
+ expect(page1.id).not.toBe(clone1.id);
215
+
216
+ const testDtoIds = (dto: BuilderPageDto) => {
217
+ const instance = BuilderPage.fromJson(dto);
218
+ const clone = BuilderPage.fromJson(instance.clone());
219
+ const p2QuestionIds = instance.questions.map((q) => q.id);
220
+ const cloneQuestionIds = clone.questions.map((q) => q.id);
221
+
222
+ // expect(instance.id).to.not.eq(clone.id);
223
+ // expect(instance.defaultQuestion.id).to.not.eq(clone.defaultQuestion.id);
224
+ expect(p2QuestionIds.length).toBe(cloneQuestionIds.length);
225
+ p2QuestionIds.forEach((id, index) => {
226
+ expect(id).not.toBe(cloneQuestionIds[index]);
227
+ });
228
+
229
+ instance.questions.forEach((q, qIndex) => {
230
+ const cloneQ = clone.questions[qIndex];
231
+ expect(cloneQ).toBeDefined();
232
+ expect(q.id).not.toBe(cloneQ.id);
233
+ q.options.forEach((op, opIndex) => {
234
+ const cloneOp = cloneQ.options[opIndex];
235
+ expect(cloneOp).toBeDefined();
236
+ expect(op.id).not.toBe(cloneOp.id);
237
+ });
238
+ });
239
+
240
+ // expect(p)
241
+ };
242
+ testDtoIds(questionPageDto);
243
+ testDtoIds(multiQuestionPageDto);
244
+ });
245
+ test("Can insert a new Question at index", () => {
246
+ const page1 = BuilderPage.create("form", "as");
247
+ const q1 = page1.addQuestion("select-one");
248
+ const q2 = page1.addQuestion("select-many");
249
+ expect(page1.questions.length).toBe(3);
250
+ expect(page1.questions[1]).toBe(q1);
251
+ const q1Clone = BuilderQuestion.fromJson(q1.clone());
252
+ const result10 = page1.insertQuestion(q1Clone, 10);
253
+ expect(result10).toBeFalsy();
254
+ const result1 = page1.insertQuestion(q1Clone, 1);
255
+ expect(result1).toBeTruthy();
256
+ expect(page1.questions[1]).toBe(q1Clone);
257
+ });
258
+ test("Can not insert a duplicate question", () => {
259
+ const page1 = BuilderPage.create("form", "as");
260
+ const q1 = page1.addQuestion("select-one");
261
+ expect(page1.questions.length).toBe(2);
262
+ expect(page1.questions[1]).toBe(q1);
263
+ const result1 = page1.insertQuestion(q1, 0);
264
+ expect(page1.questions.length).toBe(2);
265
+ expect(result1).toBeFalsy();
266
+ });
267
+
268
+ test("Can move a question", () => {
269
+ const page1 = BuilderPage.create("form", "as1");
270
+ const q1 = page1.addQuestion("select-many");
271
+ const q2 = page1.addQuestion("select-one");
272
+ const q3 = page1.addQuestion("select-one");
273
+ const q4 = page1.addQuestion("select-one");
274
+ expect(page1.questions.length).toBe(5);
275
+ const m1 = page1.moveQuestion(q1, 0);
276
+ expect(page1.questions.length).toBe(5);
277
+ expect(page1.questions[0]).toBe(q1);
278
+ expect(m1).toBeTruthy();
279
+ const illegalMove = page1.moveQuestion(q2, -1);
280
+ expect(illegalMove).toBeFalsy();
281
+ expect(page1.questions.length).toBe(5);
282
+ const m3 = page1.moveQuestion(q4, 1);
283
+ expect(m3).toBe(true);
284
+ expect(page1.questions[1]).toBe(q4);
285
+ });
286
+
287
+ test("Can not move a question that is not in questions-array", () => {
288
+ const page1 = BuilderPage.create("form", "as1");
289
+ const q1 = page1.addQuestion("select-many");
290
+ const q2 = page1.addQuestion("select-one");
291
+ expect(page1.questions.length).toBe(3);
292
+ const newQuestion = BuilderQuestion.create("select-one");
293
+ const m1 = page1.moveQuestion(newQuestion, 0);
294
+ expect(page1.questions.length).toBe(3);
295
+ expect(m1).toBe(false);
296
+ });
297
+ test("Can get all rule-variables.", () => {
298
+ const page = BuilderPage.create("question", "as1");
299
+ page.mainText.text = "Hva heter du?";
300
+ page.defaultQuestion.addOption("Ja", 1);
301
+ page.defaultQuestion.addOption("Nei", 0);
302
+ const variables = page.getQuestionVariables("ax", 1);
303
+ const first = variables[0];
304
+ expect(variables.length).toBe(1);
305
+ expect(first.options.length).toBe(2);
306
+ expect(first.options[0].value).toBe(1);
307
+ expect(first.kind === "question-variable").toBe(true);
308
+ // const newQuestion = BuilderQuestion.create('select-one');
309
+ // const m1 = page1.moveQuestion(newQuestion, 0);
310
+ // expect(page1.questions.length).toBe(3);
311
+ // expect(m1).toBe(false);
312
+ });
313
+ });
@@ -0,0 +1,249 @@
1
+ import type { BuilderQuestionDto, BuilderQuestionType } from "./Builder-question";
2
+ import { BuilderQuestion } from "./Builder-question";
3
+ import { BuilderObject, BuilderObjectId } from "./BuilderObject";
4
+ import type { BuilderOptionDto } from "./Builder-option";
5
+ import { BuilderOption } from "./Builder-option";
6
+ import type { BuilderMainVideoDto } from "./BuilderMainVideoDto";
7
+ import type { BuilderMainImageDto } from "./BuilderMainImageDto";
8
+ import type { BuilderMainTextDto } from "./BuilderMainText";
9
+ import { BuilderMainText } from "./BuilderMainText";
10
+ import { BuilderVariableOption, QuestionVariable } from "./rulebuilder/RuleVariable";
11
+ import { DUtil } from "@media-quest/engine";
12
+
13
+ const U = DUtil;
14
+ export type BuilderPageType = "info-page" | "question" | "multi-select" | "form";
15
+
16
+ export interface BuilderPageDto {
17
+ readonly id: BuilderObjectId.PageID;
18
+ prefix: string;
19
+ _type: BuilderPageType;
20
+ mainText: BuilderMainTextDto;
21
+ nextButton: BuilderOptionDto;
22
+ defaultQuestion: BuilderQuestionDto;
23
+ questions: Array<BuilderQuestionDto>;
24
+ mainMedia?: BuilderMainImageDto | BuilderMainVideoDto;
25
+ autoplaySequence: Array<string>;
26
+ tags: ReadonlyArray<string>;
27
+ }
28
+
29
+ export class BuilderPage extends BuilderObject<"builder-page", BuilderPageDto> {
30
+ readonly objectType: "builder-page" = "builder-page";
31
+ readonly id: BuilderObjectId.PageID;
32
+ private _pageType: BuilderPageType;
33
+ private _prefix = "";
34
+ private _questions: Array<BuilderQuestion> = [];
35
+ private readonly _tags: Set<string>;
36
+ private _backgroundColor = "#FFFFFF";
37
+ mainMedia: BuilderMainVideoDto | BuilderMainImageDto | false = false;
38
+ defaultQuestion: BuilderQuestion;
39
+ mainText: BuilderMainText;
40
+ nextButton = BuilderOption.create(-1, "Neste");
41
+
42
+ public static create(type: BuilderPageType, _prefix: string) {
43
+ const id = BuilderObjectId.pageId();
44
+ const mainTextDto: BuilderMainTextDto = {
45
+ text: "",
46
+ audioFile: false,
47
+ autoplay: false,
48
+ autoplayDelay: 0,
49
+ };
50
+ const nextButtonDto = BuilderOption.create(-1, "page-next-button-text").toJson();
51
+ const defaultQuestionDto = BuilderQuestion.create("select-one").toJson();
52
+ const dto: BuilderPageDto = {
53
+ _type: type,
54
+ autoplaySequence: [],
55
+ defaultQuestion: defaultQuestionDto,
56
+ id,
57
+ nextButton: nextButtonDto,
58
+ mainText: mainTextDto,
59
+ prefix: "",
60
+ questions: [],
61
+ tags: [],
62
+ };
63
+
64
+ const page = new BuilderPage(dto);
65
+ return page;
66
+ }
67
+ public static fromJson(dto: BuilderPageDto): BuilderPage {
68
+ const page = new BuilderPage(dto);
69
+ return page;
70
+ }
71
+
72
+ private constructor(dto: BuilderPageDto) {
73
+ super(dto);
74
+ this.id = dto.id;
75
+ this._pageType = dto._type;
76
+ this._prefix = dto.prefix;
77
+ this.mainText = BuilderMainText.fromJson(dto.mainText);
78
+ this.nextButton = BuilderOption.fromJson(dto.nextButton);
79
+ this.defaultQuestion = BuilderQuestion.fromJson(dto.defaultQuestion);
80
+ this._questions = dto.questions.map((q) => BuilderQuestion.fromJson(q));
81
+ const tagList: string[] = Array.isArray(dto.tags) ? dto.tags : [];
82
+ this._tags = new Set(tagList);
83
+ if (dto.mainMedia) {
84
+ this.mainMedia = dto.mainMedia;
85
+ }
86
+ this.updateRows();
87
+ }
88
+
89
+ insertQuestion(question: BuilderQuestion, atIndex: number): boolean {
90
+ const validIndexFn = U.isInRange(0, this._questions.length);
91
+ if (!validIndexFn(atIndex)) {
92
+ return false;
93
+ }
94
+ const hasQuestion = !!this._questions.find((q) => q.id === question.id);
95
+ if (hasQuestion) {
96
+ return false;
97
+ }
98
+ this._questions.splice(atIndex, 0, question);
99
+ return true;
100
+ }
101
+
102
+ addQuestion(type: BuilderQuestionType, atIndex = -1): BuilderQuestion {
103
+ const question = BuilderQuestion.create(type);
104
+ if (atIndex < this._questions.length && atIndex >= 0) {
105
+ this._questions.splice(atIndex, 0, question);
106
+ } else {
107
+ this._questions.push(question);
108
+ }
109
+ return question;
110
+ }
111
+
112
+ /**
113
+ * Move a question in questions-array
114
+ * @param question (reference)
115
+ * @param toIndex
116
+ */
117
+ moveQuestion(question: BuilderQuestion, toIndex: number): boolean {
118
+ const validToIndexFn = U.isInRange(0, this._questions.length);
119
+ if (!validToIndexFn(toIndex)) {
120
+ return false;
121
+ }
122
+ const currentIndex = this._questions.indexOf(question);
123
+ if (currentIndex < 0) {
124
+ return false;
125
+ }
126
+ this._questions.splice(currentIndex, 1);
127
+ this._questions.splice(toIndex, 0, question);
128
+ return true;
129
+ }
130
+
131
+ deleteQuestion(question: BuilderQuestion) {
132
+ this._questions = this._questions.filter((q) => q !== question);
133
+ // TODO EMIT DELETED QUESTION.
134
+ this.updateRows();
135
+ }
136
+ private updateRows() {
137
+ if (this._pageType === "question" || this._pageType === "info-page") {
138
+ this._questions = [];
139
+ }
140
+ if (this._pageType === "form" && this._questions.length === 0) {
141
+ this._questions = [];
142
+ this.addQuestion("text");
143
+ }
144
+ if (this._pageType === "multi-select" && this._questions.length === 0) {
145
+ this._questions = [];
146
+ this.addQuestion("text");
147
+ }
148
+ }
149
+
150
+ public addTag(tag: string) {
151
+ this._tags.add(tag);
152
+ }
153
+ public deleteTag(tag: string) {
154
+ this._tags.delete(tag);
155
+ }
156
+
157
+ set pageType(value: BuilderPageType) {
158
+ this._pageType = value;
159
+ // TODO Emit All questions that are deleted? Listen for removed variables??
160
+ this.updateRows();
161
+ }
162
+
163
+ getQuestionVariables(modulePrefix: string, pageNumber: number): ReadonlyArray<QuestionVariable> {
164
+ const variables: QuestionVariable[] = [];
165
+
166
+ if (this._pageType === "question") {
167
+ const pagePrefix = this.prefix;
168
+ const id = modulePrefix ? modulePrefix + "_" + pagePrefix : pagePrefix;
169
+ const label = this.mainText.text;
170
+ const op = this.defaultQuestion.options.map((o) => {
171
+ const label = o.label;
172
+ const value = o.value;
173
+ return new BuilderVariableOption(label, value);
174
+ });
175
+ const singleVar = new QuestionVariable(id, label, op, pageNumber);
176
+ variables.push(singleVar);
177
+ }
178
+ return variables;
179
+ }
180
+
181
+ get tags(): ReadonlyArray<string> {
182
+ return [...this._tags];
183
+ }
184
+ get pageType() {
185
+ return this._pageType;
186
+ }
187
+
188
+ get prefix() {
189
+ return this._prefix;
190
+ }
191
+
192
+ set prefix(value: string) {
193
+ this._prefix = value;
194
+ }
195
+
196
+ toJson(): BuilderPageDto {
197
+ const questions = this._questions.map((q) => q.toJson());
198
+ const mainText = this.mainText.toJson();
199
+ const nextButton = this.nextButton.toJson();
200
+ const mainMedia = this.mainMedia;
201
+ const dto: BuilderPageDto = {
202
+ _type: this.pageType,
203
+ mainText,
204
+ autoplaySequence: [],
205
+ nextButton,
206
+ id: this.id,
207
+ tags: [...this.tags],
208
+ prefix: this._prefix,
209
+ defaultQuestion: this.defaultQuestion.toJson(),
210
+ questions,
211
+ };
212
+ if (mainMedia) {
213
+ dto.mainMedia = mainMedia;
214
+ }
215
+ return dto;
216
+ }
217
+
218
+ clone(): BuilderPageDto {
219
+ const dto = this.toJson();
220
+ const defaultQuestionClone = this.defaultQuestion.clone();
221
+ const mainTextClone = JSON.parse(JSON.stringify(this.mainText));
222
+ // const pagesClone = this
223
+ const questionsClone = this.questions.map((q) => q.clone());
224
+ const newId = BuilderObjectId.pageId();
225
+ const clone: BuilderPageDto = {
226
+ ...dto,
227
+ id: newId,
228
+ defaultQuestion: defaultQuestionClone,
229
+ mainText: mainTextClone,
230
+ questions: questionsClone,
231
+ };
232
+ // const cloneDto
233
+ return clone;
234
+ }
235
+
236
+ get backgroundColor() {
237
+ return this._backgroundColor;
238
+ }
239
+
240
+ set backgroundColor(color: string) {
241
+ if (typeof color === "string") {
242
+ this._backgroundColor = color;
243
+ }
244
+ }
245
+
246
+ get questions(): ReadonlyArray<BuilderQuestion> {
247
+ return this._questions;
248
+ }
249
+ }