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 CHANGED
@@ -1,11 +1,16 @@
1
- # ov-cart
1
+ # ov-configurator
2
2
 
3
- Universal frontend shopping cart module. Exports `createCart(options)`.
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-cart
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-cart/dist/ov-cart.iife.js"></script>
21
+ <script src="https://cdn.jsdelivr.net/npm/ov-configurator/dist/ov-configurator.iife.js"></script>
17
22
  <script>
18
- const cart = OvCart.createCart({ element: '#cart', currency: '$' })
23
+ const { Calculator, initConfigurator } = OvConfiurator
19
24
  </script>
20
25
  ```
21
26
 
22
- The IIFE bundle exposes a global `OvCart` with `createCart` and `Cart`.
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
- ## Usage
35
+ ### Basic usage
25
36
 
26
37
  ```ts
27
- import { createCart } from 'ov-cart'
28
-
29
- const cart = createCart({
30
- element: '#my-cart',
31
- currency: '$',
32
- storageKey: 'shop-cart',
33
- attr: 'mycart', // uses data-mycart-* instead of data-cart-*
34
- classes: {
35
- countButton: 'btn btn-sm',
36
- },
37
- onUpdate(state) {
38
- // state: { products, productsCount, totalSum, count, isEmpty }
39
- if (state.isEmpty) MicroModal.close('modal-cart')
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
- if (window.clearCart) cart.clear()
64
+ calc.renderInNode(document.getElementById('form-place')!)
65
+ calc.init()
44
66
  ```
45
67
 
46
- ## Multiple carts on the same page
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
- Each `createCart()` call is an independent instance. When using multiple carts on a page, assign different `attr` values (and `storageKey` if separate storage is needed).
70
+ ### `CalculatorOptions`
49
71
 
50
- ```ts
51
- const cartA = createCart({ element: '#cart-a', attr: 'cart-a', storageKey: 'cart-a' })
52
- const cartB = createCart({ element: '#cart-b', attr: 'cart-b', storageKey: 'cart-b' })
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
- ## Options
83
+ ### `FieldOptions`
56
84
 
57
85
  | option | type | default | description |
58
86
  |---|---|---|---|
59
- | `element` | `string \| Element` | `'#cart'` | cart render container |
60
- | `icon` | `string \| Element \| null` | `'#cart-icon'` | icon element with counter and total |
61
- | `currency` | `string` | `'zł'` | currency symbol |
62
- | `storageKey` | `string` | `'cart'` | `localStorage` key |
63
- | `attr` | `string` | `'cart'` | data-attribute prefix (`'cart'` `data-cart-*`) |
64
- | `shouldAnimateIconSum` | `function(state, trigger)` | `() => true` | controls whether the icon sum animation plays |
65
- | `renderItem` | `function(id, product, count, currency)` | built-in template | returns HTML string for a single cart item |
66
- | `renderFooter` | `function(total, currency)` | built-in template | returns HTML string for the cart footer |
67
- | `onUpdate` | `function(state)` | `null` | called after every cart change |
68
- | `validateCart` | `function(products)` | `null` | called on init after loading from `localStorage`; receives and returns `CartProductEntry[]` — use to filter out stale items |
69
- | `classes` | `object` | Tailwind classes | partial override of CSS classes (unset keys fall back to defaults) |
70
- | `text` | `object` | see below | partial override of text strings |
71
-
72
- ## Default CSS classes (`classes`)
73
-
74
- | key | description |
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
- | `list` | `<ul>` product list |
77
- | `item` | `<li>` product row |
78
- | `itemInner` | inner wrapper (flex container) |
79
- | `itemContent` | block with title and description |
80
- | `itemTitle` | product title |
81
- | `itemDescription` | product description |
82
- | `itemImage` | product image |
83
- | `itemPriceBox` | price block |
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
- ### `shouldAnimateIconSum(state, trigger)`
121
+ ---
104
122
 
105
- Called before the icon sum animation. Return `false` to suppress the animation. Not called on initial render.
123
+ ## initConfigurator
106
124
 
107
- - `state` current cart state `{ products, productsCount, totalSum, count, isEmpty }`
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
- ```ts
111
- shouldAnimateIconSum: () => true
112
- ```
127
+ ### HTML structure
113
128
 
114
- ### `validateCart(products)`
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
- Called once on initialization, after restoring the cart from `localStorage`. Receives and must return a `CartProductEntry[]` array (`CartProduct & { id: string }`). Use it to filter or update stale products. The result is saved back to `localStorage`.
159
+ ### JS initialization
117
160
 
118
161
  ```ts
119
- validateCart(products) {
120
- const validIds = new Set(['sku-1', 'sku-3'])
121
- return products.filter(p => validIds.has(p.id))
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
- ## Data attributes
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
- By default the prefix is `data-cart-`. With `attr: 'mycart'` it becomes `data-mycart-`.
189
+ ### `ConfiguratorSection`
128
190
 
129
- **Buttons:**
130
- - `data-cart-action="add"` — increment item
131
- - `data-cart-action="remove"` decrement item
132
- - `data-cart-action="clear"` remove item from cart
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
- **Item container:**
135
- - `data-cart-item="<id>"` — product identifier
201
+ ### Built-in fields registered by `initConfigurator`
136
202
 
137
- **Item parameters** (on descendants of `data-cart-item`):
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
- **Icon** (`icon`):
145
- - `data-cart-icon-count` — element displaying item count
146
- - `data-cart-icon-sum` element displaying the total (must contain a `<span>`)
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
- > All attributes use the prefix from the `attr` option. With `attr: 'mycart'` → `data-mycart-action`, `data-mycart-item`, `data-mycart-icon-count`, etc.
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
- ```js
152
- import { createCart } from '../../libs/cart/cart.js'
221
+ ```ts
222
+ import { queryByPath } from 'ov-configurator'
153
223
 
154
- const cart = createCart({
155
- element: '#my-cart',
156
- currency: '₽',
157
- storageKey: 'shop-cart',
158
- attr: 'mycart', // data-mycart-* вместо data-cart-*
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
- if (window.clearCart) cart.clear()
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;
@@ -13,14 +13,14 @@ interface GetNodeOptions {
13
13
  declare class Field<TData = unknown> {
14
14
  selector: string;
15
15
  options: Required<FieldOptions<TData>>;
16
- node: HTMLElement | null;
17
- animationId: number;
16
+ nodes: HTMLElement[];
17
+ animationIds: number[];
18
18
  prevValue: unknown;
19
19
  constructor(options: FieldOptions<TData>);
20
- getNode(options: GetNodeOptions): HTMLElement | null;
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
- init(): void;
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 {};
@@ -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;