@magic-spells/cart-progress-bar 0.2.1 → 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/dist/cart-progress-bar.cjs.js +164 -234
- package/dist/cart-progress-bar.cjs.js.map +1 -1
- package/dist/cart-progress-bar.esm.js +164 -234
- package/dist/cart-progress-bar.esm.js.map +1 -1
- package/dist/cart-progress-bar.js +164 -234
- package/dist/cart-progress-bar.js.map +1 -1
- package/dist/cart-progress-bar.min.js +1 -1
- package/package.json +4 -4
- package/src/cart-progress-bar.js +164 -234
package/src/cart-progress-bar.js
CHANGED
|
@@ -6,23 +6,18 @@ import './cart-progress-bar.scss';
|
|
|
6
6
|
class ProgressBar extends HTMLElement {
|
|
7
7
|
constructor() {
|
|
8
8
|
super();
|
|
9
|
-
this
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
this.setAttribute('aria-valuemax', '100');
|
|
16
|
-
this.setAttribute('aria-valuenow', '0');
|
|
17
|
-
|
|
18
|
-
// Create the visual progress bar
|
|
19
|
-
this.innerHTML = '<div class="progress-bar-fill"></div>';
|
|
9
|
+
const _ = this;
|
|
10
|
+
_.setAttribute('role', 'progressbar');
|
|
11
|
+
_.setAttribute('aria-valuemin', '0');
|
|
12
|
+
_.setAttribute('aria-valuemax', '100');
|
|
13
|
+
_.setAttribute('aria-valuenow', '0');
|
|
14
|
+
_.innerHTML = '<div class="progress-bar-fill"></div>';
|
|
20
15
|
}
|
|
21
16
|
|
|
22
17
|
setPercent(percent) {
|
|
23
|
-
const
|
|
24
|
-
this.style.setProperty('--cart-progress-percent', `${
|
|
25
|
-
this.setAttribute('aria-valuenow',
|
|
18
|
+
const p = Math.max(0, Math.min(100, percent));
|
|
19
|
+
this.style.setProperty('--cart-progress-percent', `${p}%`);
|
|
20
|
+
this.setAttribute('aria-valuenow', p);
|
|
26
21
|
}
|
|
27
22
|
}
|
|
28
23
|
|
|
@@ -34,295 +29,230 @@ customElements.define('progress-bar', ProgressBar);
|
|
|
34
29
|
*/
|
|
35
30
|
class CartProgressBar extends HTMLElement {
|
|
36
31
|
// Private fields
|
|
37
|
-
#
|
|
38
|
-
#
|
|
39
|
-
#
|
|
40
|
-
#
|
|
41
|
-
#
|
|
42
|
-
#
|
|
43
|
-
#
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
*/
|
|
32
|
+
#threshold = 0;
|
|
33
|
+
#current = 0;
|
|
34
|
+
#percent = 0;
|
|
35
|
+
#msgAbove = '';
|
|
36
|
+
#msgBelow = '';
|
|
37
|
+
#bar = null;
|
|
38
|
+
#msgEl = null;
|
|
39
|
+
#moneyFmt = null;
|
|
40
|
+
#debounce = null;
|
|
41
|
+
|
|
48
42
|
static get observedAttributes() {
|
|
49
|
-
return ['threshold', 'current', 'message-above', 'message-below'];
|
|
43
|
+
return ['threshold', 'current', 'message-above', 'message-below', 'money-format'];
|
|
50
44
|
}
|
|
51
45
|
|
|
52
46
|
constructor() {
|
|
53
47
|
super();
|
|
54
|
-
this
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
this.#currentAmount = parseFloat(this.getAttribute('current')) || 0;
|
|
61
|
-
|
|
62
|
-
// Store original message templates
|
|
63
|
-
this.#originalAboveMessage = this.getAttribute('message-above') || '';
|
|
64
|
-
this.#originalBelowMessage = this.getAttribute('message-below') || '';
|
|
48
|
+
const _ = this;
|
|
49
|
+
_.#threshold = parseFloat(_.getAttribute('threshold')) || 0;
|
|
50
|
+
_.#current = parseFloat(_.getAttribute('current')) || 0;
|
|
51
|
+
_.#msgAbove = _.getAttribute('message-above') || '';
|
|
52
|
+
_.#msgBelow = _.getAttribute('message-below') || '';
|
|
53
|
+
_.#moneyFmt = _.getAttribute('money-format');
|
|
65
54
|
}
|
|
66
55
|
|
|
67
56
|
async connectedCallback() {
|
|
68
|
-
// ensure the child custom element has been registered
|
|
69
57
|
await customElements.whenDefined('progress-bar');
|
|
70
|
-
|
|
71
|
-
if (!customElements.get('progress-bar')) {
|
|
72
|
-
throw new Error('<progress-bar> must be registered before <cart-progress-bar> runs');
|
|
73
|
-
}
|
|
74
|
-
|
|
75
58
|
this.#render();
|
|
76
59
|
this.#updateProgress();
|
|
77
60
|
this.#attachListeners();
|
|
78
61
|
}
|
|
79
62
|
|
|
80
|
-
|
|
81
|
-
if (
|
|
63
|
+
disconnectedCallback() {
|
|
64
|
+
if (this.#debounce) clearTimeout(this.#debounce);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
attributeChangedCallback(name, oldVal, newVal) {
|
|
68
|
+
if (oldVal === newVal) return;
|
|
69
|
+
const _ = this;
|
|
82
70
|
|
|
83
71
|
switch (name) {
|
|
84
72
|
case 'threshold':
|
|
85
|
-
|
|
86
|
-
|
|
73
|
+
_.#threshold = parseFloat(newVal) || 0;
|
|
74
|
+
_.#updateProgress();
|
|
87
75
|
break;
|
|
88
76
|
case 'current':
|
|
89
|
-
|
|
90
|
-
|
|
77
|
+
_.#current = parseFloat(newVal) || 0;
|
|
78
|
+
_.#updateProgress();
|
|
91
79
|
break;
|
|
92
80
|
case 'message-above':
|
|
93
|
-
|
|
94
|
-
|
|
81
|
+
_.#msgAbove = newVal || '';
|
|
82
|
+
_.#updateMessages();
|
|
95
83
|
break;
|
|
96
84
|
case 'message-below':
|
|
97
|
-
|
|
98
|
-
|
|
85
|
+
_.#msgBelow = newVal || '';
|
|
86
|
+
_.#updateMessages();
|
|
87
|
+
break;
|
|
88
|
+
case 'money-format':
|
|
89
|
+
_.#moneyFmt = newVal;
|
|
90
|
+
_.#updateMessages();
|
|
99
91
|
break;
|
|
100
92
|
}
|
|
101
93
|
}
|
|
102
94
|
|
|
103
95
|
#render() {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
//
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
this.#messageElement = document.createElement('p');
|
|
115
|
-
this.#messageElement.setAttribute('data-content-cart-progress-message', '');
|
|
116
|
-
this.appendChild(this.#messageElement);
|
|
96
|
+
const _ = this;
|
|
97
|
+
// Find existing elements or create them
|
|
98
|
+
_.#msgEl = _.querySelector('[data-content-cart-progress-message]');
|
|
99
|
+
_.#bar = _.querySelector('progress-bar');
|
|
100
|
+
|
|
101
|
+
// Create message element if needed
|
|
102
|
+
if (!_.#msgEl && (_.#msgAbove || _.#msgBelow)) {
|
|
103
|
+
_.#msgEl = document.createElement('p');
|
|
104
|
+
_.#msgEl.setAttribute('data-content-cart-progress-message', '');
|
|
105
|
+
_.appendChild(_.#msgEl);
|
|
117
106
|
}
|
|
118
107
|
|
|
119
|
-
// Create progress bar if
|
|
120
|
-
if (!
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
this.#progressBar = document.createElement('progress-bar');
|
|
124
|
-
this.appendChild(this.#progressBar);
|
|
125
|
-
} else {
|
|
126
|
-
// Fallback: wait for definition
|
|
127
|
-
customElements.whenDefined('progress-bar').then(() => {
|
|
128
|
-
if (!this.#progressBar) {
|
|
129
|
-
this.#progressBar = document.createElement('progress-bar');
|
|
130
|
-
this.appendChild(this.#progressBar);
|
|
131
|
-
this.#updateProgress(); // Update progress after creating
|
|
132
|
-
}
|
|
133
|
-
});
|
|
134
|
-
}
|
|
108
|
+
// Create progress bar if needed
|
|
109
|
+
if (!_.#bar) {
|
|
110
|
+
_.#bar = document.createElement('progress-bar');
|
|
111
|
+
_.appendChild(_.#bar);
|
|
135
112
|
}
|
|
136
113
|
}
|
|
137
114
|
|
|
138
115
|
#updateProgress() {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
// Update progress bar
|
|
146
|
-
if (this.#progressBar) {
|
|
147
|
-
this.#progressBar.setPercent(this.#progressPercent);
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
// Update messages
|
|
151
|
-
this.#updateMessages();
|
|
152
|
-
|
|
153
|
-
// Update component state
|
|
154
|
-
this.#updateComponentState();
|
|
116
|
+
const _ = this;
|
|
117
|
+
const converted = _.#converted();
|
|
118
|
+
_.#percent = converted === 0 ? 100 : Math.min(100, (_.#current / converted) * 100);
|
|
119
|
+
if (_.#bar) _.#bar.setPercent(_.#percent);
|
|
120
|
+
_.#updateState(converted);
|
|
155
121
|
}
|
|
156
122
|
|
|
157
123
|
#attachListeners() {
|
|
158
|
-
|
|
159
|
-
const
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
// Listen for cart data changes
|
|
163
|
-
cartDialog.addEventListener('cart-dialog:data-changed', (event) => {
|
|
164
|
-
this.#handleCartDataChange(event);
|
|
165
|
-
});
|
|
124
|
+
const _ = this;
|
|
125
|
+
const panel = _.closest('cart-panel');
|
|
126
|
+
if (panel) {
|
|
127
|
+
panel.addEventListener('cart-panel:data-changed', (e) => _.#onCartChange(e));
|
|
166
128
|
}
|
|
167
129
|
}
|
|
168
130
|
|
|
169
|
-
#
|
|
170
|
-
const
|
|
171
|
-
|
|
172
|
-
if (
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
if (
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
// Convert from cents to dollars if needed (Shopify typically returns cents)
|
|
182
|
-
currentAmount = updatedCart.total_price / 100;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
this.setCurrentAmount(currentAmount);
|
|
186
|
-
}
|
|
131
|
+
#onCartChange(event) {
|
|
132
|
+
const _ = this;
|
|
133
|
+
const cart = event.detail;
|
|
134
|
+
if (!cart) return;
|
|
135
|
+
|
|
136
|
+
if (_.#debounce) clearTimeout(_.#debounce);
|
|
137
|
+
_.#debounce = setTimeout(() => {
|
|
138
|
+
_.#debounce = null;
|
|
139
|
+
// Use calculated_subtotal if available, else total_price (both in cents)
|
|
140
|
+
const amt = (cart.calculated_subtotal ?? cart.total_price ?? 0) / 100;
|
|
141
|
+
_.setCurrentAmount(amt);
|
|
142
|
+
}, 100);
|
|
187
143
|
}
|
|
188
144
|
|
|
189
|
-
#
|
|
190
|
-
const
|
|
191
|
-
const
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
// Show success message when complete
|
|
202
|
-
messageTemplate = this.#originalAboveMessage;
|
|
203
|
-
} else if (!isComplete) {
|
|
204
|
-
// Show progress message when incomplete
|
|
205
|
-
messageTemplate = this.#originalBelowMessage || this.#originalAboveMessage;
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
if (messageTemplate) {
|
|
209
|
-
// Support multiple placeholder formats: {amount}, { amount }, [amount]
|
|
210
|
-
const message = messageTemplate
|
|
211
|
-
.replace(/\{\s*amount\s*\}/g, formattedAmount)
|
|
212
|
-
.replace(/\[\s*amount\s*\]/g, formattedAmount);
|
|
213
|
-
this.#messageElement.textContent = message;
|
|
214
|
-
this.#messageElement.style.display = 'block';
|
|
145
|
+
#updateState(converted) {
|
|
146
|
+
const _ = this;
|
|
147
|
+
const complete = _.#current >= converted;
|
|
148
|
+
const remaining = Math.max(0, converted - _.#current);
|
|
149
|
+
const formatted = _.#fmtMoney(remaining);
|
|
150
|
+
|
|
151
|
+
// Update message
|
|
152
|
+
if (_.#msgEl) {
|
|
153
|
+
let tpl = complete ? _.#msgAbove : (_.#msgBelow || _.#msgAbove);
|
|
154
|
+
if (tpl) {
|
|
155
|
+
_.#msgEl.textContent = tpl.replace(/\[\s*amount\s*\]/g, formatted);
|
|
156
|
+
_.#msgEl.style.display = 'block';
|
|
215
157
|
} else {
|
|
216
|
-
|
|
217
|
-
this.#messageElement.style.display = 'none';
|
|
158
|
+
_.#msgEl.style.display = 'none';
|
|
218
159
|
}
|
|
219
160
|
}
|
|
220
|
-
}
|
|
221
161
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
this.setAttribute('complete', isComplete.toString());
|
|
162
|
+
// Update complete attribute
|
|
163
|
+
_.setAttribute('complete', complete.toString());
|
|
225
164
|
}
|
|
226
165
|
|
|
166
|
+
#fmtMoney(amt) {
|
|
167
|
+
const fmt = this.#moneyFmt;
|
|
168
|
+
if (!fmt) return amt.toFixed(2).replace(/\.00$/, '');
|
|
227
169
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
setPercent(percent) {
|
|
233
|
-
const clampedPercent = Math.max(0, Math.min(100, percent));
|
|
234
|
-
this.#progressPercent = clampedPercent;
|
|
170
|
+
const fixed = amt.toFixed(2);
|
|
171
|
+
const noDecimals = Math.round(amt).toString();
|
|
172
|
+
const withComma = fixed.replace('.', ',');
|
|
173
|
+
const noDecWithComma = noDecimals.replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
|
235
174
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
175
|
+
return fmt
|
|
176
|
+
.replace(/\{\{\s*amount_no_decimals_with_comma_separator\s*\}\}/g, noDecWithComma)
|
|
177
|
+
.replace(/\{\{\s*amount_with_comma_separator\s*\}\}/g, withComma)
|
|
178
|
+
.replace(/\{\{\s*amount_no_decimals\s*\}\}/g, noDecimals)
|
|
179
|
+
.replace(/\{\{\s*amount\s*\}\}/g, fixed);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
#converted() {
|
|
183
|
+
const rate = parseFloat(window.Shopify?.currency?.rate) || 1;
|
|
184
|
+
return this.#threshold * rate;
|
|
185
|
+
}
|
|
239
186
|
|
|
240
|
-
|
|
241
|
-
this.#
|
|
242
|
-
|
|
187
|
+
#updateMessages() {
|
|
188
|
+
this.#updateState(this.#converted());
|
|
189
|
+
}
|
|
243
190
|
|
|
244
|
-
|
|
245
|
-
|
|
191
|
+
// Public API
|
|
192
|
+
setPercent(pct) {
|
|
193
|
+
const _ = this;
|
|
194
|
+
const p = Math.max(0, Math.min(100, pct));
|
|
195
|
+
_.#percent = p;
|
|
196
|
+
if (_.#bar) _.#bar.setPercent(p);
|
|
197
|
+
// Use converted threshold for multi-currency consistency
|
|
198
|
+
_.#current = (p / 100) * _.#converted();
|
|
199
|
+
_.setAttribute('current', _.#current.toString());
|
|
200
|
+
_.#updateState(_.#converted());
|
|
246
201
|
}
|
|
247
202
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
this.#currentAmount = parseFloat(amount) || 0;
|
|
254
|
-
this.setAttribute('current', this.#currentAmount.toString());
|
|
255
|
-
this.#updateProgress();
|
|
203
|
+
setCurrentAmount(amt) {
|
|
204
|
+
const _ = this;
|
|
205
|
+
_.#current = parseFloat(amt) || 0;
|
|
206
|
+
_.setAttribute('current', _.#current.toString());
|
|
207
|
+
_.#updateProgress();
|
|
256
208
|
}
|
|
257
209
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
this.#minAmount = parseFloat(amount) || 0;
|
|
264
|
-
this.setAttribute('threshold', this.#minAmount.toString());
|
|
265
|
-
this.#updateProgress();
|
|
210
|
+
setThresholdAmount(amt) {
|
|
211
|
+
const _ = this;
|
|
212
|
+
_.#threshold = parseFloat(amt) || 0;
|
|
213
|
+
_.setAttribute('threshold', _.#threshold.toString());
|
|
214
|
+
_.#updateProgress();
|
|
266
215
|
}
|
|
267
216
|
|
|
268
|
-
/**
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
* @deprecated Use setThresholdAmount instead
|
|
272
|
-
*/
|
|
273
|
-
setMinAmount(amount) {
|
|
274
|
-
this.setThresholdAmount(amount);
|
|
217
|
+
/** @deprecated Use setThresholdAmount */
|
|
218
|
+
setMinAmount(amt) {
|
|
219
|
+
this.setThresholdAmount(amt);
|
|
275
220
|
}
|
|
276
221
|
|
|
277
|
-
/**
|
|
278
|
-
* Public API: Get current progress information
|
|
279
|
-
*/
|
|
280
222
|
getProgress() {
|
|
223
|
+
const _ = this;
|
|
224
|
+
const converted = _.#converted();
|
|
281
225
|
return {
|
|
282
|
-
currentAmount:
|
|
283
|
-
thresholdAmount:
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
226
|
+
currentAmount: _.#current,
|
|
227
|
+
thresholdAmount: _.#threshold,
|
|
228
|
+
convertedThreshold: converted,
|
|
229
|
+
minAmount: _.#threshold,
|
|
230
|
+
remainingAmount: Math.max(0, converted - _.#current),
|
|
231
|
+
percent: _.#percent,
|
|
232
|
+
isComplete: _.#current >= converted,
|
|
233
|
+
currencyRate: parseFloat(window.Shopify?.currency?.rate) || 1,
|
|
288
234
|
};
|
|
289
235
|
}
|
|
290
236
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
setMessages(aboveMessage = null, belowMessage = null) {
|
|
297
|
-
if (aboveMessage !== null) {
|
|
298
|
-
this.#originalAboveMessage = aboveMessage;
|
|
299
|
-
this.setAttribute('message-above', aboveMessage);
|
|
237
|
+
setMessages(above = null, below = null) {
|
|
238
|
+
const _ = this;
|
|
239
|
+
if (above !== null) {
|
|
240
|
+
_.#msgAbove = above;
|
|
241
|
+
_.setAttribute('message-above', above);
|
|
300
242
|
}
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
this.setAttribute('message-below', belowMessage);
|
|
243
|
+
if (below !== null) {
|
|
244
|
+
_.#msgBelow = below;
|
|
245
|
+
_.setAttribute('message-below', below);
|
|
305
246
|
}
|
|
306
|
-
|
|
307
|
-
this.#updateMessages();
|
|
247
|
+
_.#updateMessages();
|
|
308
248
|
}
|
|
309
249
|
|
|
310
|
-
// Getters
|
|
311
|
-
get currentAmount() {
|
|
312
|
-
|
|
313
|
-
}
|
|
314
|
-
get
|
|
315
|
-
|
|
316
|
-
}
|
|
317
|
-
get minAmount() {
|
|
318
|
-
return this.#minAmount;
|
|
319
|
-
} // backwards compatibility
|
|
320
|
-
get percent() {
|
|
321
|
-
return this.#progressPercent;
|
|
322
|
-
}
|
|
323
|
-
get isComplete() {
|
|
324
|
-
return this.#currentAmount >= this.#minAmount;
|
|
325
|
-
}
|
|
250
|
+
// Getters (with backwards compatibility)
|
|
251
|
+
get currentAmount() { return this.#current; }
|
|
252
|
+
get thresholdAmount() { return this.#threshold; }
|
|
253
|
+
get minAmount() { return this.#threshold; }
|
|
254
|
+
get percent() { return this.#percent; }
|
|
255
|
+
get isComplete() { return this.#current >= this.#converted(); }
|
|
326
256
|
}
|
|
327
257
|
|
|
328
258
|
// Define CartProgressBar custom element
|