Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@
},
"globals": {
"ms3": "readonly",
"ms3Config": "readonly"
"ms3Config": "readonly",
"getSelectors": "readonly"
},
"ignorePatterns": ["**/node_modules/**", "**/mgr/**"]
"ignorePatterns": ["**/node_modules/**", "**/mgr/**"],
"rules": {
"no-unused-vars": ["error", { "varsIgnorePattern": "^(CartUI|CustomerUI|OrderUI|ProductCardUI|QuantityUI)$" }]
}
}
1 change: 1 addition & 0 deletions _build/elements/settings.php
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,7 @@
"[[+jsUrl]]web\/lib\/izitoast\/iziToast.js",
"[[+jsUrl]]web\/modules\/hooks.js",
"[[+jsUrl]]web\/modules\/message.js",
"[[+jsUrl]]web\/core\/Selectors.js",
"[[+jsUrl]]web\/core\/ApiClient.js",
"[[+jsUrl]]web\/core\/TokenManager.js",
"[[+jsUrl]]web\/core\/CartAPI.js",
Expand Down
36 changes: 36 additions & 0 deletions assets/components/minishop3/js/web/core/Selectors.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/**
* Default selectors for MiniShop3 UI
* Overridable via ms3Config.selectors
* Priority: data-* attributes, fallback: CSS classes (backward compatibility)
*
* @see https://github.com/modx-pro/MiniShop3/issues/18
*/
const defaultSelectors = {
form: '[data-ms3-form], .ms3_form',
formOrder: '[data-ms3-form="order"], .ms3_order_form',
formCustomer: '[data-ms3-form="customer"], .ms3_customer_form',
cartOptions: '[data-ms3-cart-options], .ms3_cart_options',
qtyInput: '[data-ms3-qty="input"], .qty-input',
qtyInc: '[data-ms3-qty="inc"], .inc-qty',
qtyDec: '[data-ms3-qty="dec"], .dec-qty',
productCard: '[data-ms3-product-card], .ms3-product-card',
fieldError: '[data-ms3-error], .ms3_field_error',
orderCost: '#ms3_order_cost',
orderCartCost: '#ms3_order_cart_cost',
orderDeliveryCost: '#ms3_order_delivery_cost',
link: '.ms3_link'
}

/**
* Get merged selectors (defaults + ms3Config.selectors overrides)
* @returns {Object} Selectors object
*/
function getSelectors () {
const custom = (typeof window !== 'undefined' && window.ms3Config && window.ms3Config.selectors) || {}
return { ...defaultSelectors, ...custom }
}

// Expose for fallback when Selectors.js loads but getSelectors fails (e.g. in unit tests)
if (typeof window !== 'undefined') {
window.Ms3DefaultSelectors = defaultSelectors
}
48 changes: 39 additions & 9 deletions assets/components/minishop3/js/web/ms3.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,14 @@
* - data-ms3-qty="input"|"inc"|"dec" — quantity control
* - data-ms3-cart-options — cart options select
*/
/* global TokenManager, ApiClient, CartAPI, OrderAPI, CustomerAPI, CartUI, OrderUI, CustomerUI, ProductCardUI */
/* global TokenManager, ApiClient, CartAPI, OrderAPI, CustomerAPI, CartUI, OrderUI, CustomerUI, QuantityUI, ProductCardUI, getSelectors */
const ms3 = {
config: {},

get selectors () {
return this.config?.selectors || {}
},

tokenManager: null,
apiClient: null,

Expand All @@ -48,6 +52,27 @@ const ms3 = {
async init () {
this.config = window.ms3Config || {}

// Merge selectors from Selectors.js (overridable via ms3Config.selectors)
const rawSelectors = typeof getSelectors === 'function'
? getSelectors()
: (window.Ms3DefaultSelectors || {})
const selectorDefaults = window.Ms3DefaultSelectors || {
form: '[data-ms3-form], .ms3_form',
formOrder: '[data-ms3-form="order"], .ms3_order_form',
formCustomer: '[data-ms3-form="customer"], .ms3_customer_form',
cartOptions: '[data-ms3-cart-options], .ms3_cart_options',
qtyInput: '[data-ms3-qty="input"], .qty-input',
qtyInc: '[data-ms3-qty="inc"], .inc-qty',
qtyDec: '[data-ms3-qty="dec"], .dec-qty',
productCard: '[data-ms3-product-card], .ms3-product-card',
fieldError: '[data-ms3-error], .ms3_field_error',
orderCost: '#ms3_order_cost',
orderCartCost: '#ms3_order_cart_cost',
orderDeliveryCost: '#ms3_order_delivery_cost',
link: '.ms3_link'
}
this.config = { ...this.config, selectors: { ...selectorDefaults, ...rawSelectors } }

this.hooks = window.ms3Hooks || this.createFallbackHooks()
this.message = window.ms3Message || this.createFallbackMessage()

Expand Down Expand Up @@ -95,17 +120,19 @@ const ms3 = {
},

/**
* [data-ms3-form] submit handler (fallback: .ms3_form deprecated)
* Form submit handler (uses sel.form from config)
*
* Automatically calls appropriate API method based on ms3_action:
* - cart/add → cartUI.handleAdd()
* - order/submit → orderUI.handleSubmit()
* - etc.
*/
initFormHandler () {
const formSelector = this.selectors.form

document.addEventListener('submit', async (event) => {
const form = event.target
if (!form.hasAttribute('data-ms3-form') && !form.classList.contains('ms3_form')) {
if (!form.matches(formSelector)) {
return
}

Expand All @@ -126,21 +153,24 @@ const ms3 = {
},

/**
* .ms3_link click handler
* Link click handler (uses sel.link, sel.form from config)
*
* Handles clicks on buttons/links with .ms3_link class
* inside .ms3_form forms. Triggers form submit.
* Handles clicks on buttons/links with sel.link
* inside forms matching sel.form. Triggers form submit.
*/
initLinkHandler () {
const linkSelector = this.selectors.link
const formSelector = this.selectors.form

document.addEventListener('click', async (event) => {
const link = event.target.closest('.ms3_link')
const link = event.target.closest(linkSelector)
if (!link) {
return
}

const form = link.closest('[data-ms3-form]') || link.closest('.ms3_form')
const form = link.closest(formSelector)
if (!form) {
console.warn('.ms3_link must be inside a form with data-ms3-form or .ms3_form')
console.warn(`ms3_link must be inside a form matching: ${formSelector}`)
return
}

Expand Down
22 changes: 9 additions & 13 deletions assets/components/minishop3/js/web/ui/CartUI.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ class CartUI {
this.config = config
}

get selectors () {
return this.config?.selectors || {}
}

/**
* Initialize UI handlers
*/
Expand All @@ -40,22 +44,14 @@ class CartUI {
}

/**
* Product option selects: [data-ms3-cart-options] or .ms3_cart_options (fallback)
* Product option selects: uses sel.cartOptions from config
*/
initOptionSelects () {
const byData = document.querySelectorAll('[data-ms3-cart-options]')
const byClass = document.querySelectorAll('.ms3_cart_options')
const seen = new Set()
const selects = []
;[...byData, ...byClass].forEach(select => {
if (!seen.has(select)) {
seen.add(select)
selects.push(select)
}
})
selects.forEach(select => {
const { cartOptions: cartOptionsSelector, form: formSelector } = this.selectors

document.querySelectorAll(cartOptionsSelector).forEach(select => {
select.addEventListener('change', async (e) => {
const form = e.target.closest('[data-ms3-form]') || e.target.closest('.ms3_form')
const form = e.target.closest(formSelector)
if (!form) return

console.log('Option changed:', e.target.name, e.target.value)
Expand Down
8 changes: 6 additions & 2 deletions assets/components/minishop3/js/web/ui/CustomerUI.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ class CustomerUI {
this.config = config
}

get selectors () {
return this.config?.selectors || {}
}

/**
* Get lexicon string (window.ms3Lexicon, then fallback, then key)
* @param {string} key - Lexicon key
Expand All @@ -47,7 +51,7 @@ class CustomerUI {
* Initialize UI handlers
*/
init () {
document.querySelectorAll('[data-ms3-form="customer"], .ms3_customer_form').forEach(form => {
document.querySelectorAll(this.selectors.formCustomer).forEach(form => {
this.initForm(form)
})
}
Expand All @@ -72,7 +76,7 @@ class CustomerUI {
*/
initInput (input) {
input.addEventListener('change', async () => {
const form = input.closest('[data-ms3-form="customer"], .ms3_customer_form')
const form = input.closest(this.selectors.formCustomer)
if (!form) return

const parent = input.closest('div')
Expand Down
37 changes: 20 additions & 17 deletions assets/components/minishop3/js/web/ui/OrderUI.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,15 @@ class OrderUI {
this.config = config
}

get selectors () {
return this.config?.selectors || {}
}

/**
* Initialize UI handlers
*/
init () {
document.querySelectorAll('[data-ms3-form="order"], .ms3_order_form').forEach(form => {
document.querySelectorAll(this.selectors.formOrder).forEach(form => {
this.initForm(form)
})

Expand Down Expand Up @@ -157,23 +161,22 @@ class OrderUI {

if (response.success && response.data) {
const { cost, cart_cost: cartCost, delivery_cost: deliveryCost } = response.data

// Update cart cost
const cartCostEl = document.getElementById('ms3_order_cart_cost')
if (cartCostEl && cartCost !== undefined) {
cartCostEl.textContent = this.formatPrice(cartCost)
const cartCostElement = document.querySelector(this.selectors.orderCartCost)
if (cartCostElement && cartCost !== undefined) {
cartCostElement.textContent = this.formatPrice(cartCost)
}

// Update delivery cost
const deliveryCostEl = document.getElementById('ms3_order_delivery_cost')
if (deliveryCostEl && deliveryCost !== undefined) {
deliveryCostEl.textContent = this.formatPrice(deliveryCost)
const deliveryCostElement = document.querySelector(this.selectors.orderDeliveryCost)
if (deliveryCostElement && deliveryCost !== undefined) {
deliveryCostElement.textContent = this.formatPrice(deliveryCost)
}

// Update total cost
const totalCostEl = document.getElementById('ms3_order_cost')
if (totalCostEl && cost !== undefined) {
totalCostEl.textContent = this.formatPrice(cost)
const totalCostElement = document.querySelector(this.selectors.orderCost)
if (totalCostElement && cost !== undefined) {
totalCostElement.textContent = this.formatPrice(cost)
}

await this.hooks.runHooks('afterUpdateOrderCosts', { cost, cart_cost: cartCost, delivery_cost: deliveryCost })
Expand Down Expand Up @@ -264,7 +267,7 @@ class OrderUI {
}

if (response.success) {
document.querySelectorAll('[data-ms3-form="order"], .ms3_order_form').forEach(form => {
document.querySelectorAll(this.selectors.formOrder).forEach(form => {
form.reset()
})
document.dispatchEvent(new CustomEvent('ms3:cart:updated'))
Expand All @@ -283,19 +286,19 @@ class OrderUI {
* @param {Array<string>} errors - Array of field names with errors
*/
highlightErrors (errors) {
document.querySelectorAll('[data-ms3-error], .ms3_field_error').forEach(el => {
el.removeAttribute('data-ms3-error')
el.classList.remove('ms3_field_error')
document.querySelectorAll(this.selectors.fieldError).forEach(element => {
element.removeAttribute('data-ms3-error')
element.classList.remove('ms3_field_error')
})

errors.forEach(fieldName => {
const selectors = [
const selectorList = [
`[name="${fieldName}"]`,
`[name="address_${fieldName}"]`,
`[name="order_${fieldName}"]`
]

selectors.forEach(selector => {
selectorList.forEach(selector => {
const field = document.querySelector(selector)
if (field) {
field.setAttribute('data-ms3-error', '')
Expand Down
8 changes: 6 additions & 2 deletions assets/components/minishop3/js/web/ui/ProductCardUI.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ class ProductCardUI {
this.cartState = {}
}

get selectors () {
return this.config?.selectors || {}
}

/**
* Initialize product card UI
*/
Expand Down Expand Up @@ -127,7 +131,7 @@ class ProductCardUI {
* Update all product cards on page
*/
updateAllCards () {
const cards = document.querySelectorAll('[data-ms3-product-card], .ms3-product-card')
const cards = document.querySelectorAll(this.selectors.productCard)

cards.forEach(card => {
this.updateCard(card)
Expand Down Expand Up @@ -161,7 +165,7 @@ class ProductCardUI {
changeForm.style.display = 'flex'

// Update quantity input
const countInput = changeForm.querySelector('[data-ms3-qty="input"]') || changeForm.querySelector('.qty-input')
const countInput = changeForm.querySelector(this.selectors.qtyInput)
if (countInput) {
countInput.value = this.cartState[productId].totalCount
}
Expand Down
Loading