ov-configurator 1.0.0 → 1.1.1
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 +184 -116
- package/dist/calculator-form.d.ts +20 -0
- package/dist/calculator.d.ts +8 -10
- package/dist/configurator.d.ts +3 -3
- package/dist/ov-configurator.cjs +6 -30
- package/dist/ov-configurator.iife.js +6 -30
- package/dist/ov-configurator.js +1030 -462
- package/dist/types.d.ts +11 -2
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
|
-
# ov-
|
|
1
|
+
# ov-configurator
|
|
2
2
|
|
|
3
|
-
Universal frontend
|
|
3
|
+
Universal frontend product configurator module. Exports `Calculator` and `initConfigurator`.
|
|
4
|
+
|
|
5
|
+
The package ships two complementary tools:
|
|
6
|
+
|
|
7
|
+
- **`Calculator`** — a low-level class that renders a form, tracks selected values and computes derived fields (price, labels, etc.). Configure everything in JS.
|
|
8
|
+
- **`initConfigurator`** — a higher-level helper that reads an inline JS config from the DOM (`[data-configurator-options]`) and builds a fully wired `Calculator` for a product element. Designed for server-rendered pages where the config lives in HTML.
|
|
4
9
|
|
|
5
10
|
## Installation
|
|
6
11
|
|
|
7
12
|
```bash
|
|
8
|
-
npm install ov-
|
|
13
|
+
npm install ov-configurator
|
|
9
14
|
```
|
|
10
15
|
|
|
11
16
|
## CDN
|
|
@@ -13,158 +18,221 @@ npm install ov-cart
|
|
|
13
18
|
No build step required — include directly via [jsDelivr](https://www.jsdelivr.com/) or [unpkg](https://unpkg.com/):
|
|
14
19
|
|
|
15
20
|
```html
|
|
16
|
-
<script src="https://cdn.jsdelivr.net/npm/ov-
|
|
21
|
+
<script src="https://cdn.jsdelivr.net/npm/ov-configurator/dist/ov-configurator.iife.js"></script>
|
|
17
22
|
<script>
|
|
18
|
-
const
|
|
23
|
+
const { Calculator, initConfigurator } = OvConfiurator
|
|
19
24
|
</script>
|
|
20
25
|
```
|
|
21
26
|
|
|
22
|
-
The IIFE bundle exposes a global `
|
|
27
|
+
The IIFE bundle exposes a global `OvConfiurator` with `Calculator` and `initConfigurator`.
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## Calculator
|
|
32
|
+
|
|
33
|
+
Renders a `<form>` with radio/checkbox sections and recomputes fields on every change.
|
|
23
34
|
|
|
24
|
-
|
|
35
|
+
### Basic usage
|
|
25
36
|
|
|
26
37
|
```ts
|
|
27
|
-
import {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
},
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
import { Calculator } from 'ov-configurator'
|
|
39
|
+
|
|
40
|
+
interface ProductData {
|
|
41
|
+
sizes: Array<{ label: string; value: string; price: number }>
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const calc = new Calculator<ProductData>({
|
|
45
|
+
id: 'my-product',
|
|
46
|
+
parentSelector: '#product-card',
|
|
47
|
+
data: { sizes: [{ label: 'S', value: 's', price: 100 }] },
|
|
48
|
+
sectionsOptions: [
|
|
49
|
+
{
|
|
50
|
+
title: 'Size',
|
|
51
|
+
type: 'size',
|
|
52
|
+
inputs: [{ label: 'S', value: 's' }],
|
|
53
|
+
},
|
|
54
|
+
],
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
calc.addField({
|
|
58
|
+
selector: 'price',
|
|
59
|
+
calculateFunction(values, data) {
|
|
60
|
+
return data.sizes.find(s => s.value === values.size)?.price ?? 0
|
|
40
61
|
},
|
|
41
62
|
})
|
|
42
63
|
|
|
43
|
-
|
|
64
|
+
calc.renderInNode(document.getElementById('form-place')!)
|
|
65
|
+
calc.init()
|
|
44
66
|
```
|
|
45
67
|
|
|
46
|
-
|
|
68
|
+
The form is rendered into the given node. On each `change` event all fields are recomputed and written to matching `[data-calc="<selector>"]` elements inside `parentSelector`.
|
|
47
69
|
|
|
48
|
-
|
|
70
|
+
### `CalculatorOptions`
|
|
49
71
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
72
|
+
| option | type | default | description |
|
|
73
|
+
|---|---|---|---|
|
|
74
|
+
| `id` | `string` | — | unique identifier for the instance |
|
|
75
|
+
| `parentSelector` | `string` | — | CSS selector of the container element |
|
|
76
|
+
| `data` | `TData \| null` | `null` | arbitrary data object passed to `calculateFunction` |
|
|
77
|
+
| `editableFields` | `FieldOptions[]` | `[]` | fields to register on construction |
|
|
78
|
+
| `sectionsOptions` | `SectionOptions[]` | `[]` | sections to render in the form |
|
|
79
|
+
| `prefix` | `string` | `'card'` | input name prefix (`{prefix}-{id}-{type}`) |
|
|
80
|
+
| `stylePrefix` | `string` | `'calculator'` | BEM block name for generated HTML |
|
|
81
|
+
| `dataAttributePrefix` | `string` | `'calc'` | prefix for `data-{prefix}="<selector>"` attributes |
|
|
54
82
|
|
|
55
|
-
|
|
83
|
+
### `FieldOptions`
|
|
56
84
|
|
|
57
85
|
| option | type | default | description |
|
|
58
86
|
|---|---|---|---|
|
|
59
|
-
| `
|
|
60
|
-
| `
|
|
61
|
-
| `
|
|
62
|
-
| `
|
|
63
|
-
| `
|
|
64
|
-
| `
|
|
65
|
-
| `
|
|
66
|
-
| `
|
|
67
|
-
| `
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
|
87
|
+
| `selector` | `string` | — | matches `data-{dataAttributePrefix}="<selector>"` in the DOM |
|
|
88
|
+
| `calculateFunction` | `(values, data) => unknown` | `() => null` | returns the new value; `null` is ignored |
|
|
89
|
+
| `attribute` | `boolean` | `false` | write value as an attribute instead of `innerHTML` (reads `data-calc-attribute` from the node) |
|
|
90
|
+
| `animated` | `boolean` | `false` | animate block on value change (requires Animate.css) |
|
|
91
|
+
| `animatedNode` | `'parent' \| 'self' \| 'closest'` | `'parent'` | which node to animate |
|
|
92
|
+
| `animatedNodeSelector` | `string` | — | selector for `closest()` when `animatedNode='closest'` |
|
|
93
|
+
| `animation` | `string[]` | `['animate__faster', 'animate__pulse']` | Animate.css classes |
|
|
94
|
+
| `duration` | `number` | `800` | number animation duration (ms) |
|
|
95
|
+
| `dots` | `number` | `0` | decimal places for number animation |
|
|
96
|
+
|
|
97
|
+
### `SectionOptions`
|
|
98
|
+
|
|
99
|
+
| option | type | default | description |
|
|
100
|
+
|---|---|---|---|
|
|
101
|
+
| `type` | `string` | — | key used in `values` object and input `name` |
|
|
102
|
+
| `title` | `string` | — | section heading |
|
|
103
|
+
| `inputs` | `SectionInput[] \| (data) => SectionInput[]` | — | static list or factory function |
|
|
104
|
+
| `className` | `string` | `''` | extra class on each `.{stylePrefix}__section-col` |
|
|
105
|
+
| `checked` | `boolean` | `true` | pre-select first matching value |
|
|
106
|
+
| `inputType` | `string` | `'radio'` | input type (`'radio'` or `'checkbox'`) |
|
|
107
|
+
|
|
108
|
+
### Instance methods
|
|
109
|
+
|
|
110
|
+
| method | description |
|
|
75
111
|
|---|---|
|
|
76
|
-
| `
|
|
77
|
-
| `
|
|
78
|
-
| `
|
|
79
|
-
| `
|
|
80
|
-
| `
|
|
81
|
-
| `
|
|
82
|
-
| `
|
|
83
|
-
| `
|
|
84
|
-
| `itemPriceHidden` | hidden `<span>` with price (for form submission) |
|
|
85
|
-
| `controls` | +/− button group |
|
|
86
|
-
| `countButton` | +/− buttons |
|
|
87
|
-
| `countSpan` | `<span>` with item count |
|
|
88
|
-
| `removeButton` | remove item button |
|
|
89
|
-
| `footer` | cart total block |
|
|
90
|
-
| `iconHidden` | class applied to hide the cart icon |
|
|
91
|
-
| `iconSumVisible` | class applied to animate the icon sum |
|
|
92
|
-
|
|
93
|
-
## Default text (`text`)
|
|
94
|
-
|
|
95
|
-
| key | default | description |
|
|
96
|
-
|---|---|---|
|
|
97
|
-
| `sumLabel` | `'Total:'` | label before the total sum |
|
|
98
|
-
| `orderInputName` | `'order-sum'` | `name` attribute of the hidden order sum input |
|
|
99
|
-
| `addButton` | `'+'` | increment button text |
|
|
100
|
-
| `removeButton` | `'−'` | decrement button text |
|
|
101
|
-
| `clearButton` | `'✕'` | remove item button text |
|
|
112
|
+
| `addField(options)` | register a new field |
|
|
113
|
+
| `addSection(options)` | add a section to the form |
|
|
114
|
+
| `init()` | attach DOM nodes to fields and wire the `change` listener |
|
|
115
|
+
| `renderInNode(node)` | write generated HTML into `node` and store a reference |
|
|
116
|
+
| `refresh()` | re-render HTML and re-init (use when inputs change dynamically) |
|
|
117
|
+
| `getValues()` | return current `{ [type]: value }` map |
|
|
118
|
+
| `getValue(key)` | return the last saved value for a field `selector` |
|
|
119
|
+
| `saveValue(key, value)` | manually persist a value accessible via `getValue` |
|
|
102
120
|
|
|
103
|
-
|
|
121
|
+
---
|
|
104
122
|
|
|
105
|
-
|
|
123
|
+
## initConfigurator
|
|
106
124
|
|
|
107
|
-
- `
|
|
108
|
-
- `trigger` — the `<button>` element that triggered the change
|
|
125
|
+
Higher-level helper for server-rendered pages. Reads the config from an inline `<script type="text/plain" data-configurator-options>` element, receives product data externally, and builds a `Calculator` with standard fields (`price`, `old-price`, `params`, per-section value/view fields, and mirrored attribute fields).
|
|
109
126
|
|
|
110
|
-
|
|
111
|
-
shouldAnimateIconSum: () => true
|
|
112
|
-
```
|
|
127
|
+
### HTML structure
|
|
113
128
|
|
|
114
|
-
|
|
129
|
+
```html
|
|
130
|
+
<div data-calc-id="product-a">
|
|
131
|
+
<!-- output fields -->
|
|
132
|
+
<span data-calc="price"></span>
|
|
133
|
+
<span data-calc="old-price"></span>
|
|
134
|
+
<span data-calc="params"></span>
|
|
135
|
+
|
|
136
|
+
<!-- Calculator renders the form here -->
|
|
137
|
+
<div data-calc-place="product-a"></div>
|
|
138
|
+
|
|
139
|
+
<!-- inline config (JS object syntax, functions allowed) -->
|
|
140
|
+
<script type="text/plain" data-configurator-options>
|
|
141
|
+
{
|
|
142
|
+
oldPricePercent: 20,
|
|
143
|
+
getPrice: function(values, item) {
|
|
144
|
+
return item.basePrice + (item.sizePrices[values.size] || 0)
|
|
145
|
+
},
|
|
146
|
+
sections: [
|
|
147
|
+
{
|
|
148
|
+
key: 'size',
|
|
149
|
+
title: 'Size',
|
|
150
|
+
path: '$.sizes[*]',
|
|
151
|
+
postfix: ' m'
|
|
152
|
+
}
|
|
153
|
+
]
|
|
154
|
+
}
|
|
155
|
+
</script>
|
|
156
|
+
</div>
|
|
157
|
+
```
|
|
115
158
|
|
|
116
|
-
|
|
159
|
+
### JS initialization
|
|
117
160
|
|
|
118
161
|
```ts
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
162
|
+
import { initConfigurator } from 'ov-configurator'
|
|
163
|
+
import type { DataSource } from 'ov-configurator'
|
|
164
|
+
|
|
165
|
+
const data: DataSource = {
|
|
166
|
+
'product-a': {
|
|
167
|
+
basePrice: 45_000,
|
|
168
|
+
sizes: ['3x4', '3x6'],
|
|
169
|
+
sizePrices: { '3x4': 0, '3x6': 15_000 },
|
|
170
|
+
},
|
|
122
171
|
}
|
|
172
|
+
|
|
173
|
+
document.querySelectorAll<HTMLElement>('[data-calc-id]').forEach((el) => {
|
|
174
|
+
initConfigurator(data, el)
|
|
175
|
+
})
|
|
123
176
|
```
|
|
124
177
|
|
|
125
|
-
|
|
178
|
+
`initConfigurator` is also exported as `Configurator` (alias).
|
|
179
|
+
|
|
180
|
+
### `ConfiguratorOptions` (inline config object)
|
|
181
|
+
|
|
182
|
+
| key | type | description |
|
|
183
|
+
|---|---|---|
|
|
184
|
+
| `getPrice` | `(values, item, id) => number` | returns the current price |
|
|
185
|
+
| `oldPricePercent` | `number` | discount % — old price = `price / ((100 - n) / 100)` |
|
|
186
|
+
| `sections` | `ConfiguratorSection[]` | sections to build |
|
|
187
|
+
| `fields` | `ConfiguratorField[]` | optional extra fields |
|
|
126
188
|
|
|
127
|
-
|
|
189
|
+
### `ConfiguratorSection`
|
|
128
190
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
191
|
+
| key | type | description |
|
|
192
|
+
|---|---|---|
|
|
193
|
+
| `key` | `string` | field selector and value key in `values` |
|
|
194
|
+
| `title` | `string` | section heading |
|
|
195
|
+
| `path` | `string` | JSONPath / pipe expression to extract inputs from item data |
|
|
196
|
+
| `postfix` | `string` | suffix appended to values in labels and `params` |
|
|
197
|
+
| `inputs` | `SectionInput[]` | static inputs list (overrides `path`) |
|
|
198
|
+
| `labelMapping` | `Record<string, string>` | map raw values to display labels |
|
|
199
|
+
| `selectorDisplay` | `(selected, sectionData) => string` | custom display for the `*-view` field |
|
|
133
200
|
|
|
134
|
-
|
|
135
|
-
- `data-cart-item="<id>"` — product identifier
|
|
201
|
+
### Built-in fields registered by `initConfigurator`
|
|
136
202
|
|
|
137
|
-
|
|
138
|
-
- `data-cart-item-title` — name (attribute value, or `"self"` to use `textContent`)
|
|
139
|
-
- `data-cart-item-price` — price
|
|
140
|
-
- `data-cart-item-image` — image URL
|
|
141
|
-
- `data-cart-item-description` — description (multiline, separated by `\n`)
|
|
142
|
-
- `data-cart-item-params` — additional parameters
|
|
203
|
+
For each `section` with key `k` the following `data-calc` targets are populated:
|
|
143
204
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
205
|
+
| selector | content |
|
|
206
|
+
|---|---|
|
|
207
|
+
| `price` | result of `getPrice` |
|
|
208
|
+
| `old-price` | price before discount |
|
|
209
|
+
| `params` | space-joined `value + postfix` for every section |
|
|
210
|
+
| `params-view` | newline-joined `title: value + postfix` |
|
|
211
|
+
| `k` | selected raw value |
|
|
212
|
+
| `k-view` | display label (via `labelMapping` / `selectorDisplay`) |
|
|
213
|
+
| `price` *(attr)* | same value written as an HTML attribute |
|
|
214
|
+
| `old-price` *(attr)* | same |
|
|
215
|
+
| `k` *(attr)* | same |
|
|
147
216
|
|
|
148
|
-
|
|
217
|
+
### `queryByPath`
|
|
149
218
|
|
|
219
|
+
Utility used internally by `initConfigurator` to traverse item data. Available as a named export for advanced custom fields.
|
|
150
220
|
|
|
151
|
-
```
|
|
152
|
-
import {
|
|
221
|
+
```ts
|
|
222
|
+
import { queryByPath } from 'ov-configurator'
|
|
153
223
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
classes: {
|
|
160
|
-
countButton: 'btn btn-sm',
|
|
161
|
-
},
|
|
162
|
-
onUpdate(state) {
|
|
163
|
-
// state: { products, productsCount, totalSum, count, isEmpty }
|
|
164
|
-
if (state.isEmpty) MicroModal.close('modal-cart')
|
|
165
|
-
},
|
|
166
|
-
})
|
|
224
|
+
// JSONPath
|
|
225
|
+
queryByPath(data, '$.sizes[*]')
|
|
226
|
+
|
|
227
|
+
// chained operations
|
|
228
|
+
queryByPath(data, '$.variants > $first')
|
|
167
229
|
|
|
168
|
-
|
|
230
|
+
// fallback chain
|
|
231
|
+
queryByPath(data, '$.primary || $.fallback')
|
|
232
|
+
|
|
233
|
+
// save/get across operations
|
|
234
|
+
queryByPath(data, '$save:key > $.next')
|
|
169
235
|
```
|
|
170
236
|
|
|
237
|
+
Supported special operations: `$root`, `$first`, `$last`, `$save:<key>`, `$get:<key>`, any JSONPath expression.
|
|
238
|
+
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { SectionOptions, SectionInput } from './types';
|
|
2
|
+
export interface ResolvedSection {
|
|
3
|
+
options: SectionOptions;
|
|
4
|
+
inputs: SectionInput[];
|
|
5
|
+
checkedValue: string;
|
|
6
|
+
}
|
|
7
|
+
export interface CalculatorFormProps {
|
|
8
|
+
sections: SectionOptions[];
|
|
9
|
+
initialSections: ResolvedSection[];
|
|
10
|
+
initialValues: Record<string, string>;
|
|
11
|
+
stylePrefix: string;
|
|
12
|
+
dataAttributePrefix: string;
|
|
13
|
+
prefix: string;
|
|
14
|
+
id: string;
|
|
15
|
+
onValuesChange: (values: Record<string, string>) => void;
|
|
16
|
+
}
|
|
17
|
+
export declare const resolveInputs: (section: SectionOptions, values: Record<string, string>) => SectionInput[];
|
|
18
|
+
export declare function resolveSections(sections: SectionOptions[], values: Record<string, string>): ResolvedSection[];
|
|
19
|
+
export declare function renderCalculatorForm(container: HTMLElement, props: CalculatorFormProps): void;
|
|
20
|
+
export declare function unmountCalculatorForm(container: HTMLElement): void;
|
package/dist/calculator.d.ts
CHANGED
|
@@ -13,14 +13,14 @@ interface GetNodeOptions {
|
|
|
13
13
|
declare class Field<TData = unknown> {
|
|
14
14
|
selector: string;
|
|
15
15
|
options: Required<FieldOptions<TData>>;
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
nodes: HTMLElement[];
|
|
17
|
+
animationIds: number[];
|
|
18
18
|
prevValue: unknown;
|
|
19
19
|
constructor(options: FieldOptions<TData>);
|
|
20
|
-
getNode(options: GetNodeOptions): HTMLElement
|
|
20
|
+
getNode(options: GetNodeOptions): HTMLElement[];
|
|
21
21
|
changeValue(value: unknown): void;
|
|
22
22
|
animateBlock(value: unknown): void;
|
|
23
|
-
animateNumber(to: number): void;
|
|
23
|
+
animateNumber(to: number, node: HTMLElement, index: number): void;
|
|
24
24
|
}
|
|
25
25
|
export default class Calculator<TData = unknown> {
|
|
26
26
|
static fieldTypes: {
|
|
@@ -43,18 +43,16 @@ export default class Calculator<TData = unknown> {
|
|
|
43
43
|
};
|
|
44
44
|
data: TData | null;
|
|
45
45
|
fields: Field<TData>[];
|
|
46
|
-
private form;
|
|
47
46
|
constructor(options: CalculatorOptions<TData>);
|
|
48
47
|
addField(field: FieldOptions<TData>): void;
|
|
49
48
|
addSection(section: SectionOptions): void;
|
|
50
|
-
|
|
49
|
+
private sortSections;
|
|
50
|
+
private buildInitialSections;
|
|
51
|
+
private processFields;
|
|
52
|
+
render(node: HTMLElement): void;
|
|
51
53
|
getValues(): Record<string, string>;
|
|
52
|
-
changeHandler(form: HTMLFormElement): void;
|
|
53
54
|
saveValue(key: string, value: unknown): void;
|
|
54
55
|
getValue(key: string): unknown;
|
|
55
|
-
getHTML(): string;
|
|
56
|
-
getSectionHTML(options: SectionOptions): string;
|
|
57
|
-
renderInNode(node: HTMLElement): void;
|
|
58
56
|
refresh(): void;
|
|
59
57
|
}
|
|
60
58
|
export {};
|
package/dist/configurator.d.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { DataSource } from './types';
|
|
2
|
-
export declare function getOptions(dataSource: DataSource, element: HTMLElement): {
|
|
1
|
+
import { DataSource, InitConfiguratorOptions } from './types';
|
|
2
|
+
export declare function getOptions(dataSource: DataSource, element: HTMLElement, { calcIdAttribute, calcPlaceSelector, }: InitConfiguratorOptions): {
|
|
3
3
|
data: unknown;
|
|
4
4
|
uniqId: string;
|
|
5
5
|
id: string;
|
|
6
6
|
placeNode: HTMLElement;
|
|
7
7
|
} | null;
|
|
8
8
|
export declare function queryByPath(data: unknown, path: string, values?: Record<string, string>): unknown;
|
|
9
|
-
export declare function initConfigurator(dataSource: DataSource, element: HTMLElement): void;
|
|
9
|
+
export declare function initConfigurator(dataSource: DataSource, element: HTMLElement, initOptions: InitConfiguratorOptions): void;
|
|
10
10
|
/** Alias for {@link initConfigurator} */
|
|
11
11
|
export declare const Configurator: typeof initConfigurator;
|