@memberjunction/ng-forms 5.43.0 → 5.44.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/lib/dynamic-form-field/dynamic-form-field.component.d.ts +12 -0
- package/dist/lib/dynamic-form-field/dynamic-form-field.component.d.ts.map +1 -1
- package/dist/lib/dynamic-form-field/dynamic-form-field.component.js +24 -2
- package/dist/lib/dynamic-form-field/dynamic-form-field.component.js.map +1 -1
- package/dist/lib/forms.module.d.ts +2 -1
- package/dist/lib/forms.module.d.ts.map +1 -1
- package/dist/lib/forms.module.js +7 -3
- package/dist/lib/forms.module.js.map +1 -1
- package/package.json +5 -3
- package/dist/__tests__/form-response-utils.test.d.ts +0 -2
- package/dist/__tests__/form-response-utils.test.d.ts.map +0 -1
- package/dist/__tests__/form-response-utils.test.js +0 -249
- package/dist/__tests__/form-response-utils.test.js.map +0 -1
- package/dist/__tests__/index.test.d.ts +0 -2
- package/dist/__tests__/index.test.d.ts.map +0 -1
- package/dist/__tests__/index.test.js +0 -7
- package/dist/__tests__/index.test.js.map +0 -1
|
@@ -51,6 +51,18 @@ export declare class DynamicFormFieldComponent implements ControlValueAccessor,
|
|
|
51
51
|
* Whether this is a choice-based question (buttongroup, radio, dropdown, checkbox).
|
|
52
52
|
*/
|
|
53
53
|
get IsChoiceQuestion(): boolean;
|
|
54
|
+
/**
|
|
55
|
+
* Textarea only: whether the value renders as formatted Markdown with an Edit toggle
|
|
56
|
+
* (question.type.markdown === true). Used for rich agent-authored content the human
|
|
57
|
+
* reviews more than types — e.g. the Plan Mode approval form.
|
|
58
|
+
*/
|
|
59
|
+
get IsMarkdown(): boolean;
|
|
60
|
+
/** Markdown textareas start in preview; the user toggles into the raw editor as needed. */
|
|
61
|
+
MarkdownEditing: boolean;
|
|
62
|
+
/** Flip a markdown textarea between rendered preview and the raw editor. */
|
|
63
|
+
ToggleMarkdownEditing(): void;
|
|
64
|
+
/** The current value as a string for the markdown preview (empty-safe). */
|
|
65
|
+
get MarkdownValue(): string;
|
|
54
66
|
/**
|
|
55
67
|
* Available options for choice-type questions.
|
|
56
68
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dynamic-form-field.component.d.ts","sourceRoot":"","sources":["../../../src/lib/dynamic-form-field/dynamic-form-field.component.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"dynamic-form-field.component.d.ts","sourceRoot":"","sources":["../../../src/lib/dynamic-form-field/dynamic-form-field.component.ts"],"names":[],"mappings":"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"}
|
|
@@ -27,6 +27,7 @@ import * as i0 from "@angular/core";
|
|
|
27
27
|
import * as i1 from "@angular/common";
|
|
28
28
|
import * as i2 from "@angular/forms";
|
|
29
29
|
import * as i3 from "@memberjunction/ng-ui-components";
|
|
30
|
+
import * as i4 from "@memberjunction/ng-markdown";
|
|
30
31
|
export class DynamicFormFieldComponent {
|
|
31
32
|
/**
|
|
32
33
|
* The question definition that drives this field's rendering.
|
|
@@ -61,6 +62,27 @@ export class DynamicFormFieldComponent {
|
|
|
61
62
|
get IsChoiceQuestion() {
|
|
62
63
|
return ['buttongroup', 'radio', 'dropdown', 'checkbox'].includes(this.QuestionType);
|
|
63
64
|
}
|
|
65
|
+
/**
|
|
66
|
+
* Textarea only: whether the value renders as formatted Markdown with an Edit toggle
|
|
67
|
+
* (question.type.markdown === true). Used for rich agent-authored content the human
|
|
68
|
+
* reviews more than types — e.g. the Plan Mode approval form.
|
|
69
|
+
*/
|
|
70
|
+
get IsMarkdown() {
|
|
71
|
+
return (this.QuestionType === 'textarea' &&
|
|
72
|
+
typeof this.Question.type === 'object' &&
|
|
73
|
+
'markdown' in this.Question.type &&
|
|
74
|
+
this.Question.type.markdown === true);
|
|
75
|
+
}
|
|
76
|
+
/** Markdown textareas start in preview; the user toggles into the raw editor as needed. */
|
|
77
|
+
MarkdownEditing = false;
|
|
78
|
+
/** Flip a markdown textarea between rendered preview and the raw editor. */
|
|
79
|
+
ToggleMarkdownEditing() {
|
|
80
|
+
this.MarkdownEditing = !this.MarkdownEditing;
|
|
81
|
+
}
|
|
82
|
+
/** The current value as a string for the markdown preview (empty-safe). */
|
|
83
|
+
get MarkdownValue() {
|
|
84
|
+
return typeof this.Value === 'string' ? this.Value : '';
|
|
85
|
+
}
|
|
64
86
|
/**
|
|
65
87
|
* Available options for choice-type questions.
|
|
66
88
|
*/
|
|
@@ -219,7 +241,7 @@ export class DynamicFormFieldComponent {
|
|
|
219
241
|
useExisting: forwardRef(() => DynamicFormFieldComponent),
|
|
220
242
|
multi: true
|
|
221
243
|
}
|
|
222
|
-
], ngImport: i0, template: "<div class=\"form-question\" [class.required]=\"Question.required\">\n <!-- Label -->\n @if (Question.label) {\n <label class=\"question-label\">\n {{ Question.label }}\n @if (Question.required) {\n <span class=\"required-indicator\">*</span>\n }\n </label>\n }\n\n <!-- Help Text -->\n @if (Question.helpText) {\n <div class=\"help-text\">\n {{ Question.helpText }}\n </div>\n }\n\n <!-- Input Container with width class -->\n <div class=\"input-container\" [ngClass]=\"WidthClass\">\n\n <!-- Text Input -->\n @if (QuestionType === 'text') {\n <input\n type=\"text\"\n class=\"mj-input question-input\"\n [value]=\"$any(Value) || ''\"\n [placeholder]=\"Placeholder\"\n [disabled]=\"Disabled\"\n (input)=\"OnValueChange($any($event.target).value)\"\n />\n }\n\n <!-- Email Input -->\n @if (QuestionType === 'email') {\n <input\n type=\"email\"\n class=\"mj-input question-input\"\n [value]=\"$any(Value) || ''\"\n [placeholder]=\"Placeholder\"\n [disabled]=\"Disabled\"\n (input)=\"OnValueChange($any($event.target).value)\"\n />\n }\n\n <!-- Textarea -->\n @if (QuestionType === 'textarea') {\n <textarea\n class=\"mj-textarea question-textarea\"\n [value]=\"$any(Value) || ''\"\n [placeholder]=\"Placeholder\"\n [disabled]=\"Disabled\"\n (input)=\"OnValueChange($any($event.target).value)\"\n rows=\"5\"\n ></textarea>\n }\n\n <!-- Number Input -->\n @if (QuestionType === 'number') {\n <mj-numeric-input\n [ngModel]=\"$any(Value)\"\n [ngModelOptions]=\"{standalone: true}\"\n [Min]=\"Min || 0\"\n [Max]=\"Max || 999999999\"\n [Step]=\"Step || 1\"\n [Disabled]=\"Disabled\"\n (ngModelChange)=\"OnValueChange($any($event))\"\n class=\"question-input\"\n ></mj-numeric-input>\n }\n\n <!-- Currency Input -->\n @if (QuestionType === 'currency') {\n <mj-numeric-input\n [ngModel]=\"$any(Value)\"\n [ngModelOptions]=\"{standalone: true}\"\n [Min]=\"Min || 0\"\n [Max]=\"Max || 999999999\"\n [Step]=\"Step || 0.01\"\n [Decimals]=\"2\"\n [Disabled]=\"Disabled\"\n (ngModelChange)=\"OnValueChange($any($event))\"\n class=\"question-input\"\n ></mj-numeric-input>\n }\n\n <!-- Date Picker -->\n @if (QuestionType === 'date') {\n <mj-datepicker\n [ngModel]=\"$any(Value)\"\n [ngModelOptions]=\"{standalone: true}\"\n [Disabled]=\"Disabled\"\n (ValueChange)=\"OnValueChange($any($event))\"\n class=\"question-input\"\n ></mj-datepicker>\n }\n\n <!-- DateTime Picker -->\n @if (QuestionType === 'datetime') {\n <input\n type=\"datetime-local\"\n class=\"mj-input question-input\"\n [value]=\"$any(Value)\"\n [disabled]=\"Disabled\"\n (change)=\"OnValueChange($any($event.target).value)\"\n />\n }\n\n <!-- Button Group -->\n @if (QuestionType === 'buttongroup') {\n <div class=\"mj-buttongroup question-buttongroup\">\n @for (option of Options; track TrackByValue($index, option)) {\n <button\n mjButton\n [toggleable]=\"true\"\n [selected]=\"Value === option.value\"\n [disabled]=\"Disabled\"\n (click)=\"OnValueChange(option.value)\"\n class=\"buttongroup-option\"\n >\n @if (option.icon) {\n <i class=\"fa {{ option.icon }}\" aria-hidden=\"true\"></i>\n }\n {{ option.label }}\n </button>\n }\n </div>\n }\n\n <!-- Radio Buttons -->\n @if (QuestionType === 'radio') {\n <div class=\"question-radio-group\">\n @for (option of Options; track TrackByValue($index, option)) {\n <div class=\"radio-option\">\n <input\n type=\"radio\"\n class=\"mj-radio\"\n [name]=\"Question.id\"\n [value]=\"option.value\"\n [checked]=\"Value === option.value\"\n [disabled]=\"Disabled\"\n (change)=\"OnValueChange(option.value)\"\n [id]=\"Question.id + '_' + option.value\"\n />\n <label [for]=\"Question.id + '_' + option.value\" class=\"radio-label\">\n @if (option.icon) {\n <i class=\"fa {{ option.icon }}\" aria-hidden=\"true\"></i>\n }\n {{ option.label }}\n </label>\n </div>\n }\n </div>\n }\n\n <!-- Dropdown -->\n @if (QuestionType === 'dropdown') {\n <select\n [formControl]=\"Control\"\n [disabled]=\"Disabled\"\n class=\"question-dropdown\"\n >\n <option value=\"\">Select an option...</option>\n @for (option of Options; track TrackByValue($index, option)) {\n <option [value]=\"option.value\">\n {{ option.label }}\n </option>\n }\n </select>\n }\n\n <!-- Slider -->\n @if (QuestionType === 'slider') {\n <div class=\"question-slider-container\">\n <input\n type=\"range\"\n class=\"mj-slider question-slider\"\n [min]=\"GetSliderConfig().min\"\n [max]=\"GetSliderConfig().max\"\n [step]=\"GetSliderConfig().step || 1\"\n [value]=\"$any(Value) || GetSliderConfig().min\"\n [disabled]=\"Disabled\"\n (input)=\"OnValueChange(+$any($event.target).value)\"\n />\n <div class=\"slider-value\">\n {{ Value || GetSliderConfig().min }}{{ GetSliderConfig().suffix || '' }}\n </div>\n </div>\n }\n\n <!-- Date Range -->\n @if (QuestionType === 'daterange') {\n <div class=\"question-daterange\">\n <mj-datepicker\n [ngModel]=\"$any(Value)?.start\"\n [ngModelOptions]=\"{standalone: true}\"\n [Disabled]=\"Disabled\"\n (ValueChange)=\"OnDateRangeStartChange($any($event))\"\n Placeholder=\"Start Date\"\n class=\"daterange-start\"\n ></mj-datepicker>\n <span class=\"daterange-separator\">to</span>\n <mj-datepicker\n [ngModel]=\"$any(Value)?.end\"\n [ngModelOptions]=\"{standalone: true}\"\n [Disabled]=\"Disabled\"\n (ValueChange)=\"OnDateRangeEndChange($any($event))\"\n Placeholder=\"End Date\"\n class=\"daterange-end\"\n ></mj-datepicker>\n </div>\n }\n\n <!-- Time Picker -->\n @if (QuestionType === 'time') {\n <input\n type=\"time\"\n class=\"mj-input question-input\"\n [value]=\"$any(Value)\"\n [disabled]=\"Disabled\"\n (change)=\"OnValueChange($any($event.target).value)\"\n />\n }\n\n <!-- Checkboxes -->\n @if (QuestionType === 'checkbox') {\n <div class=\"question-checkbox-group\">\n @for (option of Options; track TrackByValue($index, option)) {\n <div class=\"checkbox-option\">\n <input\n type=\"checkbox\"\n class=\"mj-checkbox\"\n [checked]=\"IsChecked(option)\"\n [disabled]=\"Disabled\"\n (change)=\"ToggleCheckbox(option)\"\n [id]=\"Question.id + '_' + option.value\"\n />\n <label [for]=\"Question.id + '_' + option.value\" class=\"checkbox-label\">\n @if (option.icon) {\n <i class=\"fa {{ option.icon }}\" aria-hidden=\"true\"></i>\n }\n {{ option.label }}\n </label>\n </div>\n }\n </div>\n }\n </div><!-- end input-container -->\n\n <!-- Validation Error -->\n @if (Control.invalid && Control.touched) {\n <div class=\"validation-error\">\n @if (Control.errors?.['required']) {\n <span>This field is required</span>\n }\n @if (Control.errors?.['email']) {\n <span>Please enter a valid email address</span>\n }\n @if (Control.errors?.['min']) {\n <span>Value must be at least {{ Min }}</span>\n }\n @if (Control.errors?.['max']) {\n <span>Value must be at most {{ Max }}</span>\n }\n </div>\n }\n</div>\n", styles: [".form-question {\n margin-bottom: 16px;\n}\n\n.question-label {\n display: block;\n font-weight: 500;\n margin-bottom: 6px;\n color: var(--mj-text-primary);\n font-size: 14px;\n}\n\n.required-indicator {\n color: var(--mj-status-error);\n margin-left: 2px;\n}\n\n.help-text {\n font-size: 12px;\n color: var(--mj-text-secondary);\n margin-bottom: 8px;\n font-style: italic;\n}\n\n/* Default widths - controlled by input-container width classes */\n.question-input,\n.question-textarea,\n.question-dropdown {\n width: 100%;\n max-width: 100%;\n}\n\n.question-dropdown {\n padding: 8px 12px;\n border: 1px solid var(--mj-border-default);\n border-radius: 4px;\n font-size: 14px;\n font-family: inherit;\n background-color: var(--mj-bg-surface);\n background-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23666' d='M6 9L1 4h10z'/%3E%3C/svg%3E\");\n background-repeat: no-repeat;\n background-position: right 8px center;\n background-size: 12px;\n padding-right: 32px;\n cursor: pointer;\n transition: border-color 0.2s ease, box-shadow 0.2s ease;\n appearance: none;\n -webkit-appearance: none;\n -moz-appearance: none;\n color: var(--mj-text-primary);\n}\n\n.question-dropdown:hover {\n border-color: var(--mj-border-strong);\n}\n\n.question-dropdown:focus {\n outline: none;\n border-color: var(--mj-brand-primary);\n box-shadow: 0 0 0 3px color-mix(in srgb, var(--mj-brand-primary) 10%, transparent);\n}\n\n.question-dropdown:disabled {\n background-color: var(--mj-bg-surface-sunken);\n cursor: not-allowed;\n opacity: 0.6;\n}\n\n.question-textarea {\n resize: vertical;\n min-height: 100px;\n}\n\n.question-buttongroup {\n display: flex;\n flex-wrap: nowrap;\n gap: 8px;\n overflow-x: auto;\n overflow-y: hidden;\n -webkit-overflow-scrolling: touch;\n scrollbar-width: thin;\n scrollbar-color: color-mix(in srgb, var(--mj-brand-primary) 30%, transparent) transparent;\n}\n\n.question-buttongroup::-webkit-scrollbar {\n height: 6px;\n}\n\n.question-buttongroup::-webkit-scrollbar-track {\n background: transparent;\n}\n\n.question-buttongroup::-webkit-scrollbar-thumb {\n background: color-mix(in srgb, var(--mj-brand-primary) 30%, transparent);\n border-radius: 3px;\n}\n\n.question-buttongroup::-webkit-scrollbar-thumb:hover {\n background: color-mix(in srgb, var(--mj-brand-primary) 50%, transparent);\n}\n\n.buttongroup-option {\n flex: 0 0 auto;\n white-space: nowrap;\n}\n\n.buttongroup-option i {\n margin-right: 6px;\n}\n\n.question-radio-group,\n.question-checkbox-group {\n display: flex;\n flex-wrap: wrap;\n gap: 12px 16px;\n}\n\n.radio-option,\n.checkbox-option {\n display: flex;\n align-items: center;\n gap: 8px;\n min-width: 120px;\n flex: 0 0 auto;\n}\n\n.radio-label,\n.checkbox-label {\n cursor: pointer;\n user-select: none;\n margin: 0;\n display: flex;\n align-items: center;\n gap: 6px;\n}\n\n.radio-label i,\n.checkbox-label i {\n color: var(--mj-text-secondary);\n}\n\n.validation-error {\n color: var(--mj-status-error);\n font-size: 12px;\n margin-top: 4px;\n}\n\n/* Required field styling */\n.form-question.required .question-label {\n font-weight: 600;\n}\n\n/* Slider styling */\n.question-slider-container {\n width: 100%;\n}\n\n.question-slider {\n width: 100%;\n margin-bottom: 8px;\n}\n\n.slider-value {\n text-align: center;\n font-size: 16px;\n font-weight: 600;\n color: var(--mj-brand-primary);\n padding: 4px 0;\n}\n\n/* Date range styling */\n.question-daterange {\n display: flex;\n align-items: center;\n gap: 12px;\n flex-wrap: wrap;\n}\n\n.daterange-start,\n.daterange-end {\n flex: 1;\n min-width: 150px;\n}\n\n.daterange-separator {\n font-weight: 500;\n color: var(--mj-text-secondary);\n white-space: nowrap;\n}\n\n/* Mobile adjustments for date range */\n@media (max-width: 480px) {\n .question-daterange {\n flex-direction: column;\n align-items: stretch;\n }\n\n .daterange-separator {\n text-align: center;\n padding: 4px 0;\n }\n}\n\n/* ===== Semantic Width Classes ===== */\n/* Controls field width based on expected content length */\n\n.input-container {\n display: block;\n}\n\n/* Narrow fields - for numbers, dates, times, short codes */\n.input-container.width-narrow {\n max-width: 200px;\n}\n\n.input-container.width-narrow .question-input,\n.input-container.width-narrow .question-dropdown {\n width: 100%;\n}\n\n/* Medium fields - for names, cities, phone numbers (DEFAULT) */\n.input-container.width-medium {\n max-width: 450px;\n}\n\n.input-container.width-medium .question-input,\n.input-container.width-medium .question-dropdown {\n width: 100%;\n}\n\n/* Wide fields - for emails, URLs, long text */\n.input-container.width-wide {\n max-width: 100%;\n}\n\n.input-container.width-wide .question-input,\n.input-container.width-wide .question-dropdown {\n width: 100%;\n}\n\n/* Full width - for textarea, search fields */\n.input-container.width-full {\n max-width: 100%;\n}\n\n.input-container.width-full .question-input,\n.input-container.width-full .question-textarea,\n.input-container.width-full .question-dropdown {\n width: 100%;\n}\n\n/* Auto-width - for dropdowns, radio, checkbox (sizes to content) */\n.input-container.width-auto {\n max-width: 350px;\n min-width: 150px;\n width: auto;\n}\n\n.input-container.width-auto .question-dropdown {\n width: 100%;\n}\n\n/* Responsive adjustments for smaller screens */\n@media (max-width: 600px) {\n .input-container.width-narrow,\n .input-container.width-medium,\n .input-container.width-wide,\n .input-container.width-auto {\n max-width: 100%;\n }\n}\n"], dependencies: [{ kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i2.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i2.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i2.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: i2.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "directive", type: i3.MJButtonDirective, selector: "button[mjButton], a[mjButton]", inputs: ["variant", "size", "toggleable", "selected", "ariaLabel"], outputs: ["selectedChange"] }, { kind: "component", type: i3.MJNumericInputComponent, selector: "mj-numeric-input", inputs: ["Min", "Max", "Step", "Format", "Decimals", "Disabled", "Placeholder"] }, { kind: "component", type: i3.MJDatepickerComponent, selector: "mj-datepicker", inputs: ["Min", "Max", "Format", "Placeholder", "Disabled"], outputs: ["ValueChange"] }] });
|
|
244
|
+
], ngImport: i0, template: "<div class=\"form-question\" [class.required]=\"Question.required\">\n <!-- Label -->\n @if (Question.label) {\n <label class=\"question-label\">\n {{ Question.label }}\n @if (Question.required) {\n <span class=\"required-indicator\">*</span>\n }\n </label>\n }\n\n <!-- Help Text -->\n @if (Question.helpText) {\n <div class=\"help-text\">\n {{ Question.helpText }}\n </div>\n }\n\n <!-- Input Container with width class -->\n <div class=\"input-container\" [ngClass]=\"WidthClass\">\n\n <!-- Text Input -->\n @if (QuestionType === 'text') {\n <input\n type=\"text\"\n class=\"mj-input question-input\"\n [value]=\"$any(Value) || ''\"\n [placeholder]=\"Placeholder\"\n [disabled]=\"Disabled\"\n (input)=\"OnValueChange($any($event.target).value)\"\n />\n }\n\n <!-- Email Input -->\n @if (QuestionType === 'email') {\n <input\n type=\"email\"\n class=\"mj-input question-input\"\n [value]=\"$any(Value) || ''\"\n [placeholder]=\"Placeholder\"\n [disabled]=\"Disabled\"\n (input)=\"OnValueChange($any($event.target).value)\"\n />\n }\n\n <!-- Textarea (markdown mode: rendered preview by default, Edit toggles the raw editor) -->\n @if (QuestionType === 'textarea') {\n @if (IsMarkdown) {\n <div class=\"markdown-field\" [class.editing]=\"MarkdownEditing\">\n <div class=\"markdown-field-toolbar\">\n <span class=\"markdown-field-hint\">\n <i class=\"fa-brands fa-markdown\" aria-hidden=\"true\"></i>\n {{ MarkdownEditing ? 'Editing Markdown' : 'Formatted preview' }}\n </span>\n <button\n type=\"button\"\n class=\"markdown-field-toggle\"\n [disabled]=\"Disabled\"\n (click)=\"ToggleMarkdownEditing()\"\n [title]=\"MarkdownEditing ? 'Show the formatted preview' : 'Edit the raw Markdown'\"\n >\n <i class=\"fa-solid\" [ngClass]=\"MarkdownEditing ? 'fa-eye' : 'fa-pen'\" aria-hidden=\"true\"></i>\n {{ MarkdownEditing ? 'Preview' : 'Edit' }}\n </button>\n </div>\n @if (!MarkdownEditing) {\n <div class=\"markdown-field-preview\">\n <mj-markdown [data]=\"MarkdownValue\"></mj-markdown>\n </div>\n }\n @if (MarkdownEditing) {\n <textarea\n class=\"mj-textarea question-textarea\"\n [value]=\"$any(Value) || ''\"\n [placeholder]=\"Placeholder\"\n [disabled]=\"Disabled\"\n (input)=\"OnValueChange($any($event.target).value)\"\n rows=\"10\"\n ></textarea>\n }\n </div>\n } @else {\n <textarea\n class=\"mj-textarea question-textarea\"\n [value]=\"$any(Value) || ''\"\n [placeholder]=\"Placeholder\"\n [disabled]=\"Disabled\"\n (input)=\"OnValueChange($any($event.target).value)\"\n rows=\"5\"\n ></textarea>\n }\n }\n\n <!-- Number Input -->\n @if (QuestionType === 'number') {\n <mj-numeric-input\n [ngModel]=\"$any(Value)\"\n [ngModelOptions]=\"{standalone: true}\"\n [Min]=\"Min || 0\"\n [Max]=\"Max || 999999999\"\n [Step]=\"Step || 1\"\n [Disabled]=\"Disabled\"\n (ngModelChange)=\"OnValueChange($any($event))\"\n class=\"question-input\"\n ></mj-numeric-input>\n }\n\n <!-- Currency Input -->\n @if (QuestionType === 'currency') {\n <mj-numeric-input\n [ngModel]=\"$any(Value)\"\n [ngModelOptions]=\"{standalone: true}\"\n [Min]=\"Min || 0\"\n [Max]=\"Max || 999999999\"\n [Step]=\"Step || 0.01\"\n [Decimals]=\"2\"\n [Disabled]=\"Disabled\"\n (ngModelChange)=\"OnValueChange($any($event))\"\n class=\"question-input\"\n ></mj-numeric-input>\n }\n\n <!-- Date Picker -->\n @if (QuestionType === 'date') {\n <mj-datepicker\n [ngModel]=\"$any(Value)\"\n [ngModelOptions]=\"{standalone: true}\"\n [Disabled]=\"Disabled\"\n (ValueChange)=\"OnValueChange($any($event))\"\n class=\"question-input\"\n ></mj-datepicker>\n }\n\n <!-- DateTime Picker -->\n @if (QuestionType === 'datetime') {\n <input\n type=\"datetime-local\"\n class=\"mj-input question-input\"\n [value]=\"$any(Value)\"\n [disabled]=\"Disabled\"\n (change)=\"OnValueChange($any($event.target).value)\"\n />\n }\n\n <!-- Button Group -->\n @if (QuestionType === 'buttongroup') {\n <div class=\"mj-buttongroup question-buttongroup\">\n @for (option of Options; track TrackByValue($index, option)) {\n <button\n mjButton\n [toggleable]=\"true\"\n [selected]=\"Value === option.value\"\n [disabled]=\"Disabled\"\n (click)=\"OnValueChange(option.value)\"\n class=\"buttongroup-option\"\n >\n @if (option.icon) {\n <i class=\"fa {{ option.icon }}\" aria-hidden=\"true\"></i>\n }\n {{ option.label }}\n </button>\n }\n </div>\n }\n\n <!-- Radio Buttons -->\n @if (QuestionType === 'radio') {\n <div class=\"question-radio-group\">\n @for (option of Options; track TrackByValue($index, option)) {\n <div class=\"radio-option\">\n <input\n type=\"radio\"\n class=\"mj-radio\"\n [name]=\"Question.id\"\n [value]=\"option.value\"\n [checked]=\"Value === option.value\"\n [disabled]=\"Disabled\"\n (change)=\"OnValueChange(option.value)\"\n [id]=\"Question.id + '_' + option.value\"\n />\n <label [for]=\"Question.id + '_' + option.value\" class=\"radio-label\">\n @if (option.icon) {\n <i class=\"fa {{ option.icon }}\" aria-hidden=\"true\"></i>\n }\n {{ option.label }}\n </label>\n </div>\n }\n </div>\n }\n\n <!-- Dropdown -->\n @if (QuestionType === 'dropdown') {\n <select\n [formControl]=\"Control\"\n [disabled]=\"Disabled\"\n class=\"question-dropdown\"\n >\n <option value=\"\">Select an option...</option>\n @for (option of Options; track TrackByValue($index, option)) {\n <option [value]=\"option.value\">\n {{ option.label }}\n </option>\n }\n </select>\n }\n\n <!-- Slider -->\n @if (QuestionType === 'slider') {\n <div class=\"question-slider-container\">\n <input\n type=\"range\"\n class=\"mj-slider question-slider\"\n [min]=\"GetSliderConfig().min\"\n [max]=\"GetSliderConfig().max\"\n [step]=\"GetSliderConfig().step || 1\"\n [value]=\"$any(Value) || GetSliderConfig().min\"\n [disabled]=\"Disabled\"\n (input)=\"OnValueChange(+$any($event.target).value)\"\n />\n <div class=\"slider-value\">\n {{ Value || GetSliderConfig().min }}{{ GetSliderConfig().suffix || '' }}\n </div>\n </div>\n }\n\n <!-- Date Range -->\n @if (QuestionType === 'daterange') {\n <div class=\"question-daterange\">\n <mj-datepicker\n [ngModel]=\"$any(Value)?.start\"\n [ngModelOptions]=\"{standalone: true}\"\n [Disabled]=\"Disabled\"\n (ValueChange)=\"OnDateRangeStartChange($any($event))\"\n Placeholder=\"Start Date\"\n class=\"daterange-start\"\n ></mj-datepicker>\n <span class=\"daterange-separator\">to</span>\n <mj-datepicker\n [ngModel]=\"$any(Value)?.end\"\n [ngModelOptions]=\"{standalone: true}\"\n [Disabled]=\"Disabled\"\n (ValueChange)=\"OnDateRangeEndChange($any($event))\"\n Placeholder=\"End Date\"\n class=\"daterange-end\"\n ></mj-datepicker>\n </div>\n }\n\n <!-- Time Picker -->\n @if (QuestionType === 'time') {\n <input\n type=\"time\"\n class=\"mj-input question-input\"\n [value]=\"$any(Value)\"\n [disabled]=\"Disabled\"\n (change)=\"OnValueChange($any($event.target).value)\"\n />\n }\n\n <!-- Checkboxes -->\n @if (QuestionType === 'checkbox') {\n <div class=\"question-checkbox-group\">\n @for (option of Options; track TrackByValue($index, option)) {\n <div class=\"checkbox-option\">\n <input\n type=\"checkbox\"\n class=\"mj-checkbox\"\n [checked]=\"IsChecked(option)\"\n [disabled]=\"Disabled\"\n (change)=\"ToggleCheckbox(option)\"\n [id]=\"Question.id + '_' + option.value\"\n />\n <label [for]=\"Question.id + '_' + option.value\" class=\"checkbox-label\">\n @if (option.icon) {\n <i class=\"fa {{ option.icon }}\" aria-hidden=\"true\"></i>\n }\n {{ option.label }}\n </label>\n </div>\n }\n </div>\n }\n </div><!-- end input-container -->\n\n <!-- Validation Error -->\n @if (Control.invalid && Control.touched) {\n <div class=\"validation-error\">\n @if (Control.errors?.['required']) {\n <span>This field is required</span>\n }\n @if (Control.errors?.['email']) {\n <span>Please enter a valid email address</span>\n }\n @if (Control.errors?.['min']) {\n <span>Value must be at least {{ Min }}</span>\n }\n @if (Control.errors?.['max']) {\n <span>Value must be at most {{ Max }}</span>\n }\n </div>\n }\n</div>\n", styles: [".form-question {\n margin-bottom: 16px;\n}\n\n.question-label {\n display: block;\n font-weight: 500;\n margin-bottom: 6px;\n color: var(--mj-text-primary);\n font-size: 14px;\n}\n\n.required-indicator {\n color: var(--mj-status-error);\n margin-left: 2px;\n}\n\n.help-text {\n font-size: 12px;\n color: var(--mj-text-secondary);\n margin-bottom: 8px;\n font-style: italic;\n}\n\n/* Default widths - controlled by input-container width classes */\n.question-input,\n.question-textarea,\n.question-dropdown {\n width: 100%;\n max-width: 100%;\n}\n\n.question-dropdown {\n padding: 8px 12px;\n border: 1px solid var(--mj-border-default);\n border-radius: 4px;\n font-size: 14px;\n font-family: inherit;\n background-color: var(--mj-bg-surface);\n background-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23666' d='M6 9L1 4h10z'/%3E%3C/svg%3E\");\n background-repeat: no-repeat;\n background-position: right 8px center;\n background-size: 12px;\n padding-right: 32px;\n cursor: pointer;\n transition: border-color 0.2s ease, box-shadow 0.2s ease;\n appearance: none;\n -webkit-appearance: none;\n -moz-appearance: none;\n color: var(--mj-text-primary);\n}\n\n.question-dropdown:hover {\n border-color: var(--mj-border-strong);\n}\n\n.question-dropdown:focus {\n outline: none;\n border-color: var(--mj-brand-primary);\n box-shadow: 0 0 0 3px color-mix(in srgb, var(--mj-brand-primary) 10%, transparent);\n}\n\n.question-dropdown:disabled {\n background-color: var(--mj-bg-surface-sunken);\n cursor: not-allowed;\n opacity: 0.6;\n}\n\n.question-textarea {\n resize: vertical;\n min-height: 100px;\n}\n\n.question-buttongroup {\n display: flex;\n flex-wrap: nowrap;\n gap: 8px;\n overflow-x: auto;\n overflow-y: hidden;\n -webkit-overflow-scrolling: touch;\n scrollbar-width: thin;\n scrollbar-color: color-mix(in srgb, var(--mj-brand-primary) 30%, transparent) transparent;\n}\n\n.question-buttongroup::-webkit-scrollbar {\n height: 6px;\n}\n\n.question-buttongroup::-webkit-scrollbar-track {\n background: transparent;\n}\n\n.question-buttongroup::-webkit-scrollbar-thumb {\n background: color-mix(in srgb, var(--mj-brand-primary) 30%, transparent);\n border-radius: 3px;\n}\n\n.question-buttongroup::-webkit-scrollbar-thumb:hover {\n background: color-mix(in srgb, var(--mj-brand-primary) 50%, transparent);\n}\n\n.buttongroup-option {\n flex: 0 0 auto;\n white-space: nowrap;\n}\n\n.buttongroup-option i {\n margin-right: 6px;\n}\n\n.question-radio-group,\n.question-checkbox-group {\n display: flex;\n flex-wrap: wrap;\n gap: 12px 16px;\n}\n\n.radio-option,\n.checkbox-option {\n display: flex;\n align-items: center;\n gap: 8px;\n min-width: 120px;\n flex: 0 0 auto;\n}\n\n.radio-label,\n.checkbox-label {\n cursor: pointer;\n user-select: none;\n margin: 0;\n display: flex;\n align-items: center;\n gap: 6px;\n}\n\n.radio-label i,\n.checkbox-label i {\n color: var(--mj-text-secondary);\n}\n\n.validation-error {\n color: var(--mj-status-error);\n font-size: 12px;\n margin-top: 4px;\n}\n\n/* Required field styling */\n.form-question.required .question-label {\n font-weight: 600;\n}\n\n/* Slider styling */\n.question-slider-container {\n width: 100%;\n}\n\n.question-slider {\n width: 100%;\n margin-bottom: 8px;\n}\n\n.slider-value {\n text-align: center;\n font-size: 16px;\n font-weight: 600;\n color: var(--mj-brand-primary);\n padding: 4px 0;\n}\n\n/* Date range styling */\n.question-daterange {\n display: flex;\n align-items: center;\n gap: 12px;\n flex-wrap: wrap;\n}\n\n.daterange-start,\n.daterange-end {\n flex: 1;\n min-width: 150px;\n}\n\n.daterange-separator {\n font-weight: 500;\n color: var(--mj-text-secondary);\n white-space: nowrap;\n}\n\n/* Mobile adjustments for date range */\n@media (max-width: 480px) {\n .question-daterange {\n flex-direction: column;\n align-items: stretch;\n }\n\n .daterange-separator {\n text-align: center;\n padding: 4px 0;\n }\n}\n\n/* ===== Semantic Width Classes ===== */\n/* Controls field width based on expected content length */\n\n.input-container {\n display: block;\n}\n\n/* Narrow fields - for numbers, dates, times, short codes */\n.input-container.width-narrow {\n max-width: 200px;\n}\n\n.input-container.width-narrow .question-input,\n.input-container.width-narrow .question-dropdown {\n width: 100%;\n}\n\n/* Medium fields - for names, cities, phone numbers (DEFAULT) */\n.input-container.width-medium {\n max-width: 450px;\n}\n\n.input-container.width-medium .question-input,\n.input-container.width-medium .question-dropdown {\n width: 100%;\n}\n\n/* Wide fields - for emails, URLs, long text */\n.input-container.width-wide {\n max-width: 100%;\n}\n\n.input-container.width-wide .question-input,\n.input-container.width-wide .question-dropdown {\n width: 100%;\n}\n\n/* Full width - for textarea, search fields */\n.input-container.width-full {\n max-width: 100%;\n}\n\n.input-container.width-full .question-input,\n.input-container.width-full .question-textarea,\n.input-container.width-full .question-dropdown {\n width: 100%;\n}\n\n/* Auto-width - for dropdowns, radio, checkbox (sizes to content) */\n.input-container.width-auto {\n max-width: 350px;\n min-width: 150px;\n width: auto;\n}\n\n.input-container.width-auto .question-dropdown {\n width: 100%;\n}\n\n/* Responsive adjustments for smaller screens */\n@media (max-width: 600px) {\n .input-container.width-narrow,\n .input-container.width-medium,\n .input-container.width-wide,\n .input-container.width-auto {\n max-width: 100%;\n }\n}\n\n/* \u2500\u2500 Markdown textarea (question.type.markdown) \u2014 rendered preview + Edit toggle \u2500\u2500 */\n.markdown-field {\n border: 1px solid var(--mj-border-default);\n border-radius: 10px;\n background: var(--mj-bg-surface);\n overflow: hidden;\n}\n.markdown-field-toolbar {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 8px;\n padding: 6px 10px;\n background: var(--mj-bg-surface-sunken);\n border-bottom: 1px solid var(--mj-border-subtle);\n}\n.markdown-field-hint {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n font-size: 11px;\n font-weight: 500;\n color: var(--mj-text-muted);\n}\n.markdown-field-toggle {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n font: inherit;\n font-size: 11.5px;\n font-weight: 600;\n padding: 4px 10px;\n border-radius: 7px;\n border: 1px solid var(--mj-border-default);\n background: var(--mj-bg-surface);\n color: var(--mj-text-secondary);\n cursor: pointer;\n transition: color 0.15s ease, border-color 0.15s ease;\n}\n.markdown-field-toggle:hover:not(:disabled) {\n color: var(--mj-brand-primary);\n border-color: color-mix(in srgb, var(--mj-brand-primary) 45%, var(--mj-border-default));\n}\n.markdown-field-toggle:disabled {\n opacity: 0.5;\n cursor: not-allowed;\n}\n.markdown-field-preview {\n padding: 12px 14px;\n max-height: 420px;\n overflow-y: auto;\n font-size: 13.5px;\n line-height: 1.6;\n color: var(--mj-text-primary);\n}\n/* the raw editor inside the panel loses its own border (the panel provides it) */\n.markdown-field .question-textarea {\n border: none;\n border-radius: 0;\n width: 100%;\n}\n"], dependencies: [{ kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i2.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i2.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i2.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: i2.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "directive", type: i3.MJButtonDirective, selector: "button[mjButton], a[mjButton]", inputs: ["variant", "size", "toggleable", "selected", "ariaLabel"], outputs: ["selectedChange"] }, { kind: "component", type: i3.MJNumericInputComponent, selector: "mj-numeric-input", inputs: ["Min", "Max", "Step", "Format", "Decimals", "Disabled", "Placeholder"] }, { kind: "component", type: i3.MJDatepickerComponent, selector: "mj-datepicker", inputs: ["Min", "Max", "Format", "Placeholder", "Disabled"], outputs: ["ValueChange"] }, { kind: "component", type: i4.MarkdownComponent, selector: "mj-markdown", inputs: ["data", "enableHighlight", "enableMermaid", "enableCodeCopy", "enableCollapsibleHeadings", "collapsibleHeadingLevel", "collapsibleDefaultExpanded", "autoExpandLevels", "enableAlerts", "enableSmartypants", "enableSvgRenderer", "enableHtml", "enableJavaScript", "enableHeadingIds", "headingIdPrefix", "enableLineNumbers", "containerClass", "mermaidTheme", "sanitize"], outputs: ["rendered", "headingClick", "codeCopied"] }] });
|
|
223
245
|
}
|
|
224
246
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: DynamicFormFieldComponent, decorators: [{
|
|
225
247
|
type: Component,
|
|
@@ -229,7 +251,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
|
|
|
229
251
|
useExisting: forwardRef(() => DynamicFormFieldComponent),
|
|
230
252
|
multi: true
|
|
231
253
|
}
|
|
232
|
-
], template: "<div class=\"form-question\" [class.required]=\"Question.required\">\n <!-- Label -->\n @if (Question.label) {\n <label class=\"question-label\">\n {{ Question.label }}\n @if (Question.required) {\n <span class=\"required-indicator\">*</span>\n }\n </label>\n }\n\n <!-- Help Text -->\n @if (Question.helpText) {\n <div class=\"help-text\">\n {{ Question.helpText }}\n </div>\n }\n\n <!-- Input Container with width class -->\n <div class=\"input-container\" [ngClass]=\"WidthClass\">\n\n <!-- Text Input -->\n @if (QuestionType === 'text') {\n <input\n type=\"text\"\n class=\"mj-input question-input\"\n [value]=\"$any(Value) || ''\"\n [placeholder]=\"Placeholder\"\n [disabled]=\"Disabled\"\n (input)=\"OnValueChange($any($event.target).value)\"\n />\n }\n\n <!-- Email Input -->\n @if (QuestionType === 'email') {\n <input\n type=\"email\"\n class=\"mj-input question-input\"\n [value]=\"$any(Value) || ''\"\n [placeholder]=\"Placeholder\"\n [disabled]=\"Disabled\"\n (input)=\"OnValueChange($any($event.target).value)\"\n />\n }\n\n <!-- Textarea -->\n @if (QuestionType === 'textarea') {\n <textarea\n class=\"mj-textarea question-textarea\"\n [value]=\"$any(Value) || ''\"\n [placeholder]=\"Placeholder\"\n [disabled]=\"Disabled\"\n (input)=\"OnValueChange($any($event.target).value)\"\n rows=\"5\"\n ></textarea>\n }\n\n <!-- Number Input -->\n @if (QuestionType === 'number') {\n <mj-numeric-input\n [ngModel]=\"$any(Value)\"\n [ngModelOptions]=\"{standalone: true}\"\n [Min]=\"Min || 0\"\n [Max]=\"Max || 999999999\"\n [Step]=\"Step || 1\"\n [Disabled]=\"Disabled\"\n (ngModelChange)=\"OnValueChange($any($event))\"\n class=\"question-input\"\n ></mj-numeric-input>\n }\n\n <!-- Currency Input -->\n @if (QuestionType === 'currency') {\n <mj-numeric-input\n [ngModel]=\"$any(Value)\"\n [ngModelOptions]=\"{standalone: true}\"\n [Min]=\"Min || 0\"\n [Max]=\"Max || 999999999\"\n [Step]=\"Step || 0.01\"\n [Decimals]=\"2\"\n [Disabled]=\"Disabled\"\n (ngModelChange)=\"OnValueChange($any($event))\"\n class=\"question-input\"\n ></mj-numeric-input>\n }\n\n <!-- Date Picker -->\n @if (QuestionType === 'date') {\n <mj-datepicker\n [ngModel]=\"$any(Value)\"\n [ngModelOptions]=\"{standalone: true}\"\n [Disabled]=\"Disabled\"\n (ValueChange)=\"OnValueChange($any($event))\"\n class=\"question-input\"\n ></mj-datepicker>\n }\n\n <!-- DateTime Picker -->\n @if (QuestionType === 'datetime') {\n <input\n type=\"datetime-local\"\n class=\"mj-input question-input\"\n [value]=\"$any(Value)\"\n [disabled]=\"Disabled\"\n (change)=\"OnValueChange($any($event.target).value)\"\n />\n }\n\n <!-- Button Group -->\n @if (QuestionType === 'buttongroup') {\n <div class=\"mj-buttongroup question-buttongroup\">\n @for (option of Options; track TrackByValue($index, option)) {\n <button\n mjButton\n [toggleable]=\"true\"\n [selected]=\"Value === option.value\"\n [disabled]=\"Disabled\"\n (click)=\"OnValueChange(option.value)\"\n class=\"buttongroup-option\"\n >\n @if (option.icon) {\n <i class=\"fa {{ option.icon }}\" aria-hidden=\"true\"></i>\n }\n {{ option.label }}\n </button>\n }\n </div>\n }\n\n <!-- Radio Buttons -->\n @if (QuestionType === 'radio') {\n <div class=\"question-radio-group\">\n @for (option of Options; track TrackByValue($index, option)) {\n <div class=\"radio-option\">\n <input\n type=\"radio\"\n class=\"mj-radio\"\n [name]=\"Question.id\"\n [value]=\"option.value\"\n [checked]=\"Value === option.value\"\n [disabled]=\"Disabled\"\n (change)=\"OnValueChange(option.value)\"\n [id]=\"Question.id + '_' + option.value\"\n />\n <label [for]=\"Question.id + '_' + option.value\" class=\"radio-label\">\n @if (option.icon) {\n <i class=\"fa {{ option.icon }}\" aria-hidden=\"true\"></i>\n }\n {{ option.label }}\n </label>\n </div>\n }\n </div>\n }\n\n <!-- Dropdown -->\n @if (QuestionType === 'dropdown') {\n <select\n [formControl]=\"Control\"\n [disabled]=\"Disabled\"\n class=\"question-dropdown\"\n >\n <option value=\"\">Select an option...</option>\n @for (option of Options; track TrackByValue($index, option)) {\n <option [value]=\"option.value\">\n {{ option.label }}\n </option>\n }\n </select>\n }\n\n <!-- Slider -->\n @if (QuestionType === 'slider') {\n <div class=\"question-slider-container\">\n <input\n type=\"range\"\n class=\"mj-slider question-slider\"\n [min]=\"GetSliderConfig().min\"\n [max]=\"GetSliderConfig().max\"\n [step]=\"GetSliderConfig().step || 1\"\n [value]=\"$any(Value) || GetSliderConfig().min\"\n [disabled]=\"Disabled\"\n (input)=\"OnValueChange(+$any($event.target).value)\"\n />\n <div class=\"slider-value\">\n {{ Value || GetSliderConfig().min }}{{ GetSliderConfig().suffix || '' }}\n </div>\n </div>\n }\n\n <!-- Date Range -->\n @if (QuestionType === 'daterange') {\n <div class=\"question-daterange\">\n <mj-datepicker\n [ngModel]=\"$any(Value)?.start\"\n [ngModelOptions]=\"{standalone: true}\"\n [Disabled]=\"Disabled\"\n (ValueChange)=\"OnDateRangeStartChange($any($event))\"\n Placeholder=\"Start Date\"\n class=\"daterange-start\"\n ></mj-datepicker>\n <span class=\"daterange-separator\">to</span>\n <mj-datepicker\n [ngModel]=\"$any(Value)?.end\"\n [ngModelOptions]=\"{standalone: true}\"\n [Disabled]=\"Disabled\"\n (ValueChange)=\"OnDateRangeEndChange($any($event))\"\n Placeholder=\"End Date\"\n class=\"daterange-end\"\n ></mj-datepicker>\n </div>\n }\n\n <!-- Time Picker -->\n @if (QuestionType === 'time') {\n <input\n type=\"time\"\n class=\"mj-input question-input\"\n [value]=\"$any(Value)\"\n [disabled]=\"Disabled\"\n (change)=\"OnValueChange($any($event.target).value)\"\n />\n }\n\n <!-- Checkboxes -->\n @if (QuestionType === 'checkbox') {\n <div class=\"question-checkbox-group\">\n @for (option of Options; track TrackByValue($index, option)) {\n <div class=\"checkbox-option\">\n <input\n type=\"checkbox\"\n class=\"mj-checkbox\"\n [checked]=\"IsChecked(option)\"\n [disabled]=\"Disabled\"\n (change)=\"ToggleCheckbox(option)\"\n [id]=\"Question.id + '_' + option.value\"\n />\n <label [for]=\"Question.id + '_' + option.value\" class=\"checkbox-label\">\n @if (option.icon) {\n <i class=\"fa {{ option.icon }}\" aria-hidden=\"true\"></i>\n }\n {{ option.label }}\n </label>\n </div>\n }\n </div>\n }\n </div><!-- end input-container -->\n\n <!-- Validation Error -->\n @if (Control.invalid && Control.touched) {\n <div class=\"validation-error\">\n @if (Control.errors?.['required']) {\n <span>This field is required</span>\n }\n @if (Control.errors?.['email']) {\n <span>Please enter a valid email address</span>\n }\n @if (Control.errors?.['min']) {\n <span>Value must be at least {{ Min }}</span>\n }\n @if (Control.errors?.['max']) {\n <span>Value must be at most {{ Max }}</span>\n }\n </div>\n }\n</div>\n", styles: [".form-question {\n margin-bottom: 16px;\n}\n\n.question-label {\n display: block;\n font-weight: 500;\n margin-bottom: 6px;\n color: var(--mj-text-primary);\n font-size: 14px;\n}\n\n.required-indicator {\n color: var(--mj-status-error);\n margin-left: 2px;\n}\n\n.help-text {\n font-size: 12px;\n color: var(--mj-text-secondary);\n margin-bottom: 8px;\n font-style: italic;\n}\n\n/* Default widths - controlled by input-container width classes */\n.question-input,\n.question-textarea,\n.question-dropdown {\n width: 100%;\n max-width: 100%;\n}\n\n.question-dropdown {\n padding: 8px 12px;\n border: 1px solid var(--mj-border-default);\n border-radius: 4px;\n font-size: 14px;\n font-family: inherit;\n background-color: var(--mj-bg-surface);\n background-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23666' d='M6 9L1 4h10z'/%3E%3C/svg%3E\");\n background-repeat: no-repeat;\n background-position: right 8px center;\n background-size: 12px;\n padding-right: 32px;\n cursor: pointer;\n transition: border-color 0.2s ease, box-shadow 0.2s ease;\n appearance: none;\n -webkit-appearance: none;\n -moz-appearance: none;\n color: var(--mj-text-primary);\n}\n\n.question-dropdown:hover {\n border-color: var(--mj-border-strong);\n}\n\n.question-dropdown:focus {\n outline: none;\n border-color: var(--mj-brand-primary);\n box-shadow: 0 0 0 3px color-mix(in srgb, var(--mj-brand-primary) 10%, transparent);\n}\n\n.question-dropdown:disabled {\n background-color: var(--mj-bg-surface-sunken);\n cursor: not-allowed;\n opacity: 0.6;\n}\n\n.question-textarea {\n resize: vertical;\n min-height: 100px;\n}\n\n.question-buttongroup {\n display: flex;\n flex-wrap: nowrap;\n gap: 8px;\n overflow-x: auto;\n overflow-y: hidden;\n -webkit-overflow-scrolling: touch;\n scrollbar-width: thin;\n scrollbar-color: color-mix(in srgb, var(--mj-brand-primary) 30%, transparent) transparent;\n}\n\n.question-buttongroup::-webkit-scrollbar {\n height: 6px;\n}\n\n.question-buttongroup::-webkit-scrollbar-track {\n background: transparent;\n}\n\n.question-buttongroup::-webkit-scrollbar-thumb {\n background: color-mix(in srgb, var(--mj-brand-primary) 30%, transparent);\n border-radius: 3px;\n}\n\n.question-buttongroup::-webkit-scrollbar-thumb:hover {\n background: color-mix(in srgb, var(--mj-brand-primary) 50%, transparent);\n}\n\n.buttongroup-option {\n flex: 0 0 auto;\n white-space: nowrap;\n}\n\n.buttongroup-option i {\n margin-right: 6px;\n}\n\n.question-radio-group,\n.question-checkbox-group {\n display: flex;\n flex-wrap: wrap;\n gap: 12px 16px;\n}\n\n.radio-option,\n.checkbox-option {\n display: flex;\n align-items: center;\n gap: 8px;\n min-width: 120px;\n flex: 0 0 auto;\n}\n\n.radio-label,\n.checkbox-label {\n cursor: pointer;\n user-select: none;\n margin: 0;\n display: flex;\n align-items: center;\n gap: 6px;\n}\n\n.radio-label i,\n.checkbox-label i {\n color: var(--mj-text-secondary);\n}\n\n.validation-error {\n color: var(--mj-status-error);\n font-size: 12px;\n margin-top: 4px;\n}\n\n/* Required field styling */\n.form-question.required .question-label {\n font-weight: 600;\n}\n\n/* Slider styling */\n.question-slider-container {\n width: 100%;\n}\n\n.question-slider {\n width: 100%;\n margin-bottom: 8px;\n}\n\n.slider-value {\n text-align: center;\n font-size: 16px;\n font-weight: 600;\n color: var(--mj-brand-primary);\n padding: 4px 0;\n}\n\n/* Date range styling */\n.question-daterange {\n display: flex;\n align-items: center;\n gap: 12px;\n flex-wrap: wrap;\n}\n\n.daterange-start,\n.daterange-end {\n flex: 1;\n min-width: 150px;\n}\n\n.daterange-separator {\n font-weight: 500;\n color: var(--mj-text-secondary);\n white-space: nowrap;\n}\n\n/* Mobile adjustments for date range */\n@media (max-width: 480px) {\n .question-daterange {\n flex-direction: column;\n align-items: stretch;\n }\n\n .daterange-separator {\n text-align: center;\n padding: 4px 0;\n }\n}\n\n/* ===== Semantic Width Classes ===== */\n/* Controls field width based on expected content length */\n\n.input-container {\n display: block;\n}\n\n/* Narrow fields - for numbers, dates, times, short codes */\n.input-container.width-narrow {\n max-width: 200px;\n}\n\n.input-container.width-narrow .question-input,\n.input-container.width-narrow .question-dropdown {\n width: 100%;\n}\n\n/* Medium fields - for names, cities, phone numbers (DEFAULT) */\n.input-container.width-medium {\n max-width: 450px;\n}\n\n.input-container.width-medium .question-input,\n.input-container.width-medium .question-dropdown {\n width: 100%;\n}\n\n/* Wide fields - for emails, URLs, long text */\n.input-container.width-wide {\n max-width: 100%;\n}\n\n.input-container.width-wide .question-input,\n.input-container.width-wide .question-dropdown {\n width: 100%;\n}\n\n/* Full width - for textarea, search fields */\n.input-container.width-full {\n max-width: 100%;\n}\n\n.input-container.width-full .question-input,\n.input-container.width-full .question-textarea,\n.input-container.width-full .question-dropdown {\n width: 100%;\n}\n\n/* Auto-width - for dropdowns, radio, checkbox (sizes to content) */\n.input-container.width-auto {\n max-width: 350px;\n min-width: 150px;\n width: auto;\n}\n\n.input-container.width-auto .question-dropdown {\n width: 100%;\n}\n\n/* Responsive adjustments for smaller screens */\n@media (max-width: 600px) {\n .input-container.width-narrow,\n .input-container.width-medium,\n .input-container.width-wide,\n .input-container.width-auto {\n max-width: 100%;\n }\n}\n"] }]
|
|
254
|
+
], template: "<div class=\"form-question\" [class.required]=\"Question.required\">\n <!-- Label -->\n @if (Question.label) {\n <label class=\"question-label\">\n {{ Question.label }}\n @if (Question.required) {\n <span class=\"required-indicator\">*</span>\n }\n </label>\n }\n\n <!-- Help Text -->\n @if (Question.helpText) {\n <div class=\"help-text\">\n {{ Question.helpText }}\n </div>\n }\n\n <!-- Input Container with width class -->\n <div class=\"input-container\" [ngClass]=\"WidthClass\">\n\n <!-- Text Input -->\n @if (QuestionType === 'text') {\n <input\n type=\"text\"\n class=\"mj-input question-input\"\n [value]=\"$any(Value) || ''\"\n [placeholder]=\"Placeholder\"\n [disabled]=\"Disabled\"\n (input)=\"OnValueChange($any($event.target).value)\"\n />\n }\n\n <!-- Email Input -->\n @if (QuestionType === 'email') {\n <input\n type=\"email\"\n class=\"mj-input question-input\"\n [value]=\"$any(Value) || ''\"\n [placeholder]=\"Placeholder\"\n [disabled]=\"Disabled\"\n (input)=\"OnValueChange($any($event.target).value)\"\n />\n }\n\n <!-- Textarea (markdown mode: rendered preview by default, Edit toggles the raw editor) -->\n @if (QuestionType === 'textarea') {\n @if (IsMarkdown) {\n <div class=\"markdown-field\" [class.editing]=\"MarkdownEditing\">\n <div class=\"markdown-field-toolbar\">\n <span class=\"markdown-field-hint\">\n <i class=\"fa-brands fa-markdown\" aria-hidden=\"true\"></i>\n {{ MarkdownEditing ? 'Editing Markdown' : 'Formatted preview' }}\n </span>\n <button\n type=\"button\"\n class=\"markdown-field-toggle\"\n [disabled]=\"Disabled\"\n (click)=\"ToggleMarkdownEditing()\"\n [title]=\"MarkdownEditing ? 'Show the formatted preview' : 'Edit the raw Markdown'\"\n >\n <i class=\"fa-solid\" [ngClass]=\"MarkdownEditing ? 'fa-eye' : 'fa-pen'\" aria-hidden=\"true\"></i>\n {{ MarkdownEditing ? 'Preview' : 'Edit' }}\n </button>\n </div>\n @if (!MarkdownEditing) {\n <div class=\"markdown-field-preview\">\n <mj-markdown [data]=\"MarkdownValue\"></mj-markdown>\n </div>\n }\n @if (MarkdownEditing) {\n <textarea\n class=\"mj-textarea question-textarea\"\n [value]=\"$any(Value) || ''\"\n [placeholder]=\"Placeholder\"\n [disabled]=\"Disabled\"\n (input)=\"OnValueChange($any($event.target).value)\"\n rows=\"10\"\n ></textarea>\n }\n </div>\n } @else {\n <textarea\n class=\"mj-textarea question-textarea\"\n [value]=\"$any(Value) || ''\"\n [placeholder]=\"Placeholder\"\n [disabled]=\"Disabled\"\n (input)=\"OnValueChange($any($event.target).value)\"\n rows=\"5\"\n ></textarea>\n }\n }\n\n <!-- Number Input -->\n @if (QuestionType === 'number') {\n <mj-numeric-input\n [ngModel]=\"$any(Value)\"\n [ngModelOptions]=\"{standalone: true}\"\n [Min]=\"Min || 0\"\n [Max]=\"Max || 999999999\"\n [Step]=\"Step || 1\"\n [Disabled]=\"Disabled\"\n (ngModelChange)=\"OnValueChange($any($event))\"\n class=\"question-input\"\n ></mj-numeric-input>\n }\n\n <!-- Currency Input -->\n @if (QuestionType === 'currency') {\n <mj-numeric-input\n [ngModel]=\"$any(Value)\"\n [ngModelOptions]=\"{standalone: true}\"\n [Min]=\"Min || 0\"\n [Max]=\"Max || 999999999\"\n [Step]=\"Step || 0.01\"\n [Decimals]=\"2\"\n [Disabled]=\"Disabled\"\n (ngModelChange)=\"OnValueChange($any($event))\"\n class=\"question-input\"\n ></mj-numeric-input>\n }\n\n <!-- Date Picker -->\n @if (QuestionType === 'date') {\n <mj-datepicker\n [ngModel]=\"$any(Value)\"\n [ngModelOptions]=\"{standalone: true}\"\n [Disabled]=\"Disabled\"\n (ValueChange)=\"OnValueChange($any($event))\"\n class=\"question-input\"\n ></mj-datepicker>\n }\n\n <!-- DateTime Picker -->\n @if (QuestionType === 'datetime') {\n <input\n type=\"datetime-local\"\n class=\"mj-input question-input\"\n [value]=\"$any(Value)\"\n [disabled]=\"Disabled\"\n (change)=\"OnValueChange($any($event.target).value)\"\n />\n }\n\n <!-- Button Group -->\n @if (QuestionType === 'buttongroup') {\n <div class=\"mj-buttongroup question-buttongroup\">\n @for (option of Options; track TrackByValue($index, option)) {\n <button\n mjButton\n [toggleable]=\"true\"\n [selected]=\"Value === option.value\"\n [disabled]=\"Disabled\"\n (click)=\"OnValueChange(option.value)\"\n class=\"buttongroup-option\"\n >\n @if (option.icon) {\n <i class=\"fa {{ option.icon }}\" aria-hidden=\"true\"></i>\n }\n {{ option.label }}\n </button>\n }\n </div>\n }\n\n <!-- Radio Buttons -->\n @if (QuestionType === 'radio') {\n <div class=\"question-radio-group\">\n @for (option of Options; track TrackByValue($index, option)) {\n <div class=\"radio-option\">\n <input\n type=\"radio\"\n class=\"mj-radio\"\n [name]=\"Question.id\"\n [value]=\"option.value\"\n [checked]=\"Value === option.value\"\n [disabled]=\"Disabled\"\n (change)=\"OnValueChange(option.value)\"\n [id]=\"Question.id + '_' + option.value\"\n />\n <label [for]=\"Question.id + '_' + option.value\" class=\"radio-label\">\n @if (option.icon) {\n <i class=\"fa {{ option.icon }}\" aria-hidden=\"true\"></i>\n }\n {{ option.label }}\n </label>\n </div>\n }\n </div>\n }\n\n <!-- Dropdown -->\n @if (QuestionType === 'dropdown') {\n <select\n [formControl]=\"Control\"\n [disabled]=\"Disabled\"\n class=\"question-dropdown\"\n >\n <option value=\"\">Select an option...</option>\n @for (option of Options; track TrackByValue($index, option)) {\n <option [value]=\"option.value\">\n {{ option.label }}\n </option>\n }\n </select>\n }\n\n <!-- Slider -->\n @if (QuestionType === 'slider') {\n <div class=\"question-slider-container\">\n <input\n type=\"range\"\n class=\"mj-slider question-slider\"\n [min]=\"GetSliderConfig().min\"\n [max]=\"GetSliderConfig().max\"\n [step]=\"GetSliderConfig().step || 1\"\n [value]=\"$any(Value) || GetSliderConfig().min\"\n [disabled]=\"Disabled\"\n (input)=\"OnValueChange(+$any($event.target).value)\"\n />\n <div class=\"slider-value\">\n {{ Value || GetSliderConfig().min }}{{ GetSliderConfig().suffix || '' }}\n </div>\n </div>\n }\n\n <!-- Date Range -->\n @if (QuestionType === 'daterange') {\n <div class=\"question-daterange\">\n <mj-datepicker\n [ngModel]=\"$any(Value)?.start\"\n [ngModelOptions]=\"{standalone: true}\"\n [Disabled]=\"Disabled\"\n (ValueChange)=\"OnDateRangeStartChange($any($event))\"\n Placeholder=\"Start Date\"\n class=\"daterange-start\"\n ></mj-datepicker>\n <span class=\"daterange-separator\">to</span>\n <mj-datepicker\n [ngModel]=\"$any(Value)?.end\"\n [ngModelOptions]=\"{standalone: true}\"\n [Disabled]=\"Disabled\"\n (ValueChange)=\"OnDateRangeEndChange($any($event))\"\n Placeholder=\"End Date\"\n class=\"daterange-end\"\n ></mj-datepicker>\n </div>\n }\n\n <!-- Time Picker -->\n @if (QuestionType === 'time') {\n <input\n type=\"time\"\n class=\"mj-input question-input\"\n [value]=\"$any(Value)\"\n [disabled]=\"Disabled\"\n (change)=\"OnValueChange($any($event.target).value)\"\n />\n }\n\n <!-- Checkboxes -->\n @if (QuestionType === 'checkbox') {\n <div class=\"question-checkbox-group\">\n @for (option of Options; track TrackByValue($index, option)) {\n <div class=\"checkbox-option\">\n <input\n type=\"checkbox\"\n class=\"mj-checkbox\"\n [checked]=\"IsChecked(option)\"\n [disabled]=\"Disabled\"\n (change)=\"ToggleCheckbox(option)\"\n [id]=\"Question.id + '_' + option.value\"\n />\n <label [for]=\"Question.id + '_' + option.value\" class=\"checkbox-label\">\n @if (option.icon) {\n <i class=\"fa {{ option.icon }}\" aria-hidden=\"true\"></i>\n }\n {{ option.label }}\n </label>\n </div>\n }\n </div>\n }\n </div><!-- end input-container -->\n\n <!-- Validation Error -->\n @if (Control.invalid && Control.touched) {\n <div class=\"validation-error\">\n @if (Control.errors?.['required']) {\n <span>This field is required</span>\n }\n @if (Control.errors?.['email']) {\n <span>Please enter a valid email address</span>\n }\n @if (Control.errors?.['min']) {\n <span>Value must be at least {{ Min }}</span>\n }\n @if (Control.errors?.['max']) {\n <span>Value must be at most {{ Max }}</span>\n }\n </div>\n }\n</div>\n", styles: [".form-question {\n margin-bottom: 16px;\n}\n\n.question-label {\n display: block;\n font-weight: 500;\n margin-bottom: 6px;\n color: var(--mj-text-primary);\n font-size: 14px;\n}\n\n.required-indicator {\n color: var(--mj-status-error);\n margin-left: 2px;\n}\n\n.help-text {\n font-size: 12px;\n color: var(--mj-text-secondary);\n margin-bottom: 8px;\n font-style: italic;\n}\n\n/* Default widths - controlled by input-container width classes */\n.question-input,\n.question-textarea,\n.question-dropdown {\n width: 100%;\n max-width: 100%;\n}\n\n.question-dropdown {\n padding: 8px 12px;\n border: 1px solid var(--mj-border-default);\n border-radius: 4px;\n font-size: 14px;\n font-family: inherit;\n background-color: var(--mj-bg-surface);\n background-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23666' d='M6 9L1 4h10z'/%3E%3C/svg%3E\");\n background-repeat: no-repeat;\n background-position: right 8px center;\n background-size: 12px;\n padding-right: 32px;\n cursor: pointer;\n transition: border-color 0.2s ease, box-shadow 0.2s ease;\n appearance: none;\n -webkit-appearance: none;\n -moz-appearance: none;\n color: var(--mj-text-primary);\n}\n\n.question-dropdown:hover {\n border-color: var(--mj-border-strong);\n}\n\n.question-dropdown:focus {\n outline: none;\n border-color: var(--mj-brand-primary);\n box-shadow: 0 0 0 3px color-mix(in srgb, var(--mj-brand-primary) 10%, transparent);\n}\n\n.question-dropdown:disabled {\n background-color: var(--mj-bg-surface-sunken);\n cursor: not-allowed;\n opacity: 0.6;\n}\n\n.question-textarea {\n resize: vertical;\n min-height: 100px;\n}\n\n.question-buttongroup {\n display: flex;\n flex-wrap: nowrap;\n gap: 8px;\n overflow-x: auto;\n overflow-y: hidden;\n -webkit-overflow-scrolling: touch;\n scrollbar-width: thin;\n scrollbar-color: color-mix(in srgb, var(--mj-brand-primary) 30%, transparent) transparent;\n}\n\n.question-buttongroup::-webkit-scrollbar {\n height: 6px;\n}\n\n.question-buttongroup::-webkit-scrollbar-track {\n background: transparent;\n}\n\n.question-buttongroup::-webkit-scrollbar-thumb {\n background: color-mix(in srgb, var(--mj-brand-primary) 30%, transparent);\n border-radius: 3px;\n}\n\n.question-buttongroup::-webkit-scrollbar-thumb:hover {\n background: color-mix(in srgb, var(--mj-brand-primary) 50%, transparent);\n}\n\n.buttongroup-option {\n flex: 0 0 auto;\n white-space: nowrap;\n}\n\n.buttongroup-option i {\n margin-right: 6px;\n}\n\n.question-radio-group,\n.question-checkbox-group {\n display: flex;\n flex-wrap: wrap;\n gap: 12px 16px;\n}\n\n.radio-option,\n.checkbox-option {\n display: flex;\n align-items: center;\n gap: 8px;\n min-width: 120px;\n flex: 0 0 auto;\n}\n\n.radio-label,\n.checkbox-label {\n cursor: pointer;\n user-select: none;\n margin: 0;\n display: flex;\n align-items: center;\n gap: 6px;\n}\n\n.radio-label i,\n.checkbox-label i {\n color: var(--mj-text-secondary);\n}\n\n.validation-error {\n color: var(--mj-status-error);\n font-size: 12px;\n margin-top: 4px;\n}\n\n/* Required field styling */\n.form-question.required .question-label {\n font-weight: 600;\n}\n\n/* Slider styling */\n.question-slider-container {\n width: 100%;\n}\n\n.question-slider {\n width: 100%;\n margin-bottom: 8px;\n}\n\n.slider-value {\n text-align: center;\n font-size: 16px;\n font-weight: 600;\n color: var(--mj-brand-primary);\n padding: 4px 0;\n}\n\n/* Date range styling */\n.question-daterange {\n display: flex;\n align-items: center;\n gap: 12px;\n flex-wrap: wrap;\n}\n\n.daterange-start,\n.daterange-end {\n flex: 1;\n min-width: 150px;\n}\n\n.daterange-separator {\n font-weight: 500;\n color: var(--mj-text-secondary);\n white-space: nowrap;\n}\n\n/* Mobile adjustments for date range */\n@media (max-width: 480px) {\n .question-daterange {\n flex-direction: column;\n align-items: stretch;\n }\n\n .daterange-separator {\n text-align: center;\n padding: 4px 0;\n }\n}\n\n/* ===== Semantic Width Classes ===== */\n/* Controls field width based on expected content length */\n\n.input-container {\n display: block;\n}\n\n/* Narrow fields - for numbers, dates, times, short codes */\n.input-container.width-narrow {\n max-width: 200px;\n}\n\n.input-container.width-narrow .question-input,\n.input-container.width-narrow .question-dropdown {\n width: 100%;\n}\n\n/* Medium fields - for names, cities, phone numbers (DEFAULT) */\n.input-container.width-medium {\n max-width: 450px;\n}\n\n.input-container.width-medium .question-input,\n.input-container.width-medium .question-dropdown {\n width: 100%;\n}\n\n/* Wide fields - for emails, URLs, long text */\n.input-container.width-wide {\n max-width: 100%;\n}\n\n.input-container.width-wide .question-input,\n.input-container.width-wide .question-dropdown {\n width: 100%;\n}\n\n/* Full width - for textarea, search fields */\n.input-container.width-full {\n max-width: 100%;\n}\n\n.input-container.width-full .question-input,\n.input-container.width-full .question-textarea,\n.input-container.width-full .question-dropdown {\n width: 100%;\n}\n\n/* Auto-width - for dropdowns, radio, checkbox (sizes to content) */\n.input-container.width-auto {\n max-width: 350px;\n min-width: 150px;\n width: auto;\n}\n\n.input-container.width-auto .question-dropdown {\n width: 100%;\n}\n\n/* Responsive adjustments for smaller screens */\n@media (max-width: 600px) {\n .input-container.width-narrow,\n .input-container.width-medium,\n .input-container.width-wide,\n .input-container.width-auto {\n max-width: 100%;\n }\n}\n\n/* \u2500\u2500 Markdown textarea (question.type.markdown) \u2014 rendered preview + Edit toggle \u2500\u2500 */\n.markdown-field {\n border: 1px solid var(--mj-border-default);\n border-radius: 10px;\n background: var(--mj-bg-surface);\n overflow: hidden;\n}\n.markdown-field-toolbar {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 8px;\n padding: 6px 10px;\n background: var(--mj-bg-surface-sunken);\n border-bottom: 1px solid var(--mj-border-subtle);\n}\n.markdown-field-hint {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n font-size: 11px;\n font-weight: 500;\n color: var(--mj-text-muted);\n}\n.markdown-field-toggle {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n font: inherit;\n font-size: 11.5px;\n font-weight: 600;\n padding: 4px 10px;\n border-radius: 7px;\n border: 1px solid var(--mj-border-default);\n background: var(--mj-bg-surface);\n color: var(--mj-text-secondary);\n cursor: pointer;\n transition: color 0.15s ease, border-color 0.15s ease;\n}\n.markdown-field-toggle:hover:not(:disabled) {\n color: var(--mj-brand-primary);\n border-color: color-mix(in srgb, var(--mj-brand-primary) 45%, var(--mj-border-default));\n}\n.markdown-field-toggle:disabled {\n opacity: 0.5;\n cursor: not-allowed;\n}\n.markdown-field-preview {\n padding: 12px 14px;\n max-height: 420px;\n overflow-y: auto;\n font-size: 13.5px;\n line-height: 1.6;\n color: var(--mj-text-primary);\n}\n/* the raw editor inside the panel loses its own border (the panel provides it) */\n.markdown-field .question-textarea {\n border: none;\n border-radius: 0;\n width: 100%;\n}\n"] }]
|
|
233
255
|
}], propDecorators: { Question: [{
|
|
234
256
|
type: Input
|
|
235
257
|
}], Control: [{
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dynamic-form-field.component.js","sourceRoot":"","sources":["../../../src/lib/dynamic-form-field/dynamic-form-field.component.ts","../../../src/lib/dynamic-form-field/dynamic-form-field.component.html"],"names":[],"mappings":"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","sourcesContent":["/**\n * @fileoverview Dynamic Form Field Component\n *\n * Renders a single form field based on a `FormQuestion` definition from\n * `@memberjunction/ai-core-plus`. Supports 14+ input types including text,\n * textarea, email, number, currency, date, datetime, time, daterange,\n * buttongroup, radio, dropdown, checkbox, and slider.\n *\n * Implements `ControlValueAccessor` for seamless integration with Angular\n * Reactive Forms. Width is automatically sized based on field type or an\n * explicit `widthHint` on the question definition.\n *\n * @example\n * ```html\n * <mj-dynamic-form-field\n * [Question]=\"question\"\n * [Control]=\"formGroup.get(question.id)\"\n * [formControlName]=\"question.id\">\n * </mj-dynamic-form-field>\n * ```\n *\n * @module @memberjunction/ng-forms\n */\n\nimport { Component, Input, forwardRef, OnInit } from '@angular/core';\nimport { ControlValueAccessor, NG_VALUE_ACCESSOR, FormControl } from '@angular/forms';\nimport { FormQuestion, FormOption } from '@memberjunction/ai-core-plus';\n\n@Component({\n standalone: false,\n selector: 'mj-dynamic-form-field',\n templateUrl: './dynamic-form-field.component.html',\n styleUrls: ['./dynamic-form-field.component.css'],\n providers: [\n {\n provide: NG_VALUE_ACCESSOR,\n useExisting: forwardRef(() => DynamicFormFieldComponent),\n multi: true\n }\n ]\n})\nexport class DynamicFormFieldComponent implements ControlValueAccessor, OnInit {\n /**\n * The question definition that drives this field's rendering.\n * Determines the input type, label, validation, options, and layout.\n */\n @Input() Question!: FormQuestion;\n\n /**\n * The reactive FormControl bound to this field.\n * Used for validation state display and dropdown binding.\n */\n @Input() Control!: FormControl;\n\n /** Current field value (managed by ControlValueAccessor) */\n public Value: string | number | boolean | Date | Record<string, Date> | (string | number | boolean)[] | null = null;\n\n /** Whether this field is disabled */\n public Disabled = false;\n\n private onChange: (value: string | number | boolean | Date | Record<string, Date> | (string | number | boolean)[] | null) => void = () => {};\n private onTouched: () => void = () => {};\n\n ngOnInit(): void {\n // No-op; field is configured via inputs\n }\n\n /**\n * Resolved question type string (handles both simple string and object types).\n */\n public get QuestionType(): string {\n return typeof this.Question.type === 'string'\n ? this.Question.type\n : this.Question.type.type;\n }\n\n /**\n * Whether this is a choice-based question (buttongroup, radio, dropdown, checkbox).\n */\n public get IsChoiceQuestion(): boolean {\n return ['buttongroup', 'radio', 'dropdown', 'checkbox'].includes(this.QuestionType);\n }\n\n /**\n * Available options for choice-type questions.\n */\n public get Options(): FormOption[] {\n if (typeof this.Question.type === 'object' && 'options' in this.Question.type) {\n return this.Question.type.options;\n }\n return [];\n }\n\n /**\n * Whether multiple selections are allowed (for checkbox type).\n */\n public get AllowMultiple(): boolean {\n return this.QuestionType === 'checkbox' ||\n (typeof this.Question.type === 'object' && 'multiple' in this.Question.type && !!this.Question.type.multiple);\n }\n\n /**\n * CSS class controlling the field width based on `widthHint` or intelligent defaults.\n *\n * Width mapping:\n * - `'narrow'` (~200px): numbers, dates, times\n * - `'medium'` (~450px): text inputs (default)\n * - `'wide'` (100%): emails, buttongroup, radio, checkbox\n * - `'full'` (100%): textarea\n * - `'auto'` (150–350px): dropdowns\n */\n public get WidthClass(): string {\n if (this.Question.widthHint) {\n return `width-${this.Question.widthHint}`;\n }\n const type = this.QuestionType;\n if (['number', 'currency', 'date', 'datetime', 'time'].includes(type)) return 'width-narrow';\n if (['buttongroup', 'radio', 'checkbox'].includes(type)) return 'width-wide';\n if (type === 'dropdown') return 'width-auto';\n if (type === 'textarea') return 'width-full';\n if (type === 'email') return 'width-wide';\n return 'width-medium';\n }\n\n /** Placeholder text for text-based inputs */\n public get Placeholder(): string | undefined {\n if (typeof this.Question.type === 'object' && 'placeholder' in this.Question.type) {\n return this.Question.type.placeholder as string;\n }\n return undefined;\n }\n\n /** Minimum value for numeric inputs */\n public get Min(): number | undefined {\n if (typeof this.Question.type === 'object' && 'min' in this.Question.type) {\n return this.Question.type.min as number;\n }\n return undefined;\n }\n\n /** Maximum value for numeric inputs */\n public get Max(): number | undefined {\n if (typeof this.Question.type === 'object' && 'max' in this.Question.type) {\n return this.Question.type.max as number;\n }\n return undefined;\n }\n\n /** Step increment for numeric inputs */\n public get Step(): number | undefined {\n if (typeof this.Question.type === 'object' && 'step' in this.Question.type) {\n return this.Question.type.step as number;\n }\n return undefined;\n }\n\n /** Prefix string for currency inputs (e.g. \"$\") */\n public get Prefix(): string | undefined {\n if (typeof this.Question.type === 'object' && 'prefix' in this.Question.type) {\n return this.Question.type.prefix as string;\n }\n return undefined;\n }\n\n /** Suffix string for currency/slider inputs (e.g. \"USD\", \"%\") */\n public get Suffix(): string | undefined {\n if (typeof this.Question.type === 'object' && 'suffix' in this.Question.type) {\n return this.Question.type.suffix as string;\n }\n return undefined;\n }\n\n /** Handle value changes from any input type */\n public OnValueChange(newValue: string | number | boolean | Date | Record<string, Date> | (string | number | boolean)[] | null): void {\n this.Value = newValue;\n this.onChange(newValue);\n this.onTouched();\n }\n\n /** Toggle a checkbox option (supports single and multiple selection) */\n public ToggleCheckbox(option: FormOption): void {\n if (!this.AllowMultiple) {\n this.OnValueChange(option.value);\n } else {\n const currentValues = Array.isArray(this.Value) ? this.Value : [];\n const index = currentValues.indexOf(option.value);\n const newValues = index > -1\n ? currentValues.filter((v: string | number | boolean) => v !== option.value)\n : [...currentValues, option.value];\n this.OnValueChange(newValues);\n }\n }\n\n /** Check whether a checkbox option is currently selected */\n public IsChecked(option: FormOption): boolean {\n if (!this.AllowMultiple) {\n return this.Value === option.value;\n }\n return Array.isArray(this.Value) && this.Value.includes(option.value);\n }\n\n /** Get slider configuration with defaults */\n public GetSliderConfig(): { min: number; max: number; step?: number; suffix?: string } {\n if (typeof this.Question.type === 'object' && this.Question.type.type === 'slider') {\n return {\n min: this.Question.type.min,\n max: this.Question.type.max,\n step: this.Question.type.step,\n suffix: this.Question.type.suffix\n };\n }\n return { min: 0, max: 100, step: 1 };\n }\n\n /** Handle date range start date change */\n public OnDateRangeStartChange(value: Date): void {\n const current = (this.Value as Record<string, Date>) || {};\n this.OnValueChange({ ...current, start: value });\n }\n\n /** Handle date range end date change */\n public OnDateRangeEndChange(value: Date): void {\n const current = (this.Value as Record<string, Date>) || {};\n this.OnValueChange({ ...current, end: value });\n }\n\n /** Track-by function for options lists */\n public TrackByValue(_index: number, option: FormOption): string | number | boolean {\n return option.value;\n }\n\n // ControlValueAccessor implementation\n writeValue(value: string | number | boolean | Date | Record<string, Date> | (string | number | boolean)[] | null): void {\n this.Value = value;\n }\n\n registerOnChange(fn: (value: string | number | boolean | Date | Record<string, Date> | (string | number | boolean)[] | null) => void): void {\n this.onChange = fn;\n }\n\n registerOnTouched(fn: () => void): void {\n this.onTouched = fn;\n }\n\n setDisabledState(isDisabled: boolean): void {\n this.Disabled = isDisabled;\n }\n}\n","<div class=\"form-question\" [class.required]=\"Question.required\">\n <!-- Label -->\n @if (Question.label) {\n <label class=\"question-label\">\n {{ Question.label }}\n @if (Question.required) {\n <span class=\"required-indicator\">*</span>\n }\n </label>\n }\n\n <!-- Help Text -->\n @if (Question.helpText) {\n <div class=\"help-text\">\n {{ Question.helpText }}\n </div>\n }\n\n <!-- Input Container with width class -->\n <div class=\"input-container\" [ngClass]=\"WidthClass\">\n\n <!-- Text Input -->\n @if (QuestionType === 'text') {\n <input\n type=\"text\"\n class=\"mj-input question-input\"\n [value]=\"$any(Value) || ''\"\n [placeholder]=\"Placeholder\"\n [disabled]=\"Disabled\"\n (input)=\"OnValueChange($any($event.target).value)\"\n />\n }\n\n <!-- Email Input -->\n @if (QuestionType === 'email') {\n <input\n type=\"email\"\n class=\"mj-input question-input\"\n [value]=\"$any(Value) || ''\"\n [placeholder]=\"Placeholder\"\n [disabled]=\"Disabled\"\n (input)=\"OnValueChange($any($event.target).value)\"\n />\n }\n\n <!-- Textarea -->\n @if (QuestionType === 'textarea') {\n <textarea\n class=\"mj-textarea question-textarea\"\n [value]=\"$any(Value) || ''\"\n [placeholder]=\"Placeholder\"\n [disabled]=\"Disabled\"\n (input)=\"OnValueChange($any($event.target).value)\"\n rows=\"5\"\n ></textarea>\n }\n\n <!-- Number Input -->\n @if (QuestionType === 'number') {\n <mj-numeric-input\n [ngModel]=\"$any(Value)\"\n [ngModelOptions]=\"{standalone: true}\"\n [Min]=\"Min || 0\"\n [Max]=\"Max || 999999999\"\n [Step]=\"Step || 1\"\n [Disabled]=\"Disabled\"\n (ngModelChange)=\"OnValueChange($any($event))\"\n class=\"question-input\"\n ></mj-numeric-input>\n }\n\n <!-- Currency Input -->\n @if (QuestionType === 'currency') {\n <mj-numeric-input\n [ngModel]=\"$any(Value)\"\n [ngModelOptions]=\"{standalone: true}\"\n [Min]=\"Min || 0\"\n [Max]=\"Max || 999999999\"\n [Step]=\"Step || 0.01\"\n [Decimals]=\"2\"\n [Disabled]=\"Disabled\"\n (ngModelChange)=\"OnValueChange($any($event))\"\n class=\"question-input\"\n ></mj-numeric-input>\n }\n\n <!-- Date Picker -->\n @if (QuestionType === 'date') {\n <mj-datepicker\n [ngModel]=\"$any(Value)\"\n [ngModelOptions]=\"{standalone: true}\"\n [Disabled]=\"Disabled\"\n (ValueChange)=\"OnValueChange($any($event))\"\n class=\"question-input\"\n ></mj-datepicker>\n }\n\n <!-- DateTime Picker -->\n @if (QuestionType === 'datetime') {\n <input\n type=\"datetime-local\"\n class=\"mj-input question-input\"\n [value]=\"$any(Value)\"\n [disabled]=\"Disabled\"\n (change)=\"OnValueChange($any($event.target).value)\"\n />\n }\n\n <!-- Button Group -->\n @if (QuestionType === 'buttongroup') {\n <div class=\"mj-buttongroup question-buttongroup\">\n @for (option of Options; track TrackByValue($index, option)) {\n <button\n mjButton\n [toggleable]=\"true\"\n [selected]=\"Value === option.value\"\n [disabled]=\"Disabled\"\n (click)=\"OnValueChange(option.value)\"\n class=\"buttongroup-option\"\n >\n @if (option.icon) {\n <i class=\"fa {{ option.icon }}\" aria-hidden=\"true\"></i>\n }\n {{ option.label }}\n </button>\n }\n </div>\n }\n\n <!-- Radio Buttons -->\n @if (QuestionType === 'radio') {\n <div class=\"question-radio-group\">\n @for (option of Options; track TrackByValue($index, option)) {\n <div class=\"radio-option\">\n <input\n type=\"radio\"\n class=\"mj-radio\"\n [name]=\"Question.id\"\n [value]=\"option.value\"\n [checked]=\"Value === option.value\"\n [disabled]=\"Disabled\"\n (change)=\"OnValueChange(option.value)\"\n [id]=\"Question.id + '_' + option.value\"\n />\n <label [for]=\"Question.id + '_' + option.value\" class=\"radio-label\">\n @if (option.icon) {\n <i class=\"fa {{ option.icon }}\" aria-hidden=\"true\"></i>\n }\n {{ option.label }}\n </label>\n </div>\n }\n </div>\n }\n\n <!-- Dropdown -->\n @if (QuestionType === 'dropdown') {\n <select\n [formControl]=\"Control\"\n [disabled]=\"Disabled\"\n class=\"question-dropdown\"\n >\n <option value=\"\">Select an option...</option>\n @for (option of Options; track TrackByValue($index, option)) {\n <option [value]=\"option.value\">\n {{ option.label }}\n </option>\n }\n </select>\n }\n\n <!-- Slider -->\n @if (QuestionType === 'slider') {\n <div class=\"question-slider-container\">\n <input\n type=\"range\"\n class=\"mj-slider question-slider\"\n [min]=\"GetSliderConfig().min\"\n [max]=\"GetSliderConfig().max\"\n [step]=\"GetSliderConfig().step || 1\"\n [value]=\"$any(Value) || GetSliderConfig().min\"\n [disabled]=\"Disabled\"\n (input)=\"OnValueChange(+$any($event.target).value)\"\n />\n <div class=\"slider-value\">\n {{ Value || GetSliderConfig().min }}{{ GetSliderConfig().suffix || '' }}\n </div>\n </div>\n }\n\n <!-- Date Range -->\n @if (QuestionType === 'daterange') {\n <div class=\"question-daterange\">\n <mj-datepicker\n [ngModel]=\"$any(Value)?.start\"\n [ngModelOptions]=\"{standalone: true}\"\n [Disabled]=\"Disabled\"\n (ValueChange)=\"OnDateRangeStartChange($any($event))\"\n Placeholder=\"Start Date\"\n class=\"daterange-start\"\n ></mj-datepicker>\n <span class=\"daterange-separator\">to</span>\n <mj-datepicker\n [ngModel]=\"$any(Value)?.end\"\n [ngModelOptions]=\"{standalone: true}\"\n [Disabled]=\"Disabled\"\n (ValueChange)=\"OnDateRangeEndChange($any($event))\"\n Placeholder=\"End Date\"\n class=\"daterange-end\"\n ></mj-datepicker>\n </div>\n }\n\n <!-- Time Picker -->\n @if (QuestionType === 'time') {\n <input\n type=\"time\"\n class=\"mj-input question-input\"\n [value]=\"$any(Value)\"\n [disabled]=\"Disabled\"\n (change)=\"OnValueChange($any($event.target).value)\"\n />\n }\n\n <!-- Checkboxes -->\n @if (QuestionType === 'checkbox') {\n <div class=\"question-checkbox-group\">\n @for (option of Options; track TrackByValue($index, option)) {\n <div class=\"checkbox-option\">\n <input\n type=\"checkbox\"\n class=\"mj-checkbox\"\n [checked]=\"IsChecked(option)\"\n [disabled]=\"Disabled\"\n (change)=\"ToggleCheckbox(option)\"\n [id]=\"Question.id + '_' + option.value\"\n />\n <label [for]=\"Question.id + '_' + option.value\" class=\"checkbox-label\">\n @if (option.icon) {\n <i class=\"fa {{ option.icon }}\" aria-hidden=\"true\"></i>\n }\n {{ option.label }}\n </label>\n </div>\n }\n </div>\n }\n </div><!-- end input-container -->\n\n <!-- Validation Error -->\n @if (Control.invalid && Control.touched) {\n <div class=\"validation-error\">\n @if (Control.errors?.['required']) {\n <span>This field is required</span>\n }\n @if (Control.errors?.['email']) {\n <span>Please enter a valid email address</span>\n }\n @if (Control.errors?.['min']) {\n <span>Value must be at least {{ Min }}</span>\n }\n @if (Control.errors?.['max']) {\n <span>Value must be at most {{ Max }}</span>\n }\n </div>\n }\n</div>\n"]}
|
|
1
|
+
{"version":3,"file":"dynamic-form-field.component.js","sourceRoot":"","sources":["../../../src/lib/dynamic-form-field/dynamic-form-field.component.ts","../../../src/lib/dynamic-form-field/dynamic-form-field.component.html"],"names":[],"mappings":"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","sourcesContent":["/**\n * @fileoverview Dynamic Form Field Component\n *\n * Renders a single form field based on a `FormQuestion` definition from\n * `@memberjunction/ai-core-plus`. Supports 14+ input types including text,\n * textarea, email, number, currency, date, datetime, time, daterange,\n * buttongroup, radio, dropdown, checkbox, and slider.\n *\n * Implements `ControlValueAccessor` for seamless integration with Angular\n * Reactive Forms. Width is automatically sized based on field type or an\n * explicit `widthHint` on the question definition.\n *\n * @example\n * ```html\n * <mj-dynamic-form-field\n * [Question]=\"question\"\n * [Control]=\"formGroup.get(question.id)\"\n * [formControlName]=\"question.id\">\n * </mj-dynamic-form-field>\n * ```\n *\n * @module @memberjunction/ng-forms\n */\n\nimport { Component, Input, forwardRef, OnInit } from '@angular/core';\nimport { ControlValueAccessor, NG_VALUE_ACCESSOR, FormControl } from '@angular/forms';\nimport { FormQuestion, FormOption } from '@memberjunction/ai-core-plus';\n\n@Component({\n standalone: false,\n selector: 'mj-dynamic-form-field',\n templateUrl: './dynamic-form-field.component.html',\n styleUrls: ['./dynamic-form-field.component.css'],\n providers: [\n {\n provide: NG_VALUE_ACCESSOR,\n useExisting: forwardRef(() => DynamicFormFieldComponent),\n multi: true\n }\n ]\n})\nexport class DynamicFormFieldComponent implements ControlValueAccessor, OnInit {\n /**\n * The question definition that drives this field's rendering.\n * Determines the input type, label, validation, options, and layout.\n */\n @Input() Question!: FormQuestion;\n\n /**\n * The reactive FormControl bound to this field.\n * Used for validation state display and dropdown binding.\n */\n @Input() Control!: FormControl;\n\n /** Current field value (managed by ControlValueAccessor) */\n public Value: string | number | boolean | Date | Record<string, Date> | (string | number | boolean)[] | null = null;\n\n /** Whether this field is disabled */\n public Disabled = false;\n\n private onChange: (value: string | number | boolean | Date | Record<string, Date> | (string | number | boolean)[] | null) => void = () => {};\n private onTouched: () => void = () => {};\n\n ngOnInit(): void {\n // No-op; field is configured via inputs\n }\n\n /**\n * Resolved question type string (handles both simple string and object types).\n */\n public get QuestionType(): string {\n return typeof this.Question.type === 'string'\n ? this.Question.type\n : this.Question.type.type;\n }\n\n /**\n * Whether this is a choice-based question (buttongroup, radio, dropdown, checkbox).\n */\n public get IsChoiceQuestion(): boolean {\n return ['buttongroup', 'radio', 'dropdown', 'checkbox'].includes(this.QuestionType);\n }\n\n /**\n * Textarea only: whether the value renders as formatted Markdown with an Edit toggle\n * (question.type.markdown === true). Used for rich agent-authored content the human\n * reviews more than types — e.g. the Plan Mode approval form.\n */\n public get IsMarkdown(): boolean {\n return (\n this.QuestionType === 'textarea' &&\n typeof this.Question.type === 'object' &&\n 'markdown' in this.Question.type &&\n this.Question.type.markdown === true\n );\n }\n\n /** Markdown textareas start in preview; the user toggles into the raw editor as needed. */\n public MarkdownEditing = false;\n\n /** Flip a markdown textarea between rendered preview and the raw editor. */\n public ToggleMarkdownEditing(): void {\n this.MarkdownEditing = !this.MarkdownEditing;\n }\n\n /** The current value as a string for the markdown preview (empty-safe). */\n public get MarkdownValue(): string {\n return typeof this.Value === 'string' ? this.Value : '';\n }\n\n /**\n * Available options for choice-type questions.\n */\n public get Options(): FormOption[] {\n if (typeof this.Question.type === 'object' && 'options' in this.Question.type) {\n return this.Question.type.options;\n }\n return [];\n }\n\n /**\n * Whether multiple selections are allowed (for checkbox type).\n */\n public get AllowMultiple(): boolean {\n return this.QuestionType === 'checkbox' ||\n (typeof this.Question.type === 'object' && 'multiple' in this.Question.type && !!this.Question.type.multiple);\n }\n\n /**\n * CSS class controlling the field width based on `widthHint` or intelligent defaults.\n *\n * Width mapping:\n * - `'narrow'` (~200px): numbers, dates, times\n * - `'medium'` (~450px): text inputs (default)\n * - `'wide'` (100%): emails, buttongroup, radio, checkbox\n * - `'full'` (100%): textarea\n * - `'auto'` (150–350px): dropdowns\n */\n public get WidthClass(): string {\n if (this.Question.widthHint) {\n return `width-${this.Question.widthHint}`;\n }\n const type = this.QuestionType;\n if (['number', 'currency', 'date', 'datetime', 'time'].includes(type)) return 'width-narrow';\n if (['buttongroup', 'radio', 'checkbox'].includes(type)) return 'width-wide';\n if (type === 'dropdown') return 'width-auto';\n if (type === 'textarea') return 'width-full';\n if (type === 'email') return 'width-wide';\n return 'width-medium';\n }\n\n /** Placeholder text for text-based inputs */\n public get Placeholder(): string | undefined {\n if (typeof this.Question.type === 'object' && 'placeholder' in this.Question.type) {\n return this.Question.type.placeholder as string;\n }\n return undefined;\n }\n\n /** Minimum value for numeric inputs */\n public get Min(): number | undefined {\n if (typeof this.Question.type === 'object' && 'min' in this.Question.type) {\n return this.Question.type.min as number;\n }\n return undefined;\n }\n\n /** Maximum value for numeric inputs */\n public get Max(): number | undefined {\n if (typeof this.Question.type === 'object' && 'max' in this.Question.type) {\n return this.Question.type.max as number;\n }\n return undefined;\n }\n\n /** Step increment for numeric inputs */\n public get Step(): number | undefined {\n if (typeof this.Question.type === 'object' && 'step' in this.Question.type) {\n return this.Question.type.step as number;\n }\n return undefined;\n }\n\n /** Prefix string for currency inputs (e.g. \"$\") */\n public get Prefix(): string | undefined {\n if (typeof this.Question.type === 'object' && 'prefix' in this.Question.type) {\n return this.Question.type.prefix as string;\n }\n return undefined;\n }\n\n /** Suffix string for currency/slider inputs (e.g. \"USD\", \"%\") */\n public get Suffix(): string | undefined {\n if (typeof this.Question.type === 'object' && 'suffix' in this.Question.type) {\n return this.Question.type.suffix as string;\n }\n return undefined;\n }\n\n /** Handle value changes from any input type */\n public OnValueChange(newValue: string | number | boolean | Date | Record<string, Date> | (string | number | boolean)[] | null): void {\n this.Value = newValue;\n this.onChange(newValue);\n this.onTouched();\n }\n\n /** Toggle a checkbox option (supports single and multiple selection) */\n public ToggleCheckbox(option: FormOption): void {\n if (!this.AllowMultiple) {\n this.OnValueChange(option.value);\n } else {\n const currentValues = Array.isArray(this.Value) ? this.Value : [];\n const index = currentValues.indexOf(option.value);\n const newValues = index > -1\n ? currentValues.filter((v: string | number | boolean) => v !== option.value)\n : [...currentValues, option.value];\n this.OnValueChange(newValues);\n }\n }\n\n /** Check whether a checkbox option is currently selected */\n public IsChecked(option: FormOption): boolean {\n if (!this.AllowMultiple) {\n return this.Value === option.value;\n }\n return Array.isArray(this.Value) && this.Value.includes(option.value);\n }\n\n /** Get slider configuration with defaults */\n public GetSliderConfig(): { min: number; max: number; step?: number; suffix?: string } {\n if (typeof this.Question.type === 'object' && this.Question.type.type === 'slider') {\n return {\n min: this.Question.type.min,\n max: this.Question.type.max,\n step: this.Question.type.step,\n suffix: this.Question.type.suffix\n };\n }\n return { min: 0, max: 100, step: 1 };\n }\n\n /** Handle date range start date change */\n public OnDateRangeStartChange(value: Date): void {\n const current = (this.Value as Record<string, Date>) || {};\n this.OnValueChange({ ...current, start: value });\n }\n\n /** Handle date range end date change */\n public OnDateRangeEndChange(value: Date): void {\n const current = (this.Value as Record<string, Date>) || {};\n this.OnValueChange({ ...current, end: value });\n }\n\n /** Track-by function for options lists */\n public TrackByValue(_index: number, option: FormOption): string | number | boolean {\n return option.value;\n }\n\n // ControlValueAccessor implementation\n writeValue(value: string | number | boolean | Date | Record<string, Date> | (string | number | boolean)[] | null): void {\n this.Value = value;\n }\n\n registerOnChange(fn: (value: string | number | boolean | Date | Record<string, Date> | (string | number | boolean)[] | null) => void): void {\n this.onChange = fn;\n }\n\n registerOnTouched(fn: () => void): void {\n this.onTouched = fn;\n }\n\n setDisabledState(isDisabled: boolean): void {\n this.Disabled = isDisabled;\n }\n}\n","<div class=\"form-question\" [class.required]=\"Question.required\">\n <!-- Label -->\n @if (Question.label) {\n <label class=\"question-label\">\n {{ Question.label }}\n @if (Question.required) {\n <span class=\"required-indicator\">*</span>\n }\n </label>\n }\n\n <!-- Help Text -->\n @if (Question.helpText) {\n <div class=\"help-text\">\n {{ Question.helpText }}\n </div>\n }\n\n <!-- Input Container with width class -->\n <div class=\"input-container\" [ngClass]=\"WidthClass\">\n\n <!-- Text Input -->\n @if (QuestionType === 'text') {\n <input\n type=\"text\"\n class=\"mj-input question-input\"\n [value]=\"$any(Value) || ''\"\n [placeholder]=\"Placeholder\"\n [disabled]=\"Disabled\"\n (input)=\"OnValueChange($any($event.target).value)\"\n />\n }\n\n <!-- Email Input -->\n @if (QuestionType === 'email') {\n <input\n type=\"email\"\n class=\"mj-input question-input\"\n [value]=\"$any(Value) || ''\"\n [placeholder]=\"Placeholder\"\n [disabled]=\"Disabled\"\n (input)=\"OnValueChange($any($event.target).value)\"\n />\n }\n\n <!-- Textarea (markdown mode: rendered preview by default, Edit toggles the raw editor) -->\n @if (QuestionType === 'textarea') {\n @if (IsMarkdown) {\n <div class=\"markdown-field\" [class.editing]=\"MarkdownEditing\">\n <div class=\"markdown-field-toolbar\">\n <span class=\"markdown-field-hint\">\n <i class=\"fa-brands fa-markdown\" aria-hidden=\"true\"></i>\n {{ MarkdownEditing ? 'Editing Markdown' : 'Formatted preview' }}\n </span>\n <button\n type=\"button\"\n class=\"markdown-field-toggle\"\n [disabled]=\"Disabled\"\n (click)=\"ToggleMarkdownEditing()\"\n [title]=\"MarkdownEditing ? 'Show the formatted preview' : 'Edit the raw Markdown'\"\n >\n <i class=\"fa-solid\" [ngClass]=\"MarkdownEditing ? 'fa-eye' : 'fa-pen'\" aria-hidden=\"true\"></i>\n {{ MarkdownEditing ? 'Preview' : 'Edit' }}\n </button>\n </div>\n @if (!MarkdownEditing) {\n <div class=\"markdown-field-preview\">\n <mj-markdown [data]=\"MarkdownValue\"></mj-markdown>\n </div>\n }\n @if (MarkdownEditing) {\n <textarea\n class=\"mj-textarea question-textarea\"\n [value]=\"$any(Value) || ''\"\n [placeholder]=\"Placeholder\"\n [disabled]=\"Disabled\"\n (input)=\"OnValueChange($any($event.target).value)\"\n rows=\"10\"\n ></textarea>\n }\n </div>\n } @else {\n <textarea\n class=\"mj-textarea question-textarea\"\n [value]=\"$any(Value) || ''\"\n [placeholder]=\"Placeholder\"\n [disabled]=\"Disabled\"\n (input)=\"OnValueChange($any($event.target).value)\"\n rows=\"5\"\n ></textarea>\n }\n }\n\n <!-- Number Input -->\n @if (QuestionType === 'number') {\n <mj-numeric-input\n [ngModel]=\"$any(Value)\"\n [ngModelOptions]=\"{standalone: true}\"\n [Min]=\"Min || 0\"\n [Max]=\"Max || 999999999\"\n [Step]=\"Step || 1\"\n [Disabled]=\"Disabled\"\n (ngModelChange)=\"OnValueChange($any($event))\"\n class=\"question-input\"\n ></mj-numeric-input>\n }\n\n <!-- Currency Input -->\n @if (QuestionType === 'currency') {\n <mj-numeric-input\n [ngModel]=\"$any(Value)\"\n [ngModelOptions]=\"{standalone: true}\"\n [Min]=\"Min || 0\"\n [Max]=\"Max || 999999999\"\n [Step]=\"Step || 0.01\"\n [Decimals]=\"2\"\n [Disabled]=\"Disabled\"\n (ngModelChange)=\"OnValueChange($any($event))\"\n class=\"question-input\"\n ></mj-numeric-input>\n }\n\n <!-- Date Picker -->\n @if (QuestionType === 'date') {\n <mj-datepicker\n [ngModel]=\"$any(Value)\"\n [ngModelOptions]=\"{standalone: true}\"\n [Disabled]=\"Disabled\"\n (ValueChange)=\"OnValueChange($any($event))\"\n class=\"question-input\"\n ></mj-datepicker>\n }\n\n <!-- DateTime Picker -->\n @if (QuestionType === 'datetime') {\n <input\n type=\"datetime-local\"\n class=\"mj-input question-input\"\n [value]=\"$any(Value)\"\n [disabled]=\"Disabled\"\n (change)=\"OnValueChange($any($event.target).value)\"\n />\n }\n\n <!-- Button Group -->\n @if (QuestionType === 'buttongroup') {\n <div class=\"mj-buttongroup question-buttongroup\">\n @for (option of Options; track TrackByValue($index, option)) {\n <button\n mjButton\n [toggleable]=\"true\"\n [selected]=\"Value === option.value\"\n [disabled]=\"Disabled\"\n (click)=\"OnValueChange(option.value)\"\n class=\"buttongroup-option\"\n >\n @if (option.icon) {\n <i class=\"fa {{ option.icon }}\" aria-hidden=\"true\"></i>\n }\n {{ option.label }}\n </button>\n }\n </div>\n }\n\n <!-- Radio Buttons -->\n @if (QuestionType === 'radio') {\n <div class=\"question-radio-group\">\n @for (option of Options; track TrackByValue($index, option)) {\n <div class=\"radio-option\">\n <input\n type=\"radio\"\n class=\"mj-radio\"\n [name]=\"Question.id\"\n [value]=\"option.value\"\n [checked]=\"Value === option.value\"\n [disabled]=\"Disabled\"\n (change)=\"OnValueChange(option.value)\"\n [id]=\"Question.id + '_' + option.value\"\n />\n <label [for]=\"Question.id + '_' + option.value\" class=\"radio-label\">\n @if (option.icon) {\n <i class=\"fa {{ option.icon }}\" aria-hidden=\"true\"></i>\n }\n {{ option.label }}\n </label>\n </div>\n }\n </div>\n }\n\n <!-- Dropdown -->\n @if (QuestionType === 'dropdown') {\n <select\n [formControl]=\"Control\"\n [disabled]=\"Disabled\"\n class=\"question-dropdown\"\n >\n <option value=\"\">Select an option...</option>\n @for (option of Options; track TrackByValue($index, option)) {\n <option [value]=\"option.value\">\n {{ option.label }}\n </option>\n }\n </select>\n }\n\n <!-- Slider -->\n @if (QuestionType === 'slider') {\n <div class=\"question-slider-container\">\n <input\n type=\"range\"\n class=\"mj-slider question-slider\"\n [min]=\"GetSliderConfig().min\"\n [max]=\"GetSliderConfig().max\"\n [step]=\"GetSliderConfig().step || 1\"\n [value]=\"$any(Value) || GetSliderConfig().min\"\n [disabled]=\"Disabled\"\n (input)=\"OnValueChange(+$any($event.target).value)\"\n />\n <div class=\"slider-value\">\n {{ Value || GetSliderConfig().min }}{{ GetSliderConfig().suffix || '' }}\n </div>\n </div>\n }\n\n <!-- Date Range -->\n @if (QuestionType === 'daterange') {\n <div class=\"question-daterange\">\n <mj-datepicker\n [ngModel]=\"$any(Value)?.start\"\n [ngModelOptions]=\"{standalone: true}\"\n [Disabled]=\"Disabled\"\n (ValueChange)=\"OnDateRangeStartChange($any($event))\"\n Placeholder=\"Start Date\"\n class=\"daterange-start\"\n ></mj-datepicker>\n <span class=\"daterange-separator\">to</span>\n <mj-datepicker\n [ngModel]=\"$any(Value)?.end\"\n [ngModelOptions]=\"{standalone: true}\"\n [Disabled]=\"Disabled\"\n (ValueChange)=\"OnDateRangeEndChange($any($event))\"\n Placeholder=\"End Date\"\n class=\"daterange-end\"\n ></mj-datepicker>\n </div>\n }\n\n <!-- Time Picker -->\n @if (QuestionType === 'time') {\n <input\n type=\"time\"\n class=\"mj-input question-input\"\n [value]=\"$any(Value)\"\n [disabled]=\"Disabled\"\n (change)=\"OnValueChange($any($event.target).value)\"\n />\n }\n\n <!-- Checkboxes -->\n @if (QuestionType === 'checkbox') {\n <div class=\"question-checkbox-group\">\n @for (option of Options; track TrackByValue($index, option)) {\n <div class=\"checkbox-option\">\n <input\n type=\"checkbox\"\n class=\"mj-checkbox\"\n [checked]=\"IsChecked(option)\"\n [disabled]=\"Disabled\"\n (change)=\"ToggleCheckbox(option)\"\n [id]=\"Question.id + '_' + option.value\"\n />\n <label [for]=\"Question.id + '_' + option.value\" class=\"checkbox-label\">\n @if (option.icon) {\n <i class=\"fa {{ option.icon }}\" aria-hidden=\"true\"></i>\n }\n {{ option.label }}\n </label>\n </div>\n }\n </div>\n }\n </div><!-- end input-container -->\n\n <!-- Validation Error -->\n @if (Control.invalid && Control.touched) {\n <div class=\"validation-error\">\n @if (Control.errors?.['required']) {\n <span>This field is required</span>\n }\n @if (Control.errors?.['email']) {\n <span>Please enter a valid email address</span>\n }\n @if (Control.errors?.['min']) {\n <span>Value must be at least {{ Min }}</span>\n }\n @if (Control.errors?.['max']) {\n <span>Value must be at most {{ Max }}</span>\n }\n </div>\n }\n</div>\n"]}
|
|
@@ -5,9 +5,10 @@ import * as i3 from "./dynamic-form-response/dynamic-form-response.component";
|
|
|
5
5
|
import * as i4 from "@angular/common";
|
|
6
6
|
import * as i5 from "@angular/forms";
|
|
7
7
|
import * as i6 from "@memberjunction/ng-ui-components";
|
|
8
|
+
import * as i7 from "@memberjunction/ng-markdown";
|
|
8
9
|
export declare class DynamicFormsModule {
|
|
9
10
|
static ɵfac: i0.ɵɵFactoryDeclaration<DynamicFormsModule, never>;
|
|
10
|
-
static ɵmod: i0.ɵɵNgModuleDeclaration<DynamicFormsModule, [typeof i1.DynamicFormComponent, typeof i2.DynamicFormFieldComponent, typeof i3.DynamicFormResponseComponent], [typeof i4.CommonModule, typeof i5.FormsModule, typeof i5.ReactiveFormsModule, typeof i6.MJButtonDirective, typeof i6.MJNumericInputComponent, typeof i6.MJDatepickerComponent], [typeof i1.DynamicFormComponent, typeof i2.DynamicFormFieldComponent, typeof i3.DynamicFormResponseComponent]>;
|
|
11
|
+
static ɵmod: i0.ɵɵNgModuleDeclaration<DynamicFormsModule, [typeof i1.DynamicFormComponent, typeof i2.DynamicFormFieldComponent, typeof i3.DynamicFormResponseComponent], [typeof i4.CommonModule, typeof i5.FormsModule, typeof i5.ReactiveFormsModule, typeof i6.MJButtonDirective, typeof i6.MJNumericInputComponent, typeof i6.MJDatepickerComponent, typeof i7.MarkdownModule], [typeof i1.DynamicFormComponent, typeof i2.DynamicFormFieldComponent, typeof i3.DynamicFormResponseComponent]>;
|
|
11
12
|
static ɵinj: i0.ɵɵInjectorDeclaration<DynamicFormsModule>;
|
|
12
13
|
}
|
|
13
14
|
//# sourceMappingURL=forms.module.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"forms.module.d.ts","sourceRoot":"","sources":["../../src/lib/forms.module.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"forms.module.d.ts","sourceRoot":"","sources":["../../src/lib/forms.module.ts"],"names":[],"mappings":";;;;;;;;AAwDA,qBAqBa,kBAAkB;yCAAlB,kBAAkB;0CAAlB,kBAAkB;0CAAlB,kBAAkB;CAAI"}
|
package/dist/lib/forms.module.js
CHANGED
|
@@ -45,6 +45,7 @@ import { CommonModule } from '@angular/common';
|
|
|
45
45
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
|
46
46
|
// MJ UI components
|
|
47
47
|
import { MJButtonDirective, MJNumericInputComponent, MJDatepickerComponent } from '@memberjunction/ng-ui-components';
|
|
48
|
+
import { MarkdownModule } from '@memberjunction/ng-markdown';
|
|
48
49
|
// Components
|
|
49
50
|
import { DynamicFormComponent } from './dynamic-form/dynamic-form.component';
|
|
50
51
|
import { DynamicFormFieldComponent } from './dynamic-form-field/dynamic-form-field.component';
|
|
@@ -59,14 +60,16 @@ export class DynamicFormsModule {
|
|
|
59
60
|
ReactiveFormsModule,
|
|
60
61
|
MJButtonDirective,
|
|
61
62
|
MJNumericInputComponent,
|
|
62
|
-
MJDatepickerComponent
|
|
63
|
+
MJDatepickerComponent,
|
|
64
|
+
MarkdownModule], exports: [DynamicFormComponent,
|
|
63
65
|
DynamicFormFieldComponent,
|
|
64
66
|
DynamicFormResponseComponent] });
|
|
65
67
|
static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: DynamicFormsModule, imports: [CommonModule,
|
|
66
68
|
FormsModule,
|
|
67
69
|
ReactiveFormsModule,
|
|
68
70
|
MJNumericInputComponent,
|
|
69
|
-
MJDatepickerComponent
|
|
71
|
+
MJDatepickerComponent,
|
|
72
|
+
MarkdownModule] });
|
|
70
73
|
}
|
|
71
74
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: DynamicFormsModule, decorators: [{
|
|
72
75
|
type: NgModule,
|
|
@@ -82,7 +85,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
|
|
|
82
85
|
ReactiveFormsModule,
|
|
83
86
|
MJButtonDirective,
|
|
84
87
|
MJNumericInputComponent,
|
|
85
|
-
MJDatepickerComponent
|
|
88
|
+
MJDatepickerComponent,
|
|
89
|
+
MarkdownModule
|
|
86
90
|
],
|
|
87
91
|
exports: [
|
|
88
92
|
DynamicFormComponent,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"forms.module.js","sourceRoot":"","sources":["../../src/lib/forms.module.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyCG;AAEH,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AACzC,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAC/C,OAAO,EAAE,WAAW,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAElE,mBAAmB;AACnB,OAAO,EAAE,iBAAiB,EAAE,uBAAuB,EAAE,qBAAqB,EAAE,MAAM,kCAAkC,CAAC;
|
|
1
|
+
{"version":3,"file":"forms.module.js","sourceRoot":"","sources":["../../src/lib/forms.module.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyCG;AAEH,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AACzC,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAC/C,OAAO,EAAE,WAAW,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAElE,mBAAmB;AACnB,OAAO,EAAE,iBAAiB,EAAE,uBAAuB,EAAE,qBAAqB,EAAE,MAAM,kCAAkC,CAAC;AACrH,OAAO,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAE7D,aAAa;AACb,OAAO,EAAE,oBAAoB,EAAE,MAAM,uCAAuC,CAAC;AAC7E,OAAO,EAAE,yBAAyB,EAAE,MAAM,mDAAmD,CAAC;AAC9F,OAAO,EAAE,4BAA4B,EAAE,MAAM,yDAAyD,CAAC;;AAuBvG,MAAM,OAAO,kBAAkB;uGAAlB,kBAAkB;wGAAlB,kBAAkB,iBAnBvB,oBAAoB;YACpB,yBAAyB;YACzB,4BAA4B,aAG5B,YAAY;YACZ,WAAW;YACX,mBAAmB;YACnB,iBAAiB;YACjB,uBAAuB;YACvB,qBAAqB;YACrB,cAAc,aAGd,oBAAoB;YACpB,yBAAyB;YACzB,4BAA4B;wGAGvB,kBAAkB,YAdvB,YAAY;YACZ,WAAW;YACX,mBAAmB;YAEnB,uBAAuB;YACvB,qBAAqB;YACrB,cAAc;;2FAQT,kBAAkB;kBArB9B,QAAQ;mBAAC;oBACN,YAAY,EAAE;wBACV,oBAAoB;wBACpB,yBAAyB;wBACzB,4BAA4B;qBAC/B;oBACD,OAAO,EAAE;wBACL,YAAY;wBACZ,WAAW;wBACX,mBAAmB;wBACnB,iBAAiB;wBACjB,uBAAuB;wBACvB,qBAAqB;wBACrB,cAAc;qBACjB;oBACD,OAAO,EAAE;wBACL,oBAAoB;wBACpB,yBAAyB;wBACzB,4BAA4B;qBAC/B;iBACJ","sourcesContent":["/**\n * @fileoverview MJ Dynamic Forms Module\n *\n * Provides reusable dynamic form components that render structured forms from\n * `AgentResponseForm` JSON schemas. Supports 14+ input types with built-in\n * validation, responsive layout, and intelligent field sizing.\n *\n * ## Usage\n *\n * Import `DynamicFormsModule` into your feature module:\n *\n * ```typescript\n * import { DynamicFormsModule } from '@memberjunction/ng-forms';\n *\n * @NgModule({\n * imports: [DynamicFormsModule]\n * })\n * export class MyFeatureModule { }\n * ```\n *\n * Then use in templates:\n *\n * ```html\n * <mj-dynamic-form\n * [FormDefinition]=\"myFormSchema\"\n * [Disabled]=\"isSaving\"\n * (FormSubmitted)=\"onSubmit($event)\">\n * </mj-dynamic-form>\n * ```\n *\n * Or use the field component individually within your own reactive forms:\n *\n * ```html\n * <mj-dynamic-form-field\n * [Question]=\"question\"\n * [Control]=\"myFormGroup.get(question.id)\"\n * [formControlName]=\"question.id\">\n * </mj-dynamic-form-field>\n * ```\n *\n * @module @memberjunction/ng-forms\n */\n\nimport { NgModule } from '@angular/core';\nimport { CommonModule } from '@angular/common';\nimport { FormsModule, ReactiveFormsModule } from '@angular/forms';\n\n// MJ UI components\nimport { MJButtonDirective, MJNumericInputComponent, MJDatepickerComponent } from '@memberjunction/ng-ui-components';\nimport { MarkdownModule } from '@memberjunction/ng-markdown';\n\n// Components\nimport { DynamicFormComponent } from './dynamic-form/dynamic-form.component';\nimport { DynamicFormFieldComponent } from './dynamic-form-field/dynamic-form-field.component';\nimport { DynamicFormResponseComponent } from './dynamic-form-response/dynamic-form-response.component';\n\n@NgModule({\n declarations: [\n DynamicFormComponent,\n DynamicFormFieldComponent,\n DynamicFormResponseComponent\n ],\n imports: [\n CommonModule,\n FormsModule,\n ReactiveFormsModule,\n MJButtonDirective,\n MJNumericInputComponent,\n MJDatepickerComponent,\n MarkdownModule\n ],\n exports: [\n DynamicFormComponent,\n DynamicFormFieldComponent,\n DynamicFormResponseComponent\n ]\n})\nexport class DynamicFormsModule { }\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@memberjunction/ng-forms",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.44.0",
|
|
4
4
|
"description": "MemberJunction: Generic dynamic form rendering from JSON schemas. Renders structured forms with 14+ field types (text, textarea, number, currency, date, daterange, time, checkbox, radio, dropdown, buttongroup, slider, email) with built-in validation, responsive layout, and intelligent field sizing.",
|
|
5
5
|
"main": "./dist/public-api.js",
|
|
6
6
|
"typings": "./dist/public-api.d.ts",
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
"devDependencies": {
|
|
26
26
|
"@angular/compiler": "21.1.3",
|
|
27
27
|
"@angular/compiler-cli": "21.1.3",
|
|
28
|
+
"@memberjunction/ng-test-utils": "5.44.0",
|
|
28
29
|
"vitest": "^4.0.18"
|
|
29
30
|
},
|
|
30
31
|
"peerDependencies": {
|
|
@@ -33,8 +34,9 @@
|
|
|
33
34
|
"@angular/forms": "21.1.3"
|
|
34
35
|
},
|
|
35
36
|
"dependencies": {
|
|
36
|
-
"@memberjunction/ai-core-plus": "5.
|
|
37
|
-
"@memberjunction/ng-
|
|
37
|
+
"@memberjunction/ai-core-plus": "5.44.0",
|
|
38
|
+
"@memberjunction/ng-markdown": "5.44.0",
|
|
39
|
+
"@memberjunction/ng-ui-components": "5.44.0",
|
|
38
40
|
"tslib": "^2.8.1"
|
|
39
41
|
},
|
|
40
42
|
"sideEffects": false,
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"form-response-utils.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/form-response-utils.test.ts"],"names":[],"mappings":""}
|
|
@@ -1,249 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { FormResponseUtils } from '../lib/dynamic-form-response/form-response-utils';
|
|
3
|
-
/** Helper to build a choice-type question (buttongroup / radio / dropdown / checkbox). */
|
|
4
|
-
function choiceQuestion(choiceType, options) {
|
|
5
|
-
return { id: 'q1', label: 'Question 1', type: { type: choiceType, options } };
|
|
6
|
-
}
|
|
7
|
-
/** Helper to build a simple-type question (text, date, etc.). */
|
|
8
|
-
function simpleQuestion(type) {
|
|
9
|
-
return { id: 'q1', label: 'Q', type };
|
|
10
|
-
}
|
|
11
|
-
// We cast through `unknown` so TypeScript accepts our minimal mock shapes.
|
|
12
|
-
const asQ = (q) => q;
|
|
13
|
-
describe('FormResponseUtils', () => {
|
|
14
|
-
// ------------------------------------------------------------------ FormatValue
|
|
15
|
-
describe('FormatValue', () => {
|
|
16
|
-
it('returns empty string for null value', () => {
|
|
17
|
-
expect(FormResponseUtils.FormatValue(null, null, 'text')).toBe('');
|
|
18
|
-
});
|
|
19
|
-
it('returns empty string for undefined value', () => {
|
|
20
|
-
expect(FormResponseUtils.FormatValue(undefined, null, 'text')).toBe('');
|
|
21
|
-
});
|
|
22
|
-
it('returns stringified value for plain text type', () => {
|
|
23
|
-
expect(FormResponseUtils.FormatValue('hello', null, 'text')).toBe('hello');
|
|
24
|
-
});
|
|
25
|
-
it('returns stringified number for unknown type', () => {
|
|
26
|
-
expect(FormResponseUtils.FormatValue(42, null, null)).toBe('42');
|
|
27
|
-
});
|
|
28
|
-
it('formats date type without time', () => {
|
|
29
|
-
// Use an explicit UTC datetime to avoid local-timezone date shifting
|
|
30
|
-
const result = FormResponseUtils.FormatValue('2025-06-15T12:00:00Z', null, 'date');
|
|
31
|
-
expect(result).toContain('Jun');
|
|
32
|
-
expect(result).toContain('2025');
|
|
33
|
-
});
|
|
34
|
-
it('formats datetime type with time', () => {
|
|
35
|
-
const result = FormResponseUtils.FormatValue('2025-06-15T14:30:00Z', null, 'datetime');
|
|
36
|
-
expect(result).toContain('Jun');
|
|
37
|
-
expect(result).toContain('15');
|
|
38
|
-
expect(result).toContain('2025');
|
|
39
|
-
// Should include time portion
|
|
40
|
-
expect(result).toMatch(/\d{1,2}:\d{2}/);
|
|
41
|
-
});
|
|
42
|
-
it('formats time type', () => {
|
|
43
|
-
const result = FormResponseUtils.FormatValue('14:30', null, 'time');
|
|
44
|
-
expect(result).toMatch(/2:30\s*PM/i);
|
|
45
|
-
});
|
|
46
|
-
it('formats daterange type', () => {
|
|
47
|
-
// Use mid-month dates to avoid timezone boundary shifts
|
|
48
|
-
const result = FormResponseUtils.FormatValue({ start: '2025-01-15', end: '2025-12-15' }, null, 'daterange');
|
|
49
|
-
expect(result).toContain('Jan');
|
|
50
|
-
expect(result).toContain('Dec');
|
|
51
|
-
expect(result).toContain('to');
|
|
52
|
-
});
|
|
53
|
-
it('resolves option label for buttongroup choice type', () => {
|
|
54
|
-
const q = choiceQuestion('buttongroup', [
|
|
55
|
-
{ value: 'a', label: 'Alpha' },
|
|
56
|
-
{ value: 'b', label: 'Beta' },
|
|
57
|
-
]);
|
|
58
|
-
expect(FormResponseUtils.FormatValue('b', asQ(q), 'buttongroup')).toBe('Beta');
|
|
59
|
-
});
|
|
60
|
-
it('resolves option label for dropdown choice type', () => {
|
|
61
|
-
const q = choiceQuestion('dropdown', [
|
|
62
|
-
{ value: 1, label: 'One' },
|
|
63
|
-
{ value: 2, label: 'Two' },
|
|
64
|
-
]);
|
|
65
|
-
// value comparison is done via String(), so numeric "1" matches
|
|
66
|
-
expect(FormResponseUtils.FormatValue(1, asQ(q), 'dropdown')).toBe('One');
|
|
67
|
-
});
|
|
68
|
-
it('falls back to stringified value when no matching option', () => {
|
|
69
|
-
const q = choiceQuestion('radio', [{ value: 'a', label: 'Alpha' }]);
|
|
70
|
-
expect(FormResponseUtils.FormatValue('z', asQ(q), 'radio')).toBe('z');
|
|
71
|
-
});
|
|
72
|
-
it('auto-detects ISO datetime strings for default type', () => {
|
|
73
|
-
const result = FormResponseUtils.FormatValue('2025-03-10T15:45:00Z', null, null);
|
|
74
|
-
expect(result).toContain('Mar');
|
|
75
|
-
expect(result).toContain('10');
|
|
76
|
-
expect(result).toContain('2025');
|
|
77
|
-
});
|
|
78
|
-
it('auto-detects ISO midnight as date-only', () => {
|
|
79
|
-
const result = FormResponseUtils.FormatValue('2025-03-15T00:00:00Z', null, null);
|
|
80
|
-
// Midnight UTC should be treated as date-only (no time component)
|
|
81
|
-
expect(result).toContain('Mar');
|
|
82
|
-
expect(result).toContain('2025');
|
|
83
|
-
});
|
|
84
|
-
});
|
|
85
|
-
// -------------------------------------------------------------- ResolveOptionLabel
|
|
86
|
-
describe('ResolveOptionLabel', () => {
|
|
87
|
-
it('returns matching label', () => {
|
|
88
|
-
const q = choiceQuestion('radio', [
|
|
89
|
-
{ value: 'x', label: 'X Label' },
|
|
90
|
-
{ value: 'y', label: 'Y Label' },
|
|
91
|
-
]);
|
|
92
|
-
expect(FormResponseUtils.ResolveOptionLabel(asQ(q), 'y')).toBe('Y Label');
|
|
93
|
-
});
|
|
94
|
-
it('returns null when no match found', () => {
|
|
95
|
-
const q = choiceQuestion('radio', [{ value: 'x', label: 'X' }]);
|
|
96
|
-
expect(FormResponseUtils.ResolveOptionLabel(asQ(q), 'missing')).toBeNull();
|
|
97
|
-
});
|
|
98
|
-
it('returns null when question type is a simple string (no options)', () => {
|
|
99
|
-
const q = simpleQuestion('text');
|
|
100
|
-
expect(FormResponseUtils.ResolveOptionLabel(asQ(q), 'anything')).toBeNull();
|
|
101
|
-
});
|
|
102
|
-
it('matches numeric value via string coercion', () => {
|
|
103
|
-
const q = choiceQuestion('dropdown', [{ value: 42, label: 'Forty Two' }]);
|
|
104
|
-
expect(FormResponseUtils.ResolveOptionLabel(asQ(q), 42)).toBe('Forty Two');
|
|
105
|
-
});
|
|
106
|
-
it('matches boolean value via string coercion', () => {
|
|
107
|
-
const q = choiceQuestion('checkbox', [{ value: true, label: 'Yes' }]);
|
|
108
|
-
expect(FormResponseUtils.ResolveOptionLabel(asQ(q), true)).toBe('Yes');
|
|
109
|
-
});
|
|
110
|
-
});
|
|
111
|
-
// ------------------------------------------------------------------- FormatDate
|
|
112
|
-
describe('FormatDate', () => {
|
|
113
|
-
it('formats a valid date without time', () => {
|
|
114
|
-
const result = FormResponseUtils.FormatDate('2025-12-25', false);
|
|
115
|
-
expect(result).toContain('Dec');
|
|
116
|
-
expect(result).toContain('25');
|
|
117
|
-
expect(result).toContain('2025');
|
|
118
|
-
});
|
|
119
|
-
it('formats a valid date with time', () => {
|
|
120
|
-
const result = FormResponseUtils.FormatDate('2025-12-25T08:30:00Z', true);
|
|
121
|
-
expect(result).toContain('Dec');
|
|
122
|
-
expect(result).toContain('25');
|
|
123
|
-
expect(result).toMatch(/\d{1,2}:\d{2}/);
|
|
124
|
-
});
|
|
125
|
-
it('returns original string for invalid date', () => {
|
|
126
|
-
expect(FormResponseUtils.FormatDate('not-a-date', false)).toBe('not-a-date');
|
|
127
|
-
});
|
|
128
|
-
it('returns original string for empty string', () => {
|
|
129
|
-
expect(FormResponseUtils.FormatDate('', false)).toBe('');
|
|
130
|
-
});
|
|
131
|
-
});
|
|
132
|
-
// ------------------------------------------------------------------- FormatTime
|
|
133
|
-
describe('FormatTime', () => {
|
|
134
|
-
it('formats 24h time to 12h', () => {
|
|
135
|
-
const result = FormResponseUtils.FormatTime('14:30');
|
|
136
|
-
expect(result).toMatch(/2:30\s*PM/i);
|
|
137
|
-
});
|
|
138
|
-
it('formats midnight', () => {
|
|
139
|
-
const result = FormResponseUtils.FormatTime('00:00');
|
|
140
|
-
expect(result).toMatch(/12:00\s*AM/i);
|
|
141
|
-
});
|
|
142
|
-
it('formats noon', () => {
|
|
143
|
-
const result = FormResponseUtils.FormatTime('12:00');
|
|
144
|
-
expect(result).toMatch(/12:00\s*PM/i);
|
|
145
|
-
});
|
|
146
|
-
it('returns locale string for malformed time (NaN hours produces Invalid Date)', () => {
|
|
147
|
-
// 'bad'.split(':').map(Number) yields [NaN, NaN], which makes an invalid Date.
|
|
148
|
-
// toLocaleTimeString on an invalid Date returns 'Invalid Date' in Node.
|
|
149
|
-
const result = FormResponseUtils.FormatTime('bad');
|
|
150
|
-
expect(result).toBe('Invalid Date');
|
|
151
|
-
});
|
|
152
|
-
});
|
|
153
|
-
// --------------------------------------------------------------- FormatDateRange
|
|
154
|
-
describe('FormatDateRange', () => {
|
|
155
|
-
it('formats start and end dates', () => {
|
|
156
|
-
// Use mid-month dates to avoid timezone boundary shifts
|
|
157
|
-
const result = FormResponseUtils.FormatDateRange({ start: '2025-01-15', end: '2025-06-15' });
|
|
158
|
-
expect(result).toContain('Jan');
|
|
159
|
-
expect(result).toContain('Jun');
|
|
160
|
-
expect(result).toContain('to');
|
|
161
|
-
});
|
|
162
|
-
it('returns JSON string for non-range values', () => {
|
|
163
|
-
expect(FormResponseUtils.FormatDateRange('some string')).toBe('"some string"');
|
|
164
|
-
});
|
|
165
|
-
it('returns JSON string for null', () => {
|
|
166
|
-
expect(FormResponseUtils.FormatDateRange(null)).toBe('null');
|
|
167
|
-
});
|
|
168
|
-
it('returns JSON string for object without start/end', () => {
|
|
169
|
-
const result = FormResponseUtils.FormatDateRange({ foo: 'bar' });
|
|
170
|
-
expect(result).toContain('foo');
|
|
171
|
-
});
|
|
172
|
-
});
|
|
173
|
-
// ----------------------------------------------------------------- HumanizeKey
|
|
174
|
-
describe('HumanizeKey', () => {
|
|
175
|
-
it('converts camelCase to title case', () => {
|
|
176
|
-
expect(FormResponseUtils.HumanizeKey('firstName')).toBe('First Name');
|
|
177
|
-
});
|
|
178
|
-
it('converts snake_case to title case', () => {
|
|
179
|
-
expect(FormResponseUtils.HumanizeKey('first_name')).toBe('First Name');
|
|
180
|
-
});
|
|
181
|
-
it('converts PascalCase to title case', () => {
|
|
182
|
-
expect(FormResponseUtils.HumanizeKey('FirstName')).toBe('First Name');
|
|
183
|
-
});
|
|
184
|
-
it('handles kebab-case', () => {
|
|
185
|
-
expect(FormResponseUtils.HumanizeKey('first-name')).toBe('First Name');
|
|
186
|
-
});
|
|
187
|
-
it('handles single word', () => {
|
|
188
|
-
expect(FormResponseUtils.HumanizeKey('name')).toBe('Name');
|
|
189
|
-
});
|
|
190
|
-
it('handles all-uppercase abbreviation (treated as individual letters)', () => {
|
|
191
|
-
// Each uppercase letter gets a space before it
|
|
192
|
-
const result = FormResponseUtils.HumanizeKey('XMLParser');
|
|
193
|
-
// "XMLParser" -> " X M L Parser" -> trimmed -> "X M L Parser" -> title-cased
|
|
194
|
-
expect(result).toBe('X M L Parser');
|
|
195
|
-
});
|
|
196
|
-
it('handles empty string', () => {
|
|
197
|
-
expect(FormResponseUtils.HumanizeKey('')).toBe('');
|
|
198
|
-
});
|
|
199
|
-
});
|
|
200
|
-
// ------------------------------------------------------------------ EscapeHtml
|
|
201
|
-
describe('EscapeHtml', () => {
|
|
202
|
-
it('escapes ampersands', () => {
|
|
203
|
-
expect(FormResponseUtils.EscapeHtml('a&b')).toBe('a&b');
|
|
204
|
-
});
|
|
205
|
-
it('escapes less-than', () => {
|
|
206
|
-
expect(FormResponseUtils.EscapeHtml('<script>')).toBe('<script>');
|
|
207
|
-
});
|
|
208
|
-
it('escapes greater-than', () => {
|
|
209
|
-
expect(FormResponseUtils.EscapeHtml('a>b')).toBe('a>b');
|
|
210
|
-
});
|
|
211
|
-
it('escapes double quotes', () => {
|
|
212
|
-
expect(FormResponseUtils.EscapeHtml('"hello"')).toBe('"hello"');
|
|
213
|
-
});
|
|
214
|
-
it('escapes single quotes', () => {
|
|
215
|
-
expect(FormResponseUtils.EscapeHtml("it's")).toBe("it's");
|
|
216
|
-
});
|
|
217
|
-
it('handles all special chars together', () => {
|
|
218
|
-
expect(FormResponseUtils.EscapeHtml('<a href="x">&\'</a>')).toBe('<a href="x">&'</a>');
|
|
219
|
-
});
|
|
220
|
-
it('leaves safe strings unchanged', () => {
|
|
221
|
-
expect(FormResponseUtils.EscapeHtml('Hello World 123')).toBe('Hello World 123');
|
|
222
|
-
});
|
|
223
|
-
it('handles empty string', () => {
|
|
224
|
-
expect(FormResponseUtils.EscapeHtml('')).toBe('');
|
|
225
|
-
});
|
|
226
|
-
});
|
|
227
|
-
// --------------------------------------------------------------- FormatValueHtml
|
|
228
|
-
describe('FormatValueHtml', () => {
|
|
229
|
-
it('formats and escapes HTML', () => {
|
|
230
|
-
const result = FormResponseUtils.FormatValueHtml('<b>bold</b>', null, 'text');
|
|
231
|
-
expect(result).toBe('<b>bold</b>');
|
|
232
|
-
});
|
|
233
|
-
it('returns empty string for null', () => {
|
|
234
|
-
expect(FormResponseUtils.FormatValueHtml(null, null, 'text')).toBe('');
|
|
235
|
-
});
|
|
236
|
-
});
|
|
237
|
-
// --------------------------------------------------------- GetQuestionTypeString
|
|
238
|
-
describe('GetQuestionTypeString', () => {
|
|
239
|
-
it('returns string type directly', () => {
|
|
240
|
-
const q = simpleQuestion('text');
|
|
241
|
-
expect(FormResponseUtils.GetQuestionTypeString(asQ(q))).toBe('text');
|
|
242
|
-
});
|
|
243
|
-
it('returns type property from object type', () => {
|
|
244
|
-
const q = choiceQuestion('dropdown', []);
|
|
245
|
-
expect(FormResponseUtils.GetQuestionTypeString(asQ(q))).toBe('dropdown');
|
|
246
|
-
});
|
|
247
|
-
});
|
|
248
|
-
});
|
|
249
|
-
//# sourceMappingURL=form-response-utils.test.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"form-response-utils.test.js","sourceRoot":"","sources":["../../src/__tests__/form-response-utils.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { describe, it, expect } from 'vitest';\nimport { FormResponseUtils } from '../lib/dynamic-form-response/form-response-utils';\n\n// Minimal types matching the real FormQuestion / FormOption interfaces from @memberjunction/ai-core-plus.\n// We only need the subset that FormResponseUtils actually reads.\ninterface MockFormOption {\n value: string | number | boolean;\n label: string;\n}\n\ninterface MockFormQuestion {\n id: string;\n label: string;\n type: string | { type: string; options: MockFormOption[] };\n required?: boolean;\n}\n\n/** Helper to build a choice-type question (buttongroup / radio / dropdown / checkbox). */\nfunction choiceQuestion(\n choiceType: 'buttongroup' | 'radio' | 'dropdown' | 'checkbox',\n options: MockFormOption[],\n): MockFormQuestion {\n return { id: 'q1', label: 'Question 1', type: { type: choiceType, options } };\n}\n\n/** Helper to build a simple-type question (text, date, etc.). */\nfunction simpleQuestion(type: string): MockFormQuestion {\n return { id: 'q1', label: 'Q', type };\n}\n\n// We cast through `unknown` so TypeScript accepts our minimal mock shapes.\nconst asQ = (q: MockFormQuestion) => q as unknown as Parameters<typeof FormResponseUtils.FormatValue>[1];\n\ndescribe('FormResponseUtils', () => {\n // ------------------------------------------------------------------ FormatValue\n describe('FormatValue', () => {\n it('returns empty string for null value', () => {\n expect(FormResponseUtils.FormatValue(null, null, 'text')).toBe('');\n });\n\n it('returns empty string for undefined value', () => {\n expect(FormResponseUtils.FormatValue(undefined, null, 'text')).toBe('');\n });\n\n it('returns stringified value for plain text type', () => {\n expect(FormResponseUtils.FormatValue('hello', null, 'text')).toBe('hello');\n });\n\n it('returns stringified number for unknown type', () => {\n expect(FormResponseUtils.FormatValue(42, null, null)).toBe('42');\n });\n\n it('formats date type without time', () => {\n // Use an explicit UTC datetime to avoid local-timezone date shifting\n const result = FormResponseUtils.FormatValue('2025-06-15T12:00:00Z', null, 'date');\n expect(result).toContain('Jun');\n expect(result).toContain('2025');\n });\n\n it('formats datetime type with time', () => {\n const result = FormResponseUtils.FormatValue('2025-06-15T14:30:00Z', null, 'datetime');\n expect(result).toContain('Jun');\n expect(result).toContain('15');\n expect(result).toContain('2025');\n // Should include time portion\n expect(result).toMatch(/\\d{1,2}:\\d{2}/);\n });\n\n it('formats time type', () => {\n const result = FormResponseUtils.FormatValue('14:30', null, 'time');\n expect(result).toMatch(/2:30\\s*PM/i);\n });\n\n it('formats daterange type', () => {\n // Use mid-month dates to avoid timezone boundary shifts\n const result = FormResponseUtils.FormatValue(\n { start: '2025-01-15', end: '2025-12-15' },\n null,\n 'daterange',\n );\n expect(result).toContain('Jan');\n expect(result).toContain('Dec');\n expect(result).toContain('to');\n });\n\n it('resolves option label for buttongroup choice type', () => {\n const q = choiceQuestion('buttongroup', [\n { value: 'a', label: 'Alpha' },\n { value: 'b', label: 'Beta' },\n ]);\n expect(FormResponseUtils.FormatValue('b', asQ(q), 'buttongroup')).toBe('Beta');\n });\n\n it('resolves option label for dropdown choice type', () => {\n const q = choiceQuestion('dropdown', [\n { value: 1, label: 'One' },\n { value: 2, label: 'Two' },\n ]);\n // value comparison is done via String(), so numeric \"1\" matches\n expect(FormResponseUtils.FormatValue(1, asQ(q), 'dropdown')).toBe('One');\n });\n\n it('falls back to stringified value when no matching option', () => {\n const q = choiceQuestion('radio', [{ value: 'a', label: 'Alpha' }]);\n expect(FormResponseUtils.FormatValue('z', asQ(q), 'radio')).toBe('z');\n });\n\n it('auto-detects ISO datetime strings for default type', () => {\n const result = FormResponseUtils.FormatValue('2025-03-10T15:45:00Z', null, null);\n expect(result).toContain('Mar');\n expect(result).toContain('10');\n expect(result).toContain('2025');\n });\n\n it('auto-detects ISO midnight as date-only', () => {\n const result = FormResponseUtils.FormatValue('2025-03-15T00:00:00Z', null, null);\n // Midnight UTC should be treated as date-only (no time component)\n expect(result).toContain('Mar');\n expect(result).toContain('2025');\n });\n });\n\n // -------------------------------------------------------------- ResolveOptionLabel\n describe('ResolveOptionLabel', () => {\n it('returns matching label', () => {\n const q = choiceQuestion('radio', [\n { value: 'x', label: 'X Label' },\n { value: 'y', label: 'Y Label' },\n ]);\n expect(FormResponseUtils.ResolveOptionLabel(asQ(q)!, 'y')).toBe('Y Label');\n });\n\n it('returns null when no match found', () => {\n const q = choiceQuestion('radio', [{ value: 'x', label: 'X' }]);\n expect(FormResponseUtils.ResolveOptionLabel(asQ(q)!, 'missing')).toBeNull();\n });\n\n it('returns null when question type is a simple string (no options)', () => {\n const q = simpleQuestion('text');\n expect(FormResponseUtils.ResolveOptionLabel(asQ(q)!, 'anything')).toBeNull();\n });\n\n it('matches numeric value via string coercion', () => {\n const q = choiceQuestion('dropdown', [{ value: 42, label: 'Forty Two' }]);\n expect(FormResponseUtils.ResolveOptionLabel(asQ(q)!, 42)).toBe('Forty Two');\n });\n\n it('matches boolean value via string coercion', () => {\n const q = choiceQuestion('checkbox', [{ value: true, label: 'Yes' }]);\n expect(FormResponseUtils.ResolveOptionLabel(asQ(q)!, true)).toBe('Yes');\n });\n });\n\n // ------------------------------------------------------------------- FormatDate\n describe('FormatDate', () => {\n it('formats a valid date without time', () => {\n const result = FormResponseUtils.FormatDate('2025-12-25', false);\n expect(result).toContain('Dec');\n expect(result).toContain('25');\n expect(result).toContain('2025');\n });\n\n it('formats a valid date with time', () => {\n const result = FormResponseUtils.FormatDate('2025-12-25T08:30:00Z', true);\n expect(result).toContain('Dec');\n expect(result).toContain('25');\n expect(result).toMatch(/\\d{1,2}:\\d{2}/);\n });\n\n it('returns original string for invalid date', () => {\n expect(FormResponseUtils.FormatDate('not-a-date', false)).toBe('not-a-date');\n });\n\n it('returns original string for empty string', () => {\n expect(FormResponseUtils.FormatDate('', false)).toBe('');\n });\n });\n\n // ------------------------------------------------------------------- FormatTime\n describe('FormatTime', () => {\n it('formats 24h time to 12h', () => {\n const result = FormResponseUtils.FormatTime('14:30');\n expect(result).toMatch(/2:30\\s*PM/i);\n });\n\n it('formats midnight', () => {\n const result = FormResponseUtils.FormatTime('00:00');\n expect(result).toMatch(/12:00\\s*AM/i);\n });\n\n it('formats noon', () => {\n const result = FormResponseUtils.FormatTime('12:00');\n expect(result).toMatch(/12:00\\s*PM/i);\n });\n\n it('returns locale string for malformed time (NaN hours produces Invalid Date)', () => {\n // 'bad'.split(':').map(Number) yields [NaN, NaN], which makes an invalid Date.\n // toLocaleTimeString on an invalid Date returns 'Invalid Date' in Node.\n const result = FormResponseUtils.FormatTime('bad');\n expect(result).toBe('Invalid Date');\n });\n });\n\n // --------------------------------------------------------------- FormatDateRange\n describe('FormatDateRange', () => {\n it('formats start and end dates', () => {\n // Use mid-month dates to avoid timezone boundary shifts\n const result = FormResponseUtils.FormatDateRange({ start: '2025-01-15', end: '2025-06-15' });\n expect(result).toContain('Jan');\n expect(result).toContain('Jun');\n expect(result).toContain('to');\n });\n\n it('returns JSON string for non-range values', () => {\n expect(FormResponseUtils.FormatDateRange('some string')).toBe('\"some string\"');\n });\n\n it('returns JSON string for null', () => {\n expect(FormResponseUtils.FormatDateRange(null)).toBe('null');\n });\n\n it('returns JSON string for object without start/end', () => {\n const result = FormResponseUtils.FormatDateRange({ foo: 'bar' });\n expect(result).toContain('foo');\n });\n });\n\n // ----------------------------------------------------------------- HumanizeKey\n describe('HumanizeKey', () => {\n it('converts camelCase to title case', () => {\n expect(FormResponseUtils.HumanizeKey('firstName')).toBe('First Name');\n });\n\n it('converts snake_case to title case', () => {\n expect(FormResponseUtils.HumanizeKey('first_name')).toBe('First Name');\n });\n\n it('converts PascalCase to title case', () => {\n expect(FormResponseUtils.HumanizeKey('FirstName')).toBe('First Name');\n });\n\n it('handles kebab-case', () => {\n expect(FormResponseUtils.HumanizeKey('first-name')).toBe('First Name');\n });\n\n it('handles single word', () => {\n expect(FormResponseUtils.HumanizeKey('name')).toBe('Name');\n });\n\n it('handles all-uppercase abbreviation (treated as individual letters)', () => {\n // Each uppercase letter gets a space before it\n const result = FormResponseUtils.HumanizeKey('XMLParser');\n // \"XMLParser\" -> \" X M L Parser\" -> trimmed -> \"X M L Parser\" -> title-cased\n expect(result).toBe('X M L Parser');\n });\n\n it('handles empty string', () => {\n expect(FormResponseUtils.HumanizeKey('')).toBe('');\n });\n });\n\n // ------------------------------------------------------------------ EscapeHtml\n describe('EscapeHtml', () => {\n it('escapes ampersands', () => {\n expect(FormResponseUtils.EscapeHtml('a&b')).toBe('a&b');\n });\n\n it('escapes less-than', () => {\n expect(FormResponseUtils.EscapeHtml('<script>')).toBe('<script>');\n });\n\n it('escapes greater-than', () => {\n expect(FormResponseUtils.EscapeHtml('a>b')).toBe('a>b');\n });\n\n it('escapes double quotes', () => {\n expect(FormResponseUtils.EscapeHtml('\"hello\"')).toBe('"hello"');\n });\n\n it('escapes single quotes', () => {\n expect(FormResponseUtils.EscapeHtml(\"it's\")).toBe(\"it's\");\n });\n\n it('handles all special chars together', () => {\n expect(FormResponseUtils.EscapeHtml('<a href=\"x\">&\\'</a>')).toBe(\n '<a href="x">&'</a>',\n );\n });\n\n it('leaves safe strings unchanged', () => {\n expect(FormResponseUtils.EscapeHtml('Hello World 123')).toBe('Hello World 123');\n });\n\n it('handles empty string', () => {\n expect(FormResponseUtils.EscapeHtml('')).toBe('');\n });\n });\n\n // --------------------------------------------------------------- FormatValueHtml\n describe('FormatValueHtml', () => {\n it('formats and escapes HTML', () => {\n const result = FormResponseUtils.FormatValueHtml('<b>bold</b>', null, 'text');\n expect(result).toBe('<b>bold</b>');\n });\n\n it('returns empty string for null', () => {\n expect(FormResponseUtils.FormatValueHtml(null, null, 'text')).toBe('');\n });\n });\n\n // --------------------------------------------------------- GetQuestionTypeString\n describe('GetQuestionTypeString', () => {\n it('returns string type directly', () => {\n const q = simpleQuestion('text');\n expect(FormResponseUtils.GetQuestionTypeString(asQ(q)!)).toBe('text');\n });\n\n it('returns type property from object type', () => {\n const q = choiceQuestion('dropdown', []);\n expect(FormResponseUtils.GetQuestionTypeString(asQ(q)!)).toBe('dropdown');\n });\n });\n});\n"]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/index.test.ts"],"names":[],"mappings":""}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.test.js","sourceRoot":"","sources":["../../src/__tests__/index.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAE9C,QAAQ,CAAC,0BAA0B,EAAE,GAAG,EAAE;IACxC,EAAE,CAAC,4BAA4B,EAAE,GAAG,EAAE;QACpC,MAAM,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC1B,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC","sourcesContent":["import { describe, it, expect } from 'vitest';\n\ndescribe('@memberjunction/ng-forms', () => {\n it('should have a passing test', () => {\n expect(true).toBe(true);\n });\n});\n"]}
|