'use strict'; /** * Order card questions/options visibility (hide/show) for .product-options inside .order-card. * Self-contained port of behavior from offertetool.js and adp-offertetool-util.js, scoped to .order-card. * No dependency on order-card.js for choice value parsing or number validation. */ (function(global) { var choiceTypeParsers = { 'string': function(v) { return v; }, 'integer': function(v) { return parseInt(v, 10); } }; /* Get current value from a [data-choice] element, parsed by data-type (string/integer). */ function choiceValueForElement(choiceElement) { var parser = choiceTypeParsers[choiceElement.dataset.type] || choiceTypeParsers['string']; return parser(choiceElement.value); } /* Parse data-hide_when JSON from a choice or option element. */ function getChoiceHiddenWhen(choiceElement) { const hideWhenAttr = choiceElement.dataset.hide_when || choiceElement.getAttribute('data-hide_when'); if (!hideWhenAttr || hideWhenAttr === '') return null; try { return JSON.parse(hideWhenAttr); } catch (e) { console.warn('Failed to parse data-hide_when for element:', choiceElement, e); return null; } } function getVarToUseForCard(input) { var card = input.closest('.order-card'); if (!card) return null; var useVarWhenData = input.dataset.useVarWhen ? (function() { try { return JSON.parse(input.dataset.useVarWhen); } catch (e) { return {}; } })() : {}; var VarToUse = null; outer: for (var key in useVarWhenData) { var value = useVarWhenData[key]; for (var key2 in value) { var value2 = value[key2]; for (var key3 in value2) { var value3 = value2[key3]; var choiceElement = card.querySelector('[data-choice="' + key3 + '"]'); if (!choiceElement) continue; if (choiceElement.value === value3) { VarToUse = parseFloat(key); if (isNaN(VarToUse)) VarToUse = null; break outer; } } } } return VarToUse; } /* Validate a number input inside .product-options: step snap, excluded ranges (if present), min/max/step errors.*/ function validateNumberChoiceForCard(input) { var maxWidthAttr = input.closest('.order-card').dataset.maxWidth; var maxHeightAttr = input.closest('.order-card').dataset.maxHeight; var value = parseInt(input.value, 10); var question = input.closest('.question'); var errorDiv = question ? question.querySelector('.error') : null; if (!errorDiv) return; var lang = (document.body && document.body.dataset && document.body.dataset.language) || 'en'; var error_message = ''; var minimum = input.min !== undefined && input.min !== '' ? parseFloat(input.min) : NaN; if (input.dataset.choice && maxWidthAttr && input.dataset.choice.includes('_width')) { var maximum = maxWidthAttr; } else if (input.dataset.choice && maxHeightAttr && input.dataset.choice.includes('_height')) { var maximum; var card = input.closest('.order-card'); var root = card ? (card.querySelector('.product-options') || card) : null; try { var maxHeightData = maxHeightAttr ? JSON.parse(maxHeightAttr) : { 'rand': { 'EF': 10000, 'RF': 10000 } }; var firstKey = Object.keys(maxHeightData)[0]; var choiceKey = firstKey && firstKey.length > 3 ? firstKey.substring(0, 3) : firstKey; var randChoice = root ? root.querySelector('[data-choice="' + firstKey + '"], [data-choice="' + choiceKey + '"]') : null; var randValue = randChoice ? randChoice.value : (firstKey && maxHeightData[firstKey] ? Object.keys(maxHeightData[firstKey])[0] : null); if (firstKey && maxHeightData[firstKey] && randValue != null && maxHeightData[firstKey][randValue] !== undefined) { maximum = maxHeightData[firstKey][randValue]; } else { maximum = maxHeightAttr ? parseInt(maxHeightAttr, 10) : NaN; } } catch (e) { maximum = maxHeightAttr ? parseInt(maxHeightAttr, 10) : NaN; } } else { var maximum = input.max !== undefined && input.max !== '' ? parseFloat(input.max) : NaN; } if (!isNaN(value)) { // Step snap: snap to nearest valid step value when user types/changes var stepNum = parseInt(input.step, 10); var previousValue = parseInt(input.dataset.previousValue || input.defaultValue || input.value, 10); var valueWasSnapped = false; if (stepNum && !isNaN(previousValue)) { if (value !== previousValue + stepNum && value !== previousValue - stepNum && value !== previousValue) { var validValues = []; var minVal = isNaN(minimum) ? 0 : parseInt(minimum, 10); var maxVal = isNaN(maximum) ? value + stepNum * 10 : parseInt(maximum, 10); for (var i = minVal; i <= maxVal; i += stepNum) { validValues.push(i); } if (validValues.length > 0) { var nearestValue = validValues[0]; var minDiff = Math.abs(value - nearestValue); for (var j = 1; j < validValues.length; j++) { var diff = Math.abs(value - validValues[j]); if (diff < minDiff) { minDiff = diff; nearestValue = validValues[j]; } } if (nearestValue !== value) { input.value = nearestValue; value = nearestValue; valueWasSnapped = true; } } } } input.dataset.previousValue = String(parseInt(input.value, 10)); if (valueWasSnapped) { setTimeout(function() { input.dispatchEvent(new Event('change', { bubbles: true })); }, 500); } // Variable scoop height and excluded ranges if (input.dataset.excludedRanges) { var card = input.closest('.order-card'); var choiceElementSchoepVar = card ? card.querySelector('input[data-choice="schoepsteek_var"]') : null; var varToUse = getVarToUseForCard(input); if (choiceElementSchoepVar && varToUse !== null) { var scoopErr = { de: 'Der gewählte Wert ergibt eine zu große oder zu kleine Schaufelsteigung, wählen Sie einen anderen nom H-Wert', nl: 'De gekozen waarde resulteert in een te grote of te kleine schoepsteek, kies een andere nom H waarde', en: 'The chosen value results in a scoop pitch that is too large or too small, choose a different nom H value' }[lang] || 'The chosen value results in a scoop pitch that is too large or too small, choose a different nom H value'; var minStep = parseInt(choiceElementSchoepVar.min, 10); var maxStep = parseInt(choiceElementSchoepVar.max, 10); var ScoopcountMin = Math.ceil((value - varToUse) / maxStep); var ScoopcountMax = Math.floor((value - varToUse) / minStep); if (ScoopcountMax === 0 || ScoopcountMin === 0) { if (!error_message) error_message = scoopErr; } var scoopheightMin = ScoopcountMax !== 0 ? (value - varToUse) / ScoopcountMax : 0; var scoopheightMax = ScoopcountMin !== 0 ? (value - varToUse) / ScoopcountMin : 0; if (scoopheightMin < minStep || scoopheightMax > maxStep) { if (!error_message && (value < maximum || isNaN(maximum)) && (value > minimum || isNaN(minimum))) { error_message = scoopErr; } } else if (scoopheightMin > maxStep && scoopheightMax < minStep) { if (!error_message && (value < maximum || isNaN(maximum)) && (value > minimum || isNaN(minimum))) { error_message = scoopErr; } } var varscoopvalue = ScoopcountMin > ScoopcountMax ? ScoopcountMax : ScoopcountMin; var varscoopheight = scoopheightMax > scoopheightMin ? scoopheightMax : scoopheightMin; var optionsRoot = card.querySelector('.product-options') || card; var outputQuestion = optionsRoot ? Array.prototype.filter.call(optionsRoot.querySelectorAll('.question'), function(q) { var label = q.querySelector('label'); return label && label.textContent.trim().toLowerCase().indexOf('output') !== -1; })[0] : null; if (outputQuestion) { var outputInput = outputQuestion.querySelector('input'); if (outputInput) { outputInput.value = 'Schoepsteek: ' + varscoopheight.toFixed(1) + 'mm Schoepen: ' + varscoopvalue; } } } } // Min/max/step error messages if (!error_message && !isNaN(minimum) && value < minimum) { error_message = { nl: 'Bovenstaande afmeting is minimaal ' + input.min + ' mm', de: 'Das obige Maß beträgt mindestens ' + input.min + ' mm', en: 'The above dimension is at least ' + input.min + ' mm' }[lang] || 'The above dimension is at least ' + input.min + ' mm'; } if (!error_message && !isNaN(maximum) && value > maximum) { error_message = { de: 'Das obige Maß beträgt maximal ' + maximum + ' mm', nl: 'Bovenstaande afmeting is maximaal ' + maximum + ' mm', en: 'The above dimension is at most ' + maximum + ' mm' }[lang] || 'The above dimension is at most ' + maximum + ' mm'; } } else { error_message = { nl: 'Ongeldige waarde', de: 'Ungültiger Wert', en: 'Invalid value' }[lang] || 'Invalid value'; } const showingSnapNotice = errorDiv.classList.contains("snap-notice"); if (!showingSnapNotice) { errorDiv.innerHTML = error_message; if (!error_message) { errorDiv.classList.add('hidden'); } else { errorDiv.classList.remove('hidden'); } } // Remove existing error messages from hidden height questions if (!showingSnapNotice) { const errorHeight = document.querySelectorAll('.question.fully_hidden_question:has(input[data-choice*="_height"])'); for (let question of errorHeight) { let errorQuestion = question.querySelector('.error'); if (errorQuestion) { errorQuestion.classList.add('hidden'); } } } if (valueWasSnapped) { let snapMessage; switch (document.body.dataset.language) { case "nl": snapMessage = "Waarde is afgerond op de dichtstbijzijnde toegestane stap."; break; case "de": snapMessage = "Der Wert wurde auf den nächstzulässigen Schritt gerundet."; break; case "en": default: snapMessage = "Value was rounded to the nearest allowed step."; break; } errorDiv.innerHTML = snapMessage; errorDiv.classList.remove("hidden"); errorDiv.classList.add("snap-notice"); setTimeout(() => { errorDiv.innerHTML = ""; errorDiv.classList.add("hidden"); errorDiv.classList.remove("snap-notice"); }, 6000); } } /* Find buttons in the card and set disabled based on whether any .question .error is visible. */ function updateAddButtonState(card) { var addBtn = card.querySelector('button[data-action="add-to-project"]'); var generateCardBtn = card.querySelector('button[data-action="open-in-tab"]'); var root = card.querySelector('.product-options') || card; var errors = root.querySelectorAll('.question .error:not(.snap-notice)'); var hasError = false; for (var i = 0; i < errors.length; i++) { var err = errors[i]; if (!err.classList.contains('hidden') && err.innerHTML.trim() !== '') { hasError = true; break; } } if (addBtn) addBtn.disabled = hasError; if (generateCardBtn) generateCardBtn.disabled = hasError; } /** * Get all [data-choice] elements and a choices object (live getters) scoped to card. * @param {HTMLElement} card - .order-card element * @returns {[Object, Object]} [choices, choiceElements] */ function getAllChoicesForCard(card) { const choices = {}; const choiceElements = {}; const root = card.querySelector('.product-options') || card; root.querySelectorAll('[data-choice]').forEach(function(el) { const name = el.dataset.choice; if (!name) return; choiceElements[name] = el; Object.defineProperty(choices, name, { get: function() { return choiceValueForElement(el); }, enumerable: true, configurable: true }); }); return [choices, choiceElements]; } /** * Compute partsB and partsH from width/height choices and card data (maxWidth, maxHeight). * @param {HTMLElement} card - .order-card element * @param {Object} choiceElements - map of choice name -> element * @returns {{ partsB: number, partsH: number }} */ function computePartsForCard(card, choiceElements) { var partsH = 1; var partsB = 1; var root = card.querySelector('.product-options') || card; var maxWidthAttr = card.dataset.maxWidth; var maxHeightAttr = card.dataset.maxHeight; var maxwidth = maxWidthAttr ? parseInt(maxWidthAttr, 10) : 60000; var maxHeightData; try { maxHeightData = maxHeightAttr ? JSON.parse(maxHeightAttr) : { 'randtype': { 'EF': 10000, 'RF': 10000 } }; } catch (e) { maxHeightData = { 'randtype': { 'EF': 10000, 'RF': 10000 } }; } var firstKey = Object.keys(maxHeightData)[0]; var choiceKey = firstKey && firstKey.length > 3 ? firstKey.substring(0, 3) : firstKey; var typeChoice = root.querySelector('[data-choice="' + firstKey + '"], [data-choice="' + choiceKey + '"]'); var typeValue = typeChoice ? typeChoice.value : (firstKey && maxHeightData[firstKey] ? Object.keys(maxHeightData[firstKey])[0] : 'EF'); var maxheight = (firstKey && maxHeightData[firstKey] && maxHeightData[firstKey][typeValue] !== undefined) ? maxHeightData[firstKey][typeValue] : 2025; for (var name in choiceElements) { var choice = choiceElements[name]; var choice_id = choice.dataset.choice; var choice_value = choice.value; if (!choice_id) continue; if (choice_id === '_width') { var numWidth = parseFloat(choice_value); if (!isNaN(numWidth) && numWidth > maxwidth) { var minPartsB = Math.ceil(numWidth / maxwidth); if (minPartsB > 1 && minPartsB <= 100) { partsB = minPartsB; } } } if (choice_id && choice_id.indexOf('_height') !== -1) { var questionElement = choice.closest('.question'); if (!questionElement || !questionElement.classList.contains('fully_hidden_question')) { var numHeight = parseFloat(choice_value); if (!isNaN(numHeight) && numHeight > maxheight) { var minPartsH = Math.ceil(numHeight / maxheight); if (minPartsH > 1 && minPartsH <= 100) { partsH = minPartsH; } } else { partsH = 1; } } } } return { partsB: partsB, partsH: partsH }; } /** * Set height variable from the first visible _height choice in the card. * @param {HTMLElement} card - .order-card element * @param {Object} choiceElements - map of choice name -> element */ function assignHeight(card, choiceElements) { const root = card.querySelector('.product-options') || card; const heightVar = root.querySelector('[data-choice="_height"]'); if (!heightVar) return; for (const element of Object.values(choiceElements)) { if (element.dataset.choice && element.dataset.choice.indexOf('_height') !== -1) { const question = element.closest('.question'); const isFullyHidden = question && question.classList.contains('fully_hidden_question'); const isHidden = question && question.classList.contains('hidden_question'); const isVisible = question && !isFullyHidden && !isHidden && question.offsetParent !== null; if (isVisible) { heightVar.value = element.value; return; } } } } /** * Update visibility of questions and options inside the card based on data-hide_when and option data-hide_when. * @param {HTMLElement} card - .order-card element * @param {Event} [event] - optional event (event.target used only for compatibility; card is the source of truth) */ function updateQuestions(card, event) { const direction = card.dataset.direction; const allChoiceElements = getAllChoicesForCard(card)[1]; const settings = {}; for (const key of Object.keys(allChoiceElements)) { const el = allChoiceElements[key]; settings[key] = choiceValueForElement(el); } // Hide direction question based on card data // Not relevant for current products may become relevant later /*const showDirectionChoice = card.dataset.showDirectionChoice; const directionSelect = card.querySelector('select[data-choice="direction"]'); console.log('directionSelect', directionSelect); if (directionSelect && showDirectionChoice !== undefined) { const directionQuestion = directionSelect.closest('.question'); if (directionQuestion) { if (showDirectionChoice === 'false') { directionQuestion.classList.add('fully_hidden_question'); } else { directionQuestion.classList.remove('fully_hidden_question'); } } } */ // Hide questions based on data-hide_when for (const choiceKey of Object.keys(allChoiceElements)) { const entry = allChoiceElements[choiceKey]; const question_elem = entry.closest('.question'); // Select active height value and set every height opbject as current height value var heightInput = question_elem.querySelector('input[data-choice*="_height"]'); if (heightInput && !question_elem.classList.contains('fully_hidden_question')) { let activeHeightValue = heightInput.value; for (const heightOption of Object.keys(allChoiceElements)) { if(heightOption.includes('_height')) { allChoiceElements[heightOption].value = activeHeightValue; } } } if (!question_elem) continue; const hidden_when = getChoiceHiddenWhen(question_elem); if (hidden_when !== null) { let shouldHide = false; for (let i = 0; i < hidden_when.length; i++) { const condition = hidden_when[i]; let conditionMatches = false; for (const condKey of Object.keys(condition)) { if (settings[condKey] == condition[condKey]) { conditionMatches = true; break; } } if (conditionMatches) { shouldHide = true; break; } } if (shouldHide) { question_elem.classList.add('fully_hidden_question'); } else { question_elem.classList.remove('fully_hidden_question'); } } // Option visibility inside SELECTs if (entry.tagName === 'SELECT') { const questionElem = entry.closest('.question'); if (questionElem && (questionElem.classList.contains('fully_hidden_question') || questionElem.classList.contains('hidden_question'))) { continue; } var hidden_options = 0; var total_options = 0; var optionsList = entry.querySelectorAll('option'); var hidden_options_changed = false; for (var o = 0; o < optionsList.length; o++) { const option = optionsList[o]; total_options++; const hideWhenAttr = option.dataset.hide_when || option.getAttribute('data-hide_when'); if (hideWhenAttr && hideWhenAttr !== '') { var hide = false; var hidden_json; try { hidden_json = option._parsedHideWhen || (option._parsedHideWhen = JSON.parse(hideWhenAttr)); } catch (e) { continue; } for (var j = 0; j < hidden_json.length; j++) { const entry_json = hidden_json[j]; var condKeys = Object.keys(entry_json); if (direction !== undefined && direction !== null) { if ('direction' in entry_json) { if (entry_json.direction !== direction) continue; } } else if (allChoiceElements.direction) { if ('direction' in entry_json) { var dirVal = allChoiceElements.direction.value; if (!dirVal || !dirVal.endsWith || !dirVal.endsWith(entry_json.direction)) continue; } } var matched = 0; for (var ki = 0; ki < condKeys.length; ki++) { const k = condKeys[ki]; if (k === 'direction') continue; const value = entry_json[k]; const ref = allChoiceElements[k]; if (!ref) continue; if (ref.tagName === 'SELECT') { if (ref.value == value) { matched++; } } else if (ref.tagName === 'INPUT') { const test_val = ref.value; const numTest = parseFloat(test_val); const numVal = typeof value === 'string' ? value.trim() : value; if (typeof numVal === 'string' && numVal.indexOf('>=') === 0) { if (!isNaN(numTest) && numTest >= parseFloat(numVal.substring(2))) matched++; } else if (typeof numVal === 'string' && numVal.indexOf('<=') === 0) { if (!isNaN(numTest) && numTest <= parseFloat(numVal.substring(2))) matched++; } else if (typeof numVal === 'string' && numVal.charAt(0) === '>') { if (!isNaN(numTest) && numTest > parseFloat(numVal.substring(1))) matched++; } else if (typeof numVal === 'string' && numVal.charAt(0) === '<') { if (!isNaN(numTest) && numTest < parseFloat(numVal.substring(1))) matched++; } else if (settings[k] === value) { matched++; } } } var nonDirKeys = condKeys.filter(function(k) { return k !== 'direction'; }); if ((condKeys.length === 1 && condKeys[0] === 'direction') || (matched > 0 && matched === nonDirKeys.length)) hide = true; } if (hide) { option.classList.add('hidden_option'); hidden_options++; hidden_options_changed = true; } else { option.classList.remove('hidden_option'); hidden_options_changed = true; } } else { option.classList.remove('hidden_option'); hidden_options_changed = true; } } var optionsArray = Array.prototype.slice.call(entry.querySelectorAll('option')); var visibleOptions = optionsArray.filter(function(opt) { return !opt.classList.contains('hidden_option'); }); var selectedOption = optionsArray[entry.selectedIndex]; if (selectedOption && selectedOption.classList.contains('hidden_option')) { entry.value = visibleOptions[0].value; entry.dispatchEvent(new Event('change', { bubbles: true })); } /*// Hide question if only one option is visible var optionsArray = Array.prototype.slice.call(entry.querySelectorAll('option')); var visibleOptions = optionsArray.filter(function(opt) { return !opt.classList.contains('hidden_option'); }); if (visibleOptions.length === 0) { entry.closest('.question').classList.add('hidden_question'); entry.disabled = true; } else if (visibleOptions.length === 1 && entry.closest('.question').dataset.hide_with_one_option) { entry.closest('.question').classList.add('hidden_question'); entry.disabled = true; } else { entry.closest('.question').classList.remove('hidden_question'); entry.disabled = false; var selectedOption = optionsArray[entry.selectedIndex]; if (selectedOption && selectedOption.classList.contains('hidden_option')) { entry.value = visibleOptions[0].value; entry.dispatchEvent(new Event('change', { bubbles: true })); } }*/ } } // Validate visible number inputs (step snap, excluded ranges, min/max/step) var root = card.querySelector('.product-options') || card; var numberInputs = root.querySelectorAll('input[type="number"][data-choice]'); for (var n = 0; n < numberInputs.length; n++) { var input = numberInputs[n]; var question = input.closest('.question'); if (question && (question.classList.contains('fully_hidden_question') || question.classList.contains('hidden_question'))) continue; validateNumberChoiceForCard(input); } assignHeight(card, allChoiceElements); var parts = computePartsForCard(card, allChoiceElements); card.dataset.partsB = String(parts.partsB); card.dataset.partsH = String(parts.partsH); updateAddButtonState(card); } /** * Get current partsB and partsH for the card (from dataset, set by updateQuestions). * Use when building order payload so API receives partsB/partsH. */ function getPartsForCard(card) { var b = parseInt(card.dataset.partsB, 10); var h = parseInt(card.dataset.partsH, 10); return { partsB: isNaN(b) ? 1 : b, partsH: isNaN(h) ? 1 : h }; } // Expose for use by commonOrderCardInit and initializers global.orderCardUpdateQuestions = updateQuestions; global.orderCardAssignHeight = assignHeight; global.getAllChoicesForCard = getAllChoicesForCard; global.getChoiceHiddenWhen = getChoiceHiddenWhen; global.orderCardGetParts = getPartsForCard; })(typeof window !== 'undefined' ? window : this);