1
1
import { UCC_CART_CONTEXT } from "../contexts/ucc.context.ts" ;
2
2
import { CartConfig , CartItem } from "../types.ts" ;
3
3
import { UccModalElement } from "./ucc-modal.element.ts" ;
4
- import { debounce , delegate , getUniqueSelector } from "../utils.ts" ;
4
+ import { debounce , delegate , difference } from "../utils.ts" ;
5
5
import { UccCartRepository } from "../repositories/cart.respository.ts" ;
6
6
import { UccEvent } from "../events/ucc.event.ts" ;
7
+ import { localize } from "../localization.ts" ;
7
8
8
9
export class UccCartModalElement extends UccModalElement
9
10
{
@@ -29,20 +30,34 @@ export class UccCartModalElement extends UccModalElement
29
30
this . _context . config . subscribe ( ( config : CartConfig ) => {
30
31
if ( config ) {
31
32
32
- this . setTitle ( config . locales ! [ config . lang ] . cart_title ) ;
33
- this . setCloseButtonLabel ( config . locales ! [ config . lang ] . close_cart ) ;
33
+ // Localize UI
34
+
35
+ this . setTitle ( localize ( 'cart_title' ) ) ;
36
+ this . setCloseButtonLabel ( localize ( 'close_cart' ) ) ;
34
37
35
- this . _host . querySelector < HTMLElement > ( '.ucc-cart-total--subtotal .ucc-cart-total-label' ) ! . textContent = config . locales ! [ config . lang ] . subtotal ;
36
- this . _host . querySelector < HTMLElement > ( '.ucc-cart-total--taxes .ucc-cart-total-label' ) ! . textContent = config . locales ! [ config . lang ] . taxes ;
37
- this . _host . querySelector < HTMLElement > ( '.ucc-cart-message' ) ! . textContent = config . locales ! [ config . lang ] . shipping_and_discounts_message ;
38
- this . _host . querySelector < HTMLElement > ( '.ucc-cart-total--total .ucc-cart-total-label' ) ! . textContent = config . locales ! [ config . lang ] . total ;
39
- this . _host . querySelector < HTMLElement > ( '.ucc-cart-checkout' ) ! . textContent = config . locales ! [ config . lang ] . checkout ;
38
+ this . _host . querySelector < HTMLElement > ( '.ucc-cart-empty__message' ) ! . textContent = localize ( 'cart_empty' ) ;
39
+ this . _host . querySelector < HTMLElement > ( '.ucc-cart-total--subtotal .ucc-cart-total-label' ) ! . textContent = localize ( 'subtotal' ) ;
40
+ this . _host . querySelector < HTMLElement > ( '.ucc-cart-total--taxes .ucc-cart-total-label' ) ! . textContent = localize ( 'taxes' ) ;
41
+ this . _host . querySelector < HTMLElement > ( '.ucc-cart-total--total .ucc-cart-total-label' ) ! . textContent = localize ( 'total' ) ;
42
+ this . _host . querySelector < HTMLElement > ( '.ucc-cart-message' ) ! . textContent = localize ( 'shipping_and_discounts_message' ) ;
43
+ this . _host . querySelector < HTMLElement > ( '.ucc-cart-checkout' ) ! . textContent = localize ( 'checkout' ) ;
40
44
this . _host . querySelector < HTMLElement > ( '.ucc-cart-checkout' ) ! . setAttribute ( 'href' , config . checkoutUrl ! ) ;
41
45
42
- const cartEmptyMsg = this . _host . querySelector < HTMLElement > ( '.ucc-cart-empty__message' ) ;
43
- if ( cartEmptyMsg ) {
44
- cartEmptyMsg . textContent = config . locales ! [ config . lang ] . cart_empty ;
45
- }
46
+ Array . from ( this . _host . querySelectorAll < HTMLElement > ( '.ucc-cart-item' ) ) . forEach ( ( el ) => {
47
+
48
+ el . querySelector < HTMLElement > ( '.ucc-cart-item__remove' ) ! . setAttribute ( 'title' , localize ( 'remove' ) ) ;
49
+
50
+ const basePriceEl = el . querySelector < HTMLElement > ( '.ucc-cart-item__bundle-item--base .ucc-cart-item__bundle-item-name' ) ;
51
+ if ( basePriceEl ) {
52
+ basePriceEl . textContent = localize ( 'base_price' ) ;
53
+ }
54
+
55
+ Array . from ( el . querySelectorAll < HTMLElement > ( '.ucc-cart-item__property' ) ) . forEach ( ( propertyEl ) => {
56
+ const key = propertyEl . dataset . propertyKey ;
57
+ propertyEl . querySelector < HTMLElement > ( '.ucc-cart-item__property-key' ) ! . textContent = localize ( `property_${ key ! . toLowerCase ( ) } ` ) ;
58
+ } ) ;
59
+
60
+ } ) ;
46
61
47
62
if ( config . showPricesIncludingTax ) {
48
63
this . _host . querySelector < HTMLElement > ( '.ucc-cart-totals' ) ! . classList . add ( 'ucc-cart-totals--inc-tax' ) ;
@@ -65,49 +80,80 @@ export class UccCartModalElement extends UccModalElement
65
80
66
81
const cart = this . _context . cart . get ( ) ;
67
82
if ( cart && cart . items . length > 0 ) {
68
- const activeEl = this . _host . ownerDocument . activeElement as HTMLElement ;
69
- const activeElSelector = activeEl ? getUniqueSelector ( activeEl ) : undefined ;
70
- const activeElCaretPos = activeEl instanceof HTMLInputElement ? activeEl . selectionStart : undefined ;
71
- this . _host . querySelector ( '.ucc-cart-items' ) ! . innerHTML = cart . items . map ( ( item ) => {
72
- const propsToDisplay = Object . keys ( item . properties ?? { } ) . filter ( ( x ) => ( config . properties ?? [ ] ) . map ( y => y . toLowerCase ( ) ) . includes ( x . toLowerCase ( ) ) ) ;
73
- return `
74
- <div class="ucc-cart-item" data-id="${ item . id } ">
75
- ${ item . imageUrl ? `
76
- <figure class="ucc-cart-item__image">
77
- <img src="${ item . imageUrl } " alt="${ item . name } ">
78
- </figure>` : '' }
79
- <div class="ucc-cart-item__body">
80
- <div class="ucc-cart-item__content ucc-split">
81
- <div class="ucc-split__left ucc-split__item--fill">
82
- <h3 class="ucc-cart-item__title">${ item . name } </h3>
83
- ${ item . attributes ? `
84
- <div class="ucc-cart-item__attributes">
85
- ${ Object . keys ( item . attributes ) . map ( ( key ) => `
86
- <div class="ucc-cart-item__attribute">
87
- <span class="ucc-cart-item__attribute-key">${ key } </span>
88
- <span class="ucc-cart-item__attribute-value">${ item . attributes ! [ key ] } </span>
89
- </div>
90
- ` ) . join ( '' ) }
91
- </div>
92
- ` : '' }
93
- ${ propsToDisplay . length > 0 ? `
94
- <div class="ucc-cart-item__properties">
95
- ${ propsToDisplay . map ( ( key ) => `
96
- <div class="ucc-cart-item__property">
97
- <span class="ucc-cart-item__property-key">${ config ?. locales ! [ config . lang ] [ `property_${ key . toLowerCase ( ) } ` ] ?? key } </span>
98
- <span class="ucc-cart-item__property-value">${ item . properties ! [ key ] } </span>
99
- </div>
100
- ` ) . join ( '' ) }
101
- </div>
102
- ` : '' }
103
- ${ item . items && item . items . length ? `
104
- <div class="ucc-cart-item__bundle">
105
- <div class="ucc-cart-item__bundle-item ucc-cart-item__bundle-item--base">
106
- <span class="ucc-cart-item__bundle-item-name">${ config ?. locales ! [ config . lang ] . base_price ?? 'Base price' } </span>
107
- <span class="ucc-cart-item__bundle-item-quantity"></span>
108
- <span class="ucc-cart-item__bundle-item-quantity">${ config . showPricesIncludingTax ? item . basePrice . withTax : item . basePrice . withoutTax } </span>
83
+
84
+ // Compare cart items with DOM elements to determine what items have been added, removed or updated
85
+ const domIds = Array . from ( this . _host . querySelectorAll < HTMLElement > ( '.ucc-cart-item' ) ) . map ( ( el ) => el . dataset . id ) ;
86
+ const cartIds = cart . items . map ( ( item ) => item . id ) ;
87
+ const { added, removed, intersects } = difference ( cartIds , domIds ) ;
88
+
89
+ // Remove items that are no longer in the cart
90
+ removed . forEach ( ( id ) => {
91
+ this . _host . querySelector < HTMLElement > ( `.ucc-cart-item[data-id="${ id } "]` ) ?. remove ( ) ;
92
+ } ) ;
93
+
94
+ // Update existing items
95
+ intersects . forEach ( ( id ) =>
96
+ {
97
+ const item = cart . items . find ( ( item ) => item . id === id ) ! ;
98
+ const el = this . _host . querySelector < HTMLElement > ( `.ucc-cart-item[data-id="${ id } "]` ) ! ;
99
+
100
+ // NB: We assume that only quantity and price can change
101
+
102
+ const qtyEl = el . querySelector < HTMLInputElement > ( '.ucc-cart-item__quantity' ) ! ;
103
+ if ( parseFloat ( qtyEl . value ) !== item . quantity ) {
104
+ qtyEl . value = item . quantity . toString ( ) ;
105
+ }
106
+
107
+ const newPrice = config . showPricesIncludingTax ? item . total . withTax : item . total . withoutTax ;
108
+ const priceEl = el . querySelector < HTMLElement > ( '.ucc-cart-item__price' ) ! ;
109
+ if ( priceEl . textContent !== newPrice ) {
110
+ priceEl . textContent = newPrice ;
111
+ }
112
+ } ) ;
113
+
114
+ // Add new items to the cart
115
+ added . forEach ( ( id ) => {
116
+
117
+ const item = cart . items . find ( ( item ) => item . id === id ) ! ;
118
+
119
+ this . _host . querySelector ( '.ucc-cart-items' ) ! . insertAdjacentHTML ( 'beforeend' , `
120
+ <div class="ucc-cart-item" data-id="${ item . id } ">
121
+ ${ item . imageUrl ? `
122
+ <figure class="ucc-cart-item__image">
123
+ <img src="${ item . imageUrl } " alt="${ item . name } ">
124
+ </figure>` : '' }
125
+ <div class="ucc-cart-item__body">
126
+ <div class="ucc-cart-item__content ucc-split">
127
+ <div class="ucc-split__left ucc-split__item--fill">
128
+ <h3 class="ucc-cart-item__title">${ item . name } </h3>
129
+ ${ item . attributes ? `
130
+ <div class="ucc-cart-item__attributes">
131
+ ${ Object . keys ( item . attributes ) . map ( ( key ) => `
132
+ <div class="ucc-cart-item__attribute">
133
+ <span class="ucc-cart-item__attribute-key">${ key } </span>
134
+ <span class="ucc-cart-item__attribute-value">${ item . attributes ! [ key ] } </span>
135
+ </div>
136
+ ` ) . join ( '' ) }
137
+ </div>
138
+ ` : '' }
139
+ ${ item . properties ? `
140
+ <div class="ucc-cart-item__properties">
141
+ ${ Object . keys ( item . properties ) . map ( ( key ) => `
142
+ <div class="ucc-cart-item__property" data-property-key="${ key } ">
143
+ <span class="ucc-cart-item__property-key">${ localize ( `property_${ key . toLowerCase ( ) } ` ) } </span>
144
+ <span class="ucc-cart-item__property-value">${ item . properties ! [ key ] } </span>
109
145
</div>
110
- ${ item . items . map ( ( bundleItem ) => `
146
+ ` ) . join ( '' ) }
147
+ </div>
148
+ ` : '' }
149
+ ${ item . items && item . items . length ? `
150
+ <div class="ucc-cart-item__bundle">
151
+ <div class="ucc-cart-item__bundle-item ucc-cart-item__bundle-item--base">
152
+ <span class="ucc-cart-item__bundle-item-name">${ localize ( 'base_price' ) } </span>
153
+ <span class="ucc-cart-item__bundle-item-quantity"></span>
154
+ <span class="ucc-cart-item__bundle-item-quantity">${ config . showPricesIncludingTax ? item . basePrice . withTax : item . basePrice . withoutTax } </span>
155
+ </div>
156
+ ${ item . items . map ( ( bundleItem ) => `
111
157
<div class="ucc-cart-item__bundle-item">
112
158
<span class="ucc-cart-item__bundle-item-name">${ bundleItem . name } </span>
113
159
<span class="ucc-cart-item__bundle-item-quantity">x${ bundleItem . quantity } </span>
@@ -118,7 +164,7 @@ export class UccCartModalElement extends UccModalElement
118
164
` : '' }
119
165
</div>
120
166
<div class="ucc-split__right">
121
- <button class="ucc-cart-item__remove" title="${ config ?. locales ! [ config . lang ] . remove ?? 'Remove' } "><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-trash-2"><path d="M3 6h18"/><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/><line x1="10" x2="10" y1="11" y2="17"/><line x1="14" x2="14" y1="11" y2="17"/></svg></button>
167
+ <button class="ucc-cart-item__remove" title="${ localize ( ' remove' ) } "><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-trash-2"><path d="M3 6h18"/><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/><line x1="10" x2="10" y1="11" y2="17"/><line x1="14" x2="14" y1="11" y2="17"/></svg></button>
122
168
</div>
123
169
</div>
124
170
<div class="ucc-cart-item__foot ucc-split ucc-split--center">
@@ -131,34 +177,37 @@ export class UccCartModalElement extends UccModalElement
131
177
</div>
132
178
</div>
133
179
</div>
134
- `
135
- } ) . join ( '' ) ;
136
- this . _host . querySelector < HTMLElement > ( '.ucc-cart-totals' ) ! . style . display = '' ;
137
- this . _host . querySelector < HTMLElement > ( '.ucc-cart-message' ) ! . style . display = '' ;
180
+ ` ) ;
181
+
182
+ } ) ;
183
+
184
+ // Toggle cart items and empty message
185
+ this . _host . querySelector < HTMLElement > ( '.ucc-cart-items' ) ! . style . display = '' ;
186
+ this . _host . querySelector < HTMLElement > ( '.ucc-cart-empty' ) ! . style . display = 'none' ;
187
+
188
+ // Populate totals
138
189
this . _host . querySelector < HTMLElement > ( '.ucc-cart-total--subtotal .ucc-cart-total-value' ) ! . textContent = cart . subtotal . withoutTax ;
139
190
this . _host . querySelector < HTMLElement > ( '.ucc-cart-total--taxes .ucc-cart-total-value' ) ! . textContent = cart . subtotal . tax ;
140
191
this . _host . querySelector < HTMLElement > ( '.ucc-cart-total--total .ucc-cart-total-value' ) ! . textContent = cart . subtotal . withTax ;
192
+
193
+ // Show totals
194
+ this . _host . querySelector < HTMLElement > ( '.ucc-cart-totals' ) ! . style . display = '' ;
195
+ this . _host . querySelector < HTMLElement > ( '.ucc-cart-message' ) ! . style . display = '' ;
196
+
197
+ // Enable checkout button
141
198
this . _host . querySelector < HTMLElement > ( '.ucc-cart-checkout' ) ! . classList . remove ( 'ucc-cart-checkout--disabled' ) ;
142
- if ( activeElSelector ) {
143
- const activeEl = this . _host . querySelector < HTMLElement > ( activeElSelector ) ;
144
- if ( activeEl ) {
145
- activeEl . focus ( ) ;
146
- if ( activeEl instanceof HTMLInputElement && activeElCaretPos ) {
147
- activeEl . setSelectionRange ( activeElCaretPos ! , activeElCaretPos ! ) ;
148
- }
149
- }
150
- }
199
+
151
200
} else {
152
- this . _host . querySelector < HTMLElement > ( '.ucc-cart-items' ) ! . innerHTML = `
153
- <div class="ucc-cart-empty">
154
- <div>
155
- <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-shopping-cart"><circle cx="8" cy="21" r="1"/><circle cx="19" cy="21" r="1"/><path d="M2.05 2.05h2l2.66 12.42a2 2 0 0 0 2 1.58h9.78a2 2 0 0 0 1.95-1.57l1.65-7.43H5.12"/></svg>
156
- <div class="ucc-cart-empty__message">${ config ?. locales ! [ config . lang ] . cart_empty ?? 'Your cart is empty' } </div>
157
- </div>
158
- </div>
159
- ` ;
201
+
202
+ // Toggle cart items and empty message
203
+ this . _host . querySelector < HTMLElement > ( '.ucc-cart-items' ) ! . style . display = 'none' ;
204
+ this . _host . querySelector < HTMLElement > ( '.ucc-cart-empty' ) ! . style . display = '' ;
205
+
206
+ // Hide totals
160
207
this . _host . querySelector < HTMLElement > ( '.ucc-cart-totals' ) ! . style . display = 'none' ;
161
208
this . _host . querySelector < HTMLElement > ( '.ucc-cart-message' ) ! . style . display = 'none' ;
209
+
210
+ // Disable checkout button
162
211
this . _host . querySelector < HTMLElement > ( '.ucc-cart-checkout' ) ! . classList . add ( 'ucc-cart-checkout--disabled' ) ;
163
212
}
164
213
}
@@ -175,6 +224,12 @@ export class UccCartModalElement extends UccModalElement
175
224
private _attachCartTemplate ( ) {
176
225
this . setBody ( `
177
226
<div class="ucc-cart-items"></div>
227
+ <div class="ucc-cart-empty" style="display: none;">
228
+ <div>
229
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-shopping-cart"><circle cx="8" cy="21" r="1"/><circle cx="19" cy="21" r="1"/><path d="M2.05 2.05h2l2.66 12.42a2 2 0 0 0 2 1.58h9.78a2 2 0 0 0 1.95-1.57l1.65-7.43H5.12"/></svg>
230
+ <div class="ucc-cart-empty__message"></div>
231
+ </div>
232
+ </div>
178
233
` )
179
234
this . setFooter ( `
180
235
<div class="ucc-cart-totals">
0 commit comments