@lub-crm/forms 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +298 -0
- package/dist/lub-forms.css +1 -0
- package/dist/lub-forms.es.js +5848 -0
- package/dist/lub-forms.es.js.map +1 -0
- package/dist/lub-forms.standalone.js +10 -0
- package/dist/lub-forms.standalone.js.map +1 -0
- package/dist/lub-forms.umd.js +227 -0
- package/dist/lub-forms.umd.js.map +1 -0
- package/package.json +68 -0
- package/src/api/client.ts +115 -0
- package/src/api/index.ts +2 -0
- package/src/api/types.ts +202 -0
- package/src/core/FormProvider.tsx +228 -0
- package/src/core/FormRenderer.tsx +134 -0
- package/src/core/LubForm.tsx +476 -0
- package/src/core/StepManager.tsx +199 -0
- package/src/core/index.ts +4 -0
- package/src/embed.ts +188 -0
- package/src/fields/CheckboxField.tsx +62 -0
- package/src/fields/CheckboxGroupField.tsx +57 -0
- package/src/fields/CountryField.tsx +43 -0
- package/src/fields/DateField.tsx +33 -0
- package/src/fields/DateTimeField.tsx +33 -0
- package/src/fields/DividerField.tsx +16 -0
- package/src/fields/FieldWrapper.tsx +60 -0
- package/src/fields/FileField.tsx +45 -0
- package/src/fields/HiddenField.tsx +18 -0
- package/src/fields/HtmlField.tsx +17 -0
- package/src/fields/NumberField.tsx +39 -0
- package/src/fields/RadioField.tsx +57 -0
- package/src/fields/RecaptchaField.tsx +137 -0
- package/src/fields/SelectField.tsx +49 -0
- package/src/fields/StateField.tsx +84 -0
- package/src/fields/TextField.tsx +51 -0
- package/src/fields/TextareaField.tsx +37 -0
- package/src/fields/TimeField.tsx +33 -0
- package/src/fields/index.ts +84 -0
- package/src/hooks/index.ts +4 -0
- package/src/hooks/useConditionalLogic.ts +59 -0
- package/src/hooks/useFormApi.ts +118 -0
- package/src/hooks/useFormDesign.ts +48 -0
- package/src/hooks/useMultiStep.ts +98 -0
- package/src/index.ts +101 -0
- package/src/main.tsx +40 -0
- package/src/styles/index.css +707 -0
- package/src/utils/cn.ts +6 -0
- package/src/utils/countries.ts +163 -0
- package/src/utils/css-variables.ts +63 -0
- package/src/utils/index.ts +3 -0
- package/src/validation/conditional.ts +170 -0
- package/src/validation/index.ts +2 -0
- package/src/validation/schema-builder.ts +327 -0
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
// Country data for country/state fields
|
|
2
|
+
|
|
3
|
+
export interface Country {
|
|
4
|
+
code: string;
|
|
5
|
+
name: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface State {
|
|
9
|
+
code: string;
|
|
10
|
+
name: string;
|
|
11
|
+
countryCode: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// ISO 3166-1 alpha-2 country codes - most common countries
|
|
15
|
+
export const countries: Country[] = [
|
|
16
|
+
{ code: "US", name: "United States" },
|
|
17
|
+
{ code: "CA", name: "Canada" },
|
|
18
|
+
{ code: "GB", name: "United Kingdom" },
|
|
19
|
+
{ code: "AU", name: "Australia" },
|
|
20
|
+
{ code: "DE", name: "Germany" },
|
|
21
|
+
{ code: "FR", name: "France" },
|
|
22
|
+
{ code: "ES", name: "Spain" },
|
|
23
|
+
{ code: "IT", name: "Italy" },
|
|
24
|
+
{ code: "NL", name: "Netherlands" },
|
|
25
|
+
{ code: "BE", name: "Belgium" },
|
|
26
|
+
{ code: "AT", name: "Austria" },
|
|
27
|
+
{ code: "CH", name: "Switzerland" },
|
|
28
|
+
{ code: "SE", name: "Sweden" },
|
|
29
|
+
{ code: "NO", name: "Norway" },
|
|
30
|
+
{ code: "DK", name: "Denmark" },
|
|
31
|
+
{ code: "FI", name: "Finland" },
|
|
32
|
+
{ code: "IE", name: "Ireland" },
|
|
33
|
+
{ code: "PT", name: "Portugal" },
|
|
34
|
+
{ code: "PL", name: "Poland" },
|
|
35
|
+
{ code: "CZ", name: "Czech Republic" },
|
|
36
|
+
{ code: "JP", name: "Japan" },
|
|
37
|
+
{ code: "KR", name: "South Korea" },
|
|
38
|
+
{ code: "CN", name: "China" },
|
|
39
|
+
{ code: "IN", name: "India" },
|
|
40
|
+
{ code: "BR", name: "Brazil" },
|
|
41
|
+
{ code: "MX", name: "Mexico" },
|
|
42
|
+
{ code: "AR", name: "Argentina" },
|
|
43
|
+
{ code: "CL", name: "Chile" },
|
|
44
|
+
{ code: "CO", name: "Colombia" },
|
|
45
|
+
{ code: "PE", name: "Peru" },
|
|
46
|
+
{ code: "NZ", name: "New Zealand" },
|
|
47
|
+
{ code: "SG", name: "Singapore" },
|
|
48
|
+
{ code: "HK", name: "Hong Kong" },
|
|
49
|
+
{ code: "TW", name: "Taiwan" },
|
|
50
|
+
{ code: "MY", name: "Malaysia" },
|
|
51
|
+
{ code: "TH", name: "Thailand" },
|
|
52
|
+
{ code: "PH", name: "Philippines" },
|
|
53
|
+
{ code: "ID", name: "Indonesia" },
|
|
54
|
+
{ code: "VN", name: "Vietnam" },
|
|
55
|
+
{ code: "ZA", name: "South Africa" },
|
|
56
|
+
{ code: "AE", name: "United Arab Emirates" },
|
|
57
|
+
{ code: "SA", name: "Saudi Arabia" },
|
|
58
|
+
{ code: "IL", name: "Israel" },
|
|
59
|
+
{ code: "TR", name: "Turkey" },
|
|
60
|
+
{ code: "RU", name: "Russia" },
|
|
61
|
+
{ code: "UA", name: "Ukraine" },
|
|
62
|
+
{ code: "GR", name: "Greece" },
|
|
63
|
+
{ code: "HU", name: "Hungary" },
|
|
64
|
+
{ code: "RO", name: "Romania" },
|
|
65
|
+
];
|
|
66
|
+
|
|
67
|
+
// US States
|
|
68
|
+
export const usStates: State[] = [
|
|
69
|
+
{ code: "AL", name: "Alabama", countryCode: "US" },
|
|
70
|
+
{ code: "AK", name: "Alaska", countryCode: "US" },
|
|
71
|
+
{ code: "AZ", name: "Arizona", countryCode: "US" },
|
|
72
|
+
{ code: "AR", name: "Arkansas", countryCode: "US" },
|
|
73
|
+
{ code: "CA", name: "California", countryCode: "US" },
|
|
74
|
+
{ code: "CO", name: "Colorado", countryCode: "US" },
|
|
75
|
+
{ code: "CT", name: "Connecticut", countryCode: "US" },
|
|
76
|
+
{ code: "DE", name: "Delaware", countryCode: "US" },
|
|
77
|
+
{ code: "FL", name: "Florida", countryCode: "US" },
|
|
78
|
+
{ code: "GA", name: "Georgia", countryCode: "US" },
|
|
79
|
+
{ code: "HI", name: "Hawaii", countryCode: "US" },
|
|
80
|
+
{ code: "ID", name: "Idaho", countryCode: "US" },
|
|
81
|
+
{ code: "IL", name: "Illinois", countryCode: "US" },
|
|
82
|
+
{ code: "IN", name: "Indiana", countryCode: "US" },
|
|
83
|
+
{ code: "IA", name: "Iowa", countryCode: "US" },
|
|
84
|
+
{ code: "KS", name: "Kansas", countryCode: "US" },
|
|
85
|
+
{ code: "KY", name: "Kentucky", countryCode: "US" },
|
|
86
|
+
{ code: "LA", name: "Louisiana", countryCode: "US" },
|
|
87
|
+
{ code: "ME", name: "Maine", countryCode: "US" },
|
|
88
|
+
{ code: "MD", name: "Maryland", countryCode: "US" },
|
|
89
|
+
{ code: "MA", name: "Massachusetts", countryCode: "US" },
|
|
90
|
+
{ code: "MI", name: "Michigan", countryCode: "US" },
|
|
91
|
+
{ code: "MN", name: "Minnesota", countryCode: "US" },
|
|
92
|
+
{ code: "MS", name: "Mississippi", countryCode: "US" },
|
|
93
|
+
{ code: "MO", name: "Missouri", countryCode: "US" },
|
|
94
|
+
{ code: "MT", name: "Montana", countryCode: "US" },
|
|
95
|
+
{ code: "NE", name: "Nebraska", countryCode: "US" },
|
|
96
|
+
{ code: "NV", name: "Nevada", countryCode: "US" },
|
|
97
|
+
{ code: "NH", name: "New Hampshire", countryCode: "US" },
|
|
98
|
+
{ code: "NJ", name: "New Jersey", countryCode: "US" },
|
|
99
|
+
{ code: "NM", name: "New Mexico", countryCode: "US" },
|
|
100
|
+
{ code: "NY", name: "New York", countryCode: "US" },
|
|
101
|
+
{ code: "NC", name: "North Carolina", countryCode: "US" },
|
|
102
|
+
{ code: "ND", name: "North Dakota", countryCode: "US" },
|
|
103
|
+
{ code: "OH", name: "Ohio", countryCode: "US" },
|
|
104
|
+
{ code: "OK", name: "Oklahoma", countryCode: "US" },
|
|
105
|
+
{ code: "OR", name: "Oregon", countryCode: "US" },
|
|
106
|
+
{ code: "PA", name: "Pennsylvania", countryCode: "US" },
|
|
107
|
+
{ code: "RI", name: "Rhode Island", countryCode: "US" },
|
|
108
|
+
{ code: "SC", name: "South Carolina", countryCode: "US" },
|
|
109
|
+
{ code: "SD", name: "South Dakota", countryCode: "US" },
|
|
110
|
+
{ code: "TN", name: "Tennessee", countryCode: "US" },
|
|
111
|
+
{ code: "TX", name: "Texas", countryCode: "US" },
|
|
112
|
+
{ code: "UT", name: "Utah", countryCode: "US" },
|
|
113
|
+
{ code: "VT", name: "Vermont", countryCode: "US" },
|
|
114
|
+
{ code: "VA", name: "Virginia", countryCode: "US" },
|
|
115
|
+
{ code: "WA", name: "Washington", countryCode: "US" },
|
|
116
|
+
{ code: "WV", name: "West Virginia", countryCode: "US" },
|
|
117
|
+
{ code: "WI", name: "Wisconsin", countryCode: "US" },
|
|
118
|
+
{ code: "WY", name: "Wyoming", countryCode: "US" },
|
|
119
|
+
{ code: "DC", name: "District of Columbia", countryCode: "US" },
|
|
120
|
+
];
|
|
121
|
+
|
|
122
|
+
// Canadian Provinces
|
|
123
|
+
export const caProvinces: State[] = [
|
|
124
|
+
{ code: "AB", name: "Alberta", countryCode: "CA" },
|
|
125
|
+
{ code: "BC", name: "British Columbia", countryCode: "CA" },
|
|
126
|
+
{ code: "MB", name: "Manitoba", countryCode: "CA" },
|
|
127
|
+
{ code: "NB", name: "New Brunswick", countryCode: "CA" },
|
|
128
|
+
{ code: "NL", name: "Newfoundland and Labrador", countryCode: "CA" },
|
|
129
|
+
{ code: "NS", name: "Nova Scotia", countryCode: "CA" },
|
|
130
|
+
{ code: "ON", name: "Ontario", countryCode: "CA" },
|
|
131
|
+
{ code: "PE", name: "Prince Edward Island", countryCode: "CA" },
|
|
132
|
+
{ code: "QC", name: "Quebec", countryCode: "CA" },
|
|
133
|
+
{ code: "SK", name: "Saskatchewan", countryCode: "CA" },
|
|
134
|
+
{ code: "NT", name: "Northwest Territories", countryCode: "CA" },
|
|
135
|
+
{ code: "NU", name: "Nunavut", countryCode: "CA" },
|
|
136
|
+
{ code: "YT", name: "Yukon", countryCode: "CA" },
|
|
137
|
+
];
|
|
138
|
+
|
|
139
|
+
// Get states for a country
|
|
140
|
+
export function getStatesForCountry(countryCode: string): State[] {
|
|
141
|
+
switch (countryCode) {
|
|
142
|
+
case "US":
|
|
143
|
+
return usStates;
|
|
144
|
+
case "CA":
|
|
145
|
+
return caProvinces;
|
|
146
|
+
default:
|
|
147
|
+
return [];
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Get country by code
|
|
152
|
+
export function getCountryByCode(code: string): Country | undefined {
|
|
153
|
+
return countries.find((c) => c.code === code);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Get state by code
|
|
157
|
+
export function getStateByCode(
|
|
158
|
+
stateCode: string,
|
|
159
|
+
countryCode: string,
|
|
160
|
+
): State | undefined {
|
|
161
|
+
const states = getStatesForCountry(countryCode);
|
|
162
|
+
return states.find((s) => s.code === stateCode);
|
|
163
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { FormDesign } from "@/api/types";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Convert FormDesign to CSS custom properties
|
|
5
|
+
*/
|
|
6
|
+
export function formDesignToCssVariables(
|
|
7
|
+
design: FormDesign,
|
|
8
|
+
): Record<string, string> {
|
|
9
|
+
const vars: Record<string, string> = {};
|
|
10
|
+
|
|
11
|
+
// Colors
|
|
12
|
+
if (design.background_color) {
|
|
13
|
+
vars["--lub-bg-color"] = design.background_color;
|
|
14
|
+
}
|
|
15
|
+
if (design.text_color) {
|
|
16
|
+
vars["--lub-text-color"] = design.text_color;
|
|
17
|
+
}
|
|
18
|
+
if (design.primary_color) {
|
|
19
|
+
vars["--lub-primary-color"] = design.primary_color;
|
|
20
|
+
}
|
|
21
|
+
if (design.border_color) {
|
|
22
|
+
vars["--lub-border-color"] = design.border_color;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Typography
|
|
26
|
+
if (design.font_family) {
|
|
27
|
+
vars["--lub-font-family"] = design.font_family;
|
|
28
|
+
}
|
|
29
|
+
if (design.font_size) {
|
|
30
|
+
vars["--lub-font-size"] = design.font_size;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Spacing
|
|
34
|
+
if (design.padding) {
|
|
35
|
+
vars["--lub-padding"] = design.padding;
|
|
36
|
+
}
|
|
37
|
+
if (design.field_spacing) {
|
|
38
|
+
vars["--lub-field-spacing"] = design.field_spacing;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Button styling
|
|
42
|
+
if (design.button_style) {
|
|
43
|
+
if (design.button_style.background_color) {
|
|
44
|
+
vars["--lub-btn-bg"] = design.button_style.background_color;
|
|
45
|
+
}
|
|
46
|
+
if (design.button_style.text_color) {
|
|
47
|
+
vars["--lub-btn-text"] = design.button_style.text_color;
|
|
48
|
+
}
|
|
49
|
+
if (design.button_style.border_radius) {
|
|
50
|
+
vars["--lub-btn-radius"] = design.button_style.border_radius;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return vars;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Apply CSS variables to an element's style
|
|
59
|
+
*/
|
|
60
|
+
export function applyCssVariables(design: FormDesign): React.CSSProperties {
|
|
61
|
+
const vars = formDesignToCssVariables(design);
|
|
62
|
+
return vars as React.CSSProperties;
|
|
63
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import type { Condition, FormField, Operator } from "@/api/types";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Evaluate if a field should be visible based on its conditional logic
|
|
5
|
+
*/
|
|
6
|
+
export function evaluateFieldVisibility(
|
|
7
|
+
field: FormField,
|
|
8
|
+
formValues: Record<string, unknown>,
|
|
9
|
+
): boolean {
|
|
10
|
+
const logic = field.conditional_logic;
|
|
11
|
+
if (!logic) return true;
|
|
12
|
+
|
|
13
|
+
// Check show_if condition
|
|
14
|
+
if (logic.show_if) {
|
|
15
|
+
return evaluateCondition(logic.show_if, formValues);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Check hide_if condition
|
|
19
|
+
if (logic.hide_if) {
|
|
20
|
+
return !evaluateCondition(logic.hide_if, formValues);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Evaluate if a field should be required based on its conditional logic
|
|
28
|
+
*/
|
|
29
|
+
export function evaluateFieldRequired(
|
|
30
|
+
field: FormField,
|
|
31
|
+
formValues: Record<string, unknown>,
|
|
32
|
+
): boolean {
|
|
33
|
+
const logic = field.conditional_logic;
|
|
34
|
+
|
|
35
|
+
// Start with base required state
|
|
36
|
+
let isRequired = field.required;
|
|
37
|
+
|
|
38
|
+
// Check required_if condition
|
|
39
|
+
if (logic?.required_if) {
|
|
40
|
+
const conditionMet = evaluateCondition(logic.required_if, formValues);
|
|
41
|
+
isRequired = isRequired || conditionMet;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return isRequired;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Evaluate a condition against form values
|
|
49
|
+
*/
|
|
50
|
+
export function evaluateCondition(
|
|
51
|
+
condition: Condition,
|
|
52
|
+
formValues: Record<string, unknown>,
|
|
53
|
+
): boolean {
|
|
54
|
+
const fieldValue = formValues[condition.field_name];
|
|
55
|
+
let result = compareValues(fieldValue, condition.operator, condition.value);
|
|
56
|
+
|
|
57
|
+
// Evaluate AND conditions
|
|
58
|
+
if (condition.and && condition.and.length > 0) {
|
|
59
|
+
for (const andCond of condition.and) {
|
|
60
|
+
if (!evaluateCondition(andCond, formValues)) {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Evaluate OR conditions - at least one must be true
|
|
67
|
+
if (condition.or && condition.or.length > 0) {
|
|
68
|
+
let orResult = false;
|
|
69
|
+
for (const orCond of condition.or) {
|
|
70
|
+
if (evaluateCondition(orCond, formValues)) {
|
|
71
|
+
orResult = true;
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
// For OR conditions to pass, main condition AND at least one OR must be true
|
|
76
|
+
result = result && orResult;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return result;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Compare field value against condition value using the specified operator
|
|
84
|
+
*/
|
|
85
|
+
function compareValues(
|
|
86
|
+
fieldValue: unknown,
|
|
87
|
+
operator: Operator,
|
|
88
|
+
compareValue: unknown,
|
|
89
|
+
): boolean {
|
|
90
|
+
switch (operator) {
|
|
91
|
+
case "equals":
|
|
92
|
+
return normalizeValue(fieldValue) === normalizeValue(compareValue);
|
|
93
|
+
|
|
94
|
+
case "not_equals":
|
|
95
|
+
return normalizeValue(fieldValue) !== normalizeValue(compareValue);
|
|
96
|
+
|
|
97
|
+
case "contains":
|
|
98
|
+
return String(fieldValue ?? "")
|
|
99
|
+
.toLowerCase()
|
|
100
|
+
.includes(String(compareValue).toLowerCase());
|
|
101
|
+
|
|
102
|
+
case "not_contains":
|
|
103
|
+
return !String(fieldValue ?? "")
|
|
104
|
+
.toLowerCase()
|
|
105
|
+
.includes(String(compareValue).toLowerCase());
|
|
106
|
+
|
|
107
|
+
case "greater_than":
|
|
108
|
+
return Number(fieldValue) > Number(compareValue);
|
|
109
|
+
|
|
110
|
+
case "less_than":
|
|
111
|
+
return Number(fieldValue) < Number(compareValue);
|
|
112
|
+
|
|
113
|
+
case "is_empty":
|
|
114
|
+
return isEmpty(fieldValue);
|
|
115
|
+
|
|
116
|
+
case "is_not_empty":
|
|
117
|
+
return !isEmpty(fieldValue);
|
|
118
|
+
|
|
119
|
+
default:
|
|
120
|
+
return true;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Normalize value for comparison
|
|
126
|
+
*/
|
|
127
|
+
function normalizeValue(value: unknown): string {
|
|
128
|
+
if (value === null || value === undefined) return "";
|
|
129
|
+
if (Array.isArray(value)) return value.sort().join(",");
|
|
130
|
+
return String(value).toLowerCase().trim();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Check if value is empty
|
|
135
|
+
*/
|
|
136
|
+
function isEmpty(value: unknown): boolean {
|
|
137
|
+
if (value === null || value === undefined) return true;
|
|
138
|
+
if (typeof value === "string") return value.trim() === "";
|
|
139
|
+
if (Array.isArray(value)) return value.length === 0;
|
|
140
|
+
if (typeof value === "object") return Object.keys(value).length === 0;
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Get all fields that should be visible based on conditional logic
|
|
146
|
+
*/
|
|
147
|
+
export function getVisibleFields(
|
|
148
|
+
fields: FormField[],
|
|
149
|
+
formValues: Record<string, unknown>,
|
|
150
|
+
): FormField[] {
|
|
151
|
+
return fields.filter((field) => evaluateFieldVisibility(field, formValues));
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Get required field names based on conditional logic
|
|
156
|
+
*/
|
|
157
|
+
export function getRequiredFieldNames(
|
|
158
|
+
fields: FormField[],
|
|
159
|
+
formValues: Record<string, unknown>,
|
|
160
|
+
): Set<string> {
|
|
161
|
+
const required = new Set<string>();
|
|
162
|
+
|
|
163
|
+
for (const field of fields) {
|
|
164
|
+
if (evaluateFieldRequired(field, formValues)) {
|
|
165
|
+
required.add(field.name);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return required;
|
|
170
|
+
}
|