$html .= '<td style="white-space:nowrap;">'
. '<span class="vmicon vmicon-16-move"
style="cursor:move;font-size:20px;color:#666;vertical-align:middle;margin:0 6px 0 5px;"
title="Reorder">⋮⋮</span>'
. JHTML::_('link', ...)
. '<input type="hidden" class="vmchild-ordering"
name="childs[' . $child->virtuemart_product_id . '][pordering]"
value="' . (int)($child->pordering ?? 0) . '" />'
. '</td>';
if (isset($childIds[$product_id])) {
$sorted = self::sortChildIds($product_id, $childIds[$product_id], $field->options);
$allIds = array_merge([$product_id], array_values($childIds[$product_id]));
$db = JFactory::getDBO();
$db->setQuery('SELECT virtuemart_product_id, pordering FROM #__virtuemart_products'
. ' WHERE virtuemart_product_id IN (' . implode(',', array_map('intval', $allIds)) . ')');
$porderingMap = $db->loadAssocList('virtuemart_product_id', 'pordering');
$sorted[] = ['parent_id' => $product_id, 'vm_product_id' => $product_id]; // parent included
usort($sorted, function ($a, $b) use ($porderingMap) {
$posA = isset($porderingMap[$a['vm_product_id']]) ? (int)$porderingMap[$a['vm_product_id']] : 999;
$posB = isset($porderingMap[$b['vm_product_id']]) ? (int)$porderingMap[$b['vm_product_id']] : 999;
return $posA - $posB;
});
} else {
$sorted[] = ['parent_id' => $product_id, 'vm_product_id' => $product_id];
}
// array_unshift(...) removed - parent is now part of the sorted array
jQuery(document).ready(function ($) {
$('#syncro').sortable({
cursorAt: { top: 0, left: 0 },
handle: '.vmicon-16-move',
update: function (event, ui) {
$(this).find('.vmchild-ordering').each(function (index, el) {
$(el).val(index);
});
}
});
$('#syncro .vmchild-ordering').each(function (index, el) {
$(el).val(index);
});
});
} else {
// Parent variant: only update pordering if explicitly submitted
if (isset($child['pordering'])) {
$db = JFactory::getDBO();
$db->setQuery('UPDATE #__virtuemart_products SET pordering = '
. (int)$child['pordering']
. ' WHERE virtuemart_product_id = ' . (int)$productId);
$db->execute();
}
}
<div class="chosen-container chosen-container-multi" id="categories_chosen" style="width: 0px;"> Virtuemart.loadCategoryTree() (in components/com_virtuemart/assets/js/ajax_catree.js) fires an AJAX request during document.ready. On AJAX success, it calls:jQuery('select#' + id).chosen({ select_some_options_text: Virtuemart.selectSomeCategory });
// no width option passed
<select> element at the moment of initialization. If the AJAX response arrives while the "Product Information" <li> is hidden by UIkit's switcher (display: none), the measured width is 0. Chosen then stamps style="width: 0px;" on its container, and nothing corrects it when the tab becomes visible again.Virtuemart.updateChosenDropdownLayout() (generated by vmjsapi.php) passes width: '100%' to .chosen(), which avoids the problem. But the categories field carries the class vm-chzn-add, which intentionally excludes it from that initializer (select:not(.vm-chzn-add)). So the categories select is always initialized through the AJAX path, without a width fallback.width: '100%' to the .chosen() call so the container always gets a percentage-based width instead of a pixel-measured one.// Before (line ~87):
jQuery('select#'+id).chosen({select_some_options_text: Virtuemart.selectSomeCategory});
// After:
jQuery('select#'+id).chosen({select_some_options_text: Virtuemart.selectSomeCategory, width: '100%'});
show with bubbles: true on each <li> of the switcher when it becomes active (confirmed in uikit.js - createEvent(e, bubbles = true)). Adding a listener on the tabs container catches any other Chosen widget that may have suffered the same fate:jQuery(document).ready(function($) {
var tabsContainer = document.getElementById('vmuikit-admin-ui-tabs');
if (!tabsContainer) { return; }
tabsContainer.addEventListener('show', function(e) {
$(e.target).find('.chosen-container').each(function() {
if (this.style.width === '0px') {
$(this).css('width', '100%');
}
});
});
});
.chosen-container with width: 0px means the .each() body never runs).administrator/templates/vmadmin/html/com_virtuemart/product/product_edit_information.php via vmJsApi::addJScript().// In <thead> - after the price column header:
<th style="text-align: left !important;"><?php echo vmText::_('COM_VIRTUEMART_PRODUCT_DISCOUNT')?></th>
// In each <tr> child row - after the price <td>:
<td><?php echo $this->renderDiscountList(
isset($child->allPrices[$child->selectedPrice]['product_discount_id'])
? (int)$child->allPrices[$child->selectedPrice]['product_discount_id']
: -1,
'childs['.$child->virtuemart_product_id.'][mprices][product_discount_id][]'
) ?></td>// In thead - after COM_VIRTUEMART_PRODUCT_FORM_PRICE_COST:
$html .= '<th style="text-align: left !important;width:80px;">'.vmText::_('COM_VIRTUEMART_PRODUCT_DISCOUNT').'</th>';
// In each child row - after the product_price / virtuemart_product_price_id inputs:
$selectedDiscount = isset($child->allPrices[$child->selectedPrice]['product_discount_id'])
? (int)$child->allPrices[$child->selectedPrice]['product_discount_id']
: -1;
$discountRates = array();
$discountRates[] = JHtml::_('select.option', '-1', vmText::_('COM_VIRTUEMART_PRODUCT_DISCOUNT_NONE'), 'product_discount_id');
$discountRates[] = JHtml::_('select.option', '0', vmText::_('COM_VIRTUEMART_PRODUCT_DISCOUNT_NO_SPECIAL'), 'product_discount_id');
if (!class_exists('VirtueMartModelCalc')) { VmModel::getModel('calc'); }
foreach (VirtueMartModelCalc::getDiscounts() as $disc) {
$discountRates[] = JHtml::_('select.option', $disc->virtuemart_calc_id, $disc->calc_name, 'product_discount_id');
}
$html .= '<td>'.JHtml::_('select.genericlist', $discountRates,
'childs['.$child->virtuemart_product_id.'][mprices][product_discount_id][]',
'class="vm-chzn-add"', 'product_discount_id', 'text', $selectedDiscount, '[').'</td>';// BEFORE (buggy - inside if (!$isChild), never saved for children):
if (!$isChild){
// ...
$pricesToStore['product_discount_id'] = !empty($data['mprices']['product_discount_id'][$k])
? (int)$data['mprices']['product_discount_id'][$k] : 0; // resets to 0 if missing
// ...
}
// AFTER (outside the block, preserved when not submitted):
if (!$isChild){
// ... other fields only ...
}
if (isset($data['mprices']['product_discount_id'][$k])) {
$pricesToStore['product_discount_id'] = (int)$data['mprices']['product_discount_id'][$k];
}
// If not in POST, existing DB value is preserved-- Diagnostic
SELECT p.virtuemart_product_id, p.product_parent_id,
pp.product_price, pp.product_discount_id
FROM #__virtuemart_products p
LEFT JOIN #__virtuemart_product_prices pp USING (virtuemart_product_id)
WHERE p.product_parent_id > 0;
-- Fix: NULL/0 -> -1 for children with no intentional discount
UPDATE #__virtuemart_product_prices
SET product_discount_id = -1
WHERE virtuemart_product_id IN (
SELECT virtuemart_product_id
FROM #__virtuemart_products
WHERE product_parent_id > 0
)
AND (product_discount_id IS NULL OR product_discount_id = 0);Page created in 0.089 seconds with 13 queries.