"use strict"; const Animate = function(value, target, duration, _function) { function easeOutCubic(t) { return (t = t - 1.0) * t * t + 1.0; } duration = duration || 0.25; value = value || 0; let start = performance.now(), end = start + duration; let from = value, to = target; let func = easeOutCubic; requestAnimationFrame( function animate(time) { let t = func( (performance.now() - start) / 1000.0 / duration); value = map(t, 0.0, 1.0, from, to, true); // console.log(t, value); _function(value); if (t < 1) requestAnimationFrame(animate); } ); } ////////////// ITEM BELOW ///////////////// // Item is global beecause it can be used both in selection and interface const Item = function(id, selection) { const showroom = conf.models; const pipeline = conf.config.pipeline; const item = this; // Set pipeline parameters based on global pipeline for (let n of pipeline) { this[n.name] = null; } this.currentPipePos = -1; // Last available pipeline element = pipeline[this.currentPipePos] // Need for quick access to element params let model = null; // Item interface and usage parameters this.id = null; this.name = null; this.price = { value: 0, string: null }; this.childModels = []; // Currently unused this.image = { element: null, id: null, size: null }; this.isFinished = false; // Need to set different values based on current selection and discount models function getPrice() { function getDirect(model) { return { value: model.price.value, string: model.price.valueName } } function getProgressive(model, selectionList) { // console.log(model, item, selectionList); let sameNeighbourCount = 0, sameNeighbourList = []; for (let n in selectionList) { const m = selectionList[n][model.type]; const i = !!m && m.id === model.id && selectionList[n].isFinished; if (i && item != selectionList[n]) { sameNeighbourList[n] = true; sameNeighbourCount++; } else { sameNeighbourList[n] = false; } } sameNeighbourCount--; if (sameNeighbourCount < 0 ) sameNeighbourCount = 0; // Changing the same at all selecion for (let n in selectionList) { const m = selectionList[n][model.type]; if (!!m && m.id === model.id) { selectionList[n].price = getIterative(model.price, sameNeighbourCount); } } function getIterative(price, index) { return { value: price.value[index] || price.value[price.value.length - 1], string: price.valueName[index] || price.valueName[price.valueName.length - 1], } } return getIterative( model.price, sameNeighbourCount ); } for (let i = item.currentPipePos; i > -1; i-- ) { const model = item[pipeline[i].name]; if (!model.price) continue; if (!model.price.pricingModel) if (!!model.price.value || !!model.price.valueName) return getDirect(model); else continue; else if (model.price.pricingModel.name === "direct") return getDirect(model); else if (model.price.pricingModel.name === "progressive") return getProgressive(model, selection.list); } return { value: 0, string: null }; } // Get model pipeline and interface params by id from showroom // Currentry no support for resetOnNeighborsChange this.set = function(id) { item.currentPipePos = -1; function findModel(models, id, depth) { for (let i of models) { if (item.currentPipePos !== -1) { break }; item[pipeline[depth].name] = i; if (i.id === id) { // console.log('success, currentPos: ', + (depth) ); item.currentPipePos = depth; break; } else if (depth < pipeline.length - 1) { findModel(i.models, id, depth + 1); } } if (item.currentPipePos < depth) { // console.log("not found in depth " + depth); for (let i = depth; i < pipeline.length; i++) item[pipeline[i].name] = null; } } // findModel(showroom, id, pipeline.length); findModel(showroom, id, item.currentPipePos + 1); if (item.currentPipePos !== -1) model = item[pipeline[item.currentPipePos].name]; // As a result we get a model. Now set it up this.isFinished = item.currentPipePos === pipeline.length - 1; this.name = (!!model) ? model.name : null; this.id = (!!model) ? model.id : null; this.childModels = (!!model) ? (!!model.models) ? model.models : null : showroom; // Currently unused this.image = (!!model) ? { element: document.getElementById(model.image.id), id: model.image.id, size: model.image.width } : { element: null, id: null, size: null }; console.log(this.image); this.price = getPrice(); // console.log(item.name, item.id, item.childModels, item.image, item.price); } this.set(id); }; //////////////////////////// SELECTION BELOW ////////////////////////////////// const Selection = function( initList ) { const MAX = this.max = conf.config.maxItems; const pipeline = this.modelPipeline = conf.config.pipeline; const selection = this; let controller = {}; let images = {}; // main selection list this.list = []; // current selection position this.currentPos = 0; this.mode = 'payment'; // 'request' this.addModel = function( id ) { // pos maybe used later selection.list.push( new Item( id, selection ) ); selection.currentPos = selection.list.length - 1; // switches to the fresh added images.addImage( selection.list[selection.currentPos], selection.currentPos); images.updateCurrentStatus(); controller.update( selection.currentPos ); setModelsString(); } this.removeModel = function(pos) { images.removeImage(pos); selection.list.splice(pos, 1); // define current if (selection.currentPos >= pos && selection.currentPos != 0) { selection.currentPos--; } if (selection.list.length === 0) selection.addModel(); images.updateCurrentStatus(); controller.update( selection.currentPos ); setModelsString(); } this.updateModel = function(pos, id) { selection.list[pos].set(id); setModelsString(); } // sets current and update current image this.setCurrent = function(pos) { selection.currentPos = pos; images.updateCurrentStatus(); // update images (set all to inactive) // update controller controller.update( selection.currentPos ); } // This is very fast implemented logic to change the value to send function setModelsString() { const arr = []; for (let i of selection.list) { if (!!i.id) arr.push(i.id) }; document.getElementById('input-models').value = JSON.stringify(arr); } this.killBadNeighbours = function() { const current = selection.list[selection.currentPos]; const id = (!!current.model) ? current.model.id : null; console.log("mmm", current, id, selection.list); for (let i = 0; i < selection.list.length; i++) { const m = selection.list[i]; // console.log(m == current, m.model && m.model.id ); if (m === current) {console.log("m === current"); continue;} if (!current.model) {console.log("!current.model"); continue;} if (!!m.model && m.model.id === id) { console.log("!!m.model && m.model.id === id"); continue; } selection.removeModel(i); i--; } } this.do = function(id) { console.log( 'button pressed: ' + id ); selection.updateModel(selection.currentPos, id); selection.killBadNeighbours(); controller.update( selection.currentPos ); images.updateAllImages(); setModelsString(); } this.attachController = function( obj ) { controller = obj; images = controller.images; } this.init = function() { if (!initList || initList.length == 0) initList.push(null); for(let i in initList) { if (i < MAX) this.addModel(i, initList[i] ); } // console.log(controller, images, this.list); // controller.images.updateAllImages(); // // reset controller to default params // controller.update( selection.currentPos ); // console.log(selection.list); } } ////////////////////////////// IMAGE BELOW ///////////////////////////////////// const Images = function(selection) { this.container = getElement('conf-images'); this.list = []; // Set connections this.selection = selection; selection.images = this; this.shift = 0; const Image = function(localSelection, pos = null) { this.pos = pos; const getModelString = function(selection) { let currentString = conf.config.imageIdSuffix; return (!!selection.id) ? selection.image.id : currentString + '-null'; } this.update = function(localSelection) { this.container.innerHTML = ''; this.img = document.getElementById( getModelString(localSelection) ).cloneNode(); this.img.classList.add('selection-image'); this.img.onclick = function() { selection.setCurrent( this.parentNode.dataset.pos ); } this.container.appendChild(this.img); this.status = document.createElement('div'); this.status.classList.add('selection-image-status'); this.container.appendChild(this.status); this.del = document.createElement('img'); this.del.classList.add('selection-image-delete'); this.del.src = 'css/interface/delete-icon.svg'; this.del.setAttribute('style', "position: absolute; top: 6px; right: 6px;" + " width: 40px; height: 40px; margin: 6px 6px 6px auto;"); this.del.onclick = function() { selection.removeModel( this.parentNode.dataset.pos ); } this.container.appendChild(this.del); } this.container = document.createElement('div'); this.container.classList.add('selection-image-container'); this.container.setAttribute('style', "position: relative;"); this.container.id = 'selection-image-' + pos; this.container.dataset.pos = pos; this.update(localSelection); } this.init = function() { for(let i = 0; i < selection.list.length; i++) { this.addImage( selection.list[selection.current], selection.current ); } this.updateCurrentStatus(); } this.addImage = function(selection, pos) { let img = new Image(selection, pos); img.pos = pos; this.list.push(img); this.container.appendChild( img.container ); } this.removeImage = function(pos) { this.container.removeChild( this.list[pos].container ); delete this.list[pos]; this.list.splice(pos, 1); for (let i = pos; i < this.list.length; i++) { this.list[i].pos--; this.list[i].container.id = 'selection-image-' + this.list[i].pos; this.list[i].container.dataset.pos = this.list[i].pos; } } // updates pos and current this.updateCurrentStatus = function() { let width = this.container.clientWidth; let currentWidth = 0, leftWidth = 0, rightWidth = 0; // console.log(selection.currentPos, selection.list.length); for (let i = 0; i < selection.list.length; i++) { this.list[i].status.classList.remove('status-selected'); if (i == selection.currentPos) { this.list[i].status.classList.add('status-selected'); currentWidth += this.list[i].container.clientWidth; } else if (i < selection.currentPos) leftWidth += this.list[i].container.clientWidth; else if (i >= selection.currentPos) rightWidth += this.list[i].container.clientWidth; } // console.log(leftWidth, currentWidth, rightWidth); // let f = width / 2.0 - (currentWidth / 2.0 + (leftWidth - rightWidth)) ; let f = width / 2.0 - currentWidth / 2.0 - leftWidth; // + (currentWidth - leftWidth - rightWidth) / 2.0 for (let i = 0; i < selection.list.length; i++) { Animate(this.shift, f, 0.15, function(value) { selection.images.list[i].container.style.transform = 'translate(' + value + 'px, 0)'; } ); } this.shift = f; } this.updateAllImages = function() { for (let i = 0; i < selection.list.length; i++) { this.list[i].update( selection.list[i] ); } this.updateCurrentStatus(); } } ////////////////////////////// BUTTON BELOW //////////////////////////////////// const Button = function(model, index, mark) { const item = (!!model) ? new Item(model.id, selection) : null; const type = !!model && !!model.altCta ? "a" : "button"; // console.log(item, model, index, mark); let button = document.createElement(type); let parameters = document.createElement('div'); button.appendChild(parameters); button.classList.add("option-button"); parameters.classList.add("parameters"); button.setAttribute('style', "position: relative; overflow: hidden;"); let progressBar = document.createElement('div'); progressBar.setAttribute('style', "position: absolute; top: 0px; left: 0px; background: white; opacity: 0.1; height: 100%;"); progressBar.style.width = '0%'; button.appendChild(progressBar); button.disable = function() { button.disabled = true; } button.enable = function() { button.disabled = false; } button.markAsSelected = function() { button.classList.add('selected'); button.disable(); } button.markAsDisabled = function() { button.classList.add('disabled'); button.disable(); } button.markAsNormal = function() { button.classList.remove('selected'); button.enable(); } button.setId = function() { if (!!index) button.id = index; } const actions = new function() { const delay = 0.4; const duration = 1.0; const end = duration + delay; let start = 0, timer, progress; let active = false; let requestId, click, hold; this.hold = function(clickFunction, holdFunction) { if (typeof clickFunction == "function") click = clickFunction; if (typeof holdFunction == "function") hold = holdFunction; if (!active) { start = performance.now(); active = true; } timer = ( performance.now() - start ) / 1000.0; progress = clamp(map(timer, delay, duration) * 100, 0, 100); progressBar.style.width = progress + '%'; requestId = requestAnimationFrame(actions.hold); if (timer > duration) { actions.stop() } } this.stop = function() { cancelAnimationFrame(requestId); active = false; if (timer <= delay) { click(); } else if (timer <= duration) { progressBar.style.width = progress + '%'; } else { hold(); this.out(); } } this.out = function() { if (!!active) this.stop(); timer = 0.0; progress = 0; progressBar.style.width = progress + '%'; } } button.setInteractions = function(altCta) { let click = function() { selection.do( item.id ) }; let hold = function() { controller.overlay.setVisible() }; if (!altCta) { button.onmousedown = function() { actions.hold( click, null ) }; // button.onmousedown = function() { actions.hold( click, hold ) }; // button.ontouchstart = function(event) { // // event.preventDefault(); // actions.hold( click, hold ); // }; button.ontouchstart = function(event) { // event.preventDefault(); actions.hold( click, null ); }; button.onmouseup = function () { actions.out() }; button.ontouchend = function(event) { // event.preventDefault(); actions.out(); }; // button.onclick = function() { selection.do(model, button.id); // mark selection }; // or selection.do(button, type)? button.onmouseover = function() { modelHoverIn(button) }; button.onmouseout = function() { modelHoverOut(button) }; } else { button.href = altCta; } } button.updateName = function() { } button.updatePrice = function() { } button.updateColor = function() { } function checkAvailable(model) { return (!!model && !!model.available); } button.setParameters = function() { parameters.innerHTML = ''; let name = document.createElement('p'); name.classList.add("name"); name.innerHTML = model.name; parameters.appendChild(name); // update price let price = document.createElement('p'); price.classList.add("price"); price.innerHTML = item.price.string; parameters.appendChild(price); } if (!!item) { button.setId(); button.setInteractions(model.altCta); button.setParameters(); if (mark === 'selected') button.markAsSelected(); else if (mark === 'disabled') button.markAsDisabled(); else button.markAsNormal(); } return button; } /////////////////////////// CONTROLLER BELOW /////////////////////////////////// const Controller = function(selection) { const content = getElement('conf-controller-content'); const price = document.getElementById('conf-continue-price'); const continueButton = document.getElementById('conf-continue-button'); const pipeline = selection.modelPipeline; const showroom = conf.models; const mode = selection.mode; // 'request' or 'payment' this.images = new Images( selection ); // Current steps model depend on html structure. // It is better to clear an html and build it again based on pipeline this.steps = []; for (let i of pipeline) { this.steps.push({ name: i.name, isPipeline: true, element: document.getElementById( 'conf-' + i.name ), buttons: [], updatable: true, blocking: (conf.config.pipelineModel === "stepByStep") ? true : false, type: 'multiple', text: i.text, id: 'conf-' + i.name, }); } this.steps.push({ name: 'next', isPipeline: false, element: document.getElementById( 'conf-next' ), buttons: [], updatable: false, blocking: true, type: 'continue', text: 'Go Next', id: 'conf-next', }); this.updatePrice = function() { let total = 0, items = 0; for (let i in selection.list) { let s = selection.list[i]; if (s.isFinished) { total += s.price.value; items++; } } if (document.getElementById('input-delivery').checked) total += 25.00; price.innerHTML = total.toFixed(2); price.innerHTML += ' (' + items + ' ' + ((items != 1) ? 'items' : 'item') + ')'; } this.updateContinue = function() { console.log(continueButton); if (!!selection.list[0].isFinished) { continueButton.classList.remove('disabled'); continueButton.disabled = false; } else { continueButton.classList.add('disabled'); continueButton.disabled = true; } } this.updateForm = function(count) {} this.updateFormDelivery = function(sel) { const deliverySelector = document.getElementById('card-input-delivery'); const deliveryInput = document.getElementById('input-delivery'); const allowList = ["AT", "BE", "CY", "DK", "EE", "FI", "FR", "DE", "GR", "HU", "IT", "LV", "LI", "LT", "LU", "MT", "MC", "ME", "NL", "NO", "PL", "PT", "SK", "SI", "ES", "SE", "CH", "GB"]; for (let i = 0; i < allowList.length; i++) { if (sel.options[sel.selectedIndex].value === allowList[i]) { deliverySelector.classList.remove('hidden'); return; } } deliveryInput.checked = false; deliverySelector.classList.add('hidden'); controller.updatePrice(); } // updates all controller parameters to current selection state this.update = function(pos) { let currentSelection = selection.list[pos]; // // Why we need this? // if (pos === 0 && !currentSelection) { // selection.addModel(); // currentSelection = selection.list[pos]; // } // loop through the steps to reset all buttons (except accessories) for (let i in this.steps) { const step = this.steps[i]; // update buttons updateButtonsInBlock(step, Number(i), currentSelection); } // const block = this.steps[pipeline[current.currentPipePos]].block; const block = this.steps[currentSelection.currentPipePos + 1].element; // console.log(block, currentSelection); this.scroll(block, 0.4); controller.updateContinue(); controller.updatePrice(); } // updates all buttons in certain block relative to selection function updateButtonsInBlock(step, stepIndex, selectedItem) { // console.log(step, stepIndex, selectedItem); const buttonsParent = step.element.getElementsByClassName('option-buttons')[0]; const buttons = buttonsParent.getElementsByClassName('option-button'); const type = step.name; // console.log(buttonsParent, buttons, type); // remove all buttons in this block if (step.updatable) { for (let i = buttons.length - 1; i >= 0; i--) { buttons[i].remove(); } for (let i in step.buttons) { delete step.buttons[i]; } } // and set up the new ones... // console.log(step.isPipeline, type, selectedItem[type]); // first define what we need to cycle let cycleList = null, selectedIndex = null; if (step.isPipeline) { if (selectedItem.currentPipePos < stepIndex - 1) cycleList = null; else { // console.log(selectedItem.currentPipePos, stepIndex); function getModels(models, item, depth, index) { let type = pipeline[depth].name; // console.log("start", type, depth, index); for (let i in models) { // console.log(i, item[type].id, models[i].id); if (!!item[type] && item[type].id === models[i].id) { selectedIndex = i; // console.log('found') break; } else selectedIndex = null; } if (index === depth) return models; else { // console.log("ищем дальше", models[selectedIndex].models) return getModels(models[selectedIndex].models, item, depth + 1, index); } } cycleList = getModels(showroom, selectedItem, 0, stepIndex); // console.log( cycleList, selectedIndex ); } } // than cycle! if (!!cycleList) { // console.log(cycleList); for (let i in cycleList) { const model = cycleList[i]; let mark = (i === selectedIndex) ? 'selected' : null; const first = selection.list[0]; const cp = selection.currentPos; // console.log(cycleList[i].type === 'model') // console.log("cp", cp); // console.log(first.id, model.id); // if (cp != 0 && cycleList[i].type === 'model' && first.model.id !== model.id) // mark = 'disabled'; if (!model.isVisible) { continue }; if (!model.isAvailable) mark = 'disabled'; let button; step.buttons.push( button = new Button( model, i, mark ) ); buttonsParent.appendChild( step.buttons[ step.buttons.length - 1 ] ); } buttonsParent.parentElement.classList.remove('disabled'); } else if (!!step.isPipeline) { for(let i = 0; i < 2; i++) { step.buttons.push( new Button() ); buttonsParent.appendChild( step.buttons[ step.buttons.length - 1 ] ); buttonsParent.parentElement.classList.add('disabled'); } } else { // for «next» section console.log( step.element); if (!!selectedItem.isFinished && selection.list.length < selection.max) { console.log('finished', step.element); step.element.classList.remove('disabled'); step.element.getElementsByClassName('option-next')[0].disabled = false; } else { console.log('not finished', step.element); step.element.classList.add('disabled'); console.log('not finished', step.element); step.element.getElementsByClassName('option-next')[0].disabled = true; } } } // scroll to necessary block this.scroll = function(block, duration) { function easeOutCubic(t) { return (t = t - 1.0) * t * t + 1.0; } const shift = 180; getPageSize(); duration = duration || 0.25; let value = content.scrollLeft; let start = performance.now(), end = start + duration; let from = value, to = getElementPosition(block).x - ((page.width > 800) ? ((page.width - 640 + shift) / 2) : 40); let func = easeOutCubic; requestAnimationFrame( function animate(time) { let t = func( (performance.now() - start) / 1000.0 / duration); value = map(t, 0.0, 1.0, from, to, true); // console.log(t, value); content.scrollTo(value, 0.0); if (t < 1) { requestAnimationFrame(animate); } } ); } this.overlay = new function() { const parent = document.getElementById('conf-overlay-background'); const container = document.createElement('div'); parent.appendChild(container); container.classList.add('conf-overlay'); parent.style.display = 'none'; const image = document.createElement('img'); image.classList.add('conf-overlay-image'); image.src = "previews/trunk-1.png"; container.appendChild(image); const header = document.createElement('h2'); header.classList.add('conf-overlay-header'); header.innerHTML = "Hedfan & Baradway TrunkMate"; container.appendChild(header); const text = document.createElement('p'); text.classList.add('conf-overlay-text'); text.innerHTML = "Hedfan & Baradway TrunkMate was designed for owners of large auto with spacious luggage. Multiple sizes, a robust frame with the ability to install one TrunkMate compartment on another, open up a previously unimaginable opportunity to make the best use of the car’s trunk space. Each family member can choose its own TrunkMate luggage to keep personal belongings in order throughout the trip."; container.appendChild(text); this.setInvisible = function() { parent.style.display = 'none'; } this.setVisible = function() { parent.style.display = 'inherit'; } this.del = document.createElement('img'); this.del.classList.add('selection-image-delete'); this.del.src = 'css/interface/delete-icon.svg'; this.del.setAttribute('style', "position: absolute; top: 6px; right: 6px;" + " width: 40px; height: 40px; margin: 6px 6px 6px auto;"); this.del.onclick = function() { controller.overlay.setInvisible() }; container.appendChild(this.del); } function setContinueButton( value ) {} function setNextButton( value ) {} // check for switch from siopa to classic or vice a versa function checkIfSelectionIsReady(selection) { if (!!selection.color) return true; else return false; } } let conf, selection, controller; function start() { const initList = [ // "carbonone", "carbonone-carbonfiber-01", // "carbonone-carbonfiber-01-graphite", // "fff", // "keymate", // "keymate-steel-01", // "keymate-steel-01-blue" ]; selection = new Selection( initList ); controller = new Controller( selection ); selection.attachController( controller ); selection.init(); } function continueButtonPressed() { const thisButton = document.getElementById('conf-continue-button'); const form = getElement('conf-form'); form.classList.remove('disabled'); const inputs = form.getElementsByTagName('input'); for (let i = 0; i < inputs.length; i++) { inputs[i].disabled = false; } thisButton.innerHTML = 'Back to choice ↑' thisButton.onclick = backToController; const curtain = document.getElementById('conf-curtain'); curtain.classList.add('curtain-opened'); } function backToController() { const thisButton = document.getElementById('conf-continue-button'); const form = getElement('conf-form'); form.classList.add('disabled'); thisButton.innerHTML = 'Continue ↓' thisButton.onclick = (function() {continueButtonPressed(thisButton)}); const curtain = document.getElementById('conf-curtain'); curtain.classList.remove('curtain-opened'); controller.updateFormDelivery(); } // scroll to necessary block function scrollPage(block, duration) {} // add one item to the choice function addNewItem() { selection.addModel(); } // triggered when next button pressed function nextPressed() { addNewItem(); } // triggered when model button hovered function modelHoverIn(button, model) {} // triggered when model button hovered out function modelHoverOut(button, model) {} function loadConfig() { const xhr = new XMLHttpRequest(); xhr.open('GET', '/models.json?13', false); xhr.send(); if (xhr.status != 200) { console.log( xhr.status + ': ' + xhr.statusText ); // пример вывода: 404: Not Found } else { conf = JSON.parse(xhr.responseText); start(); } } // load page fully load than continue document.addEventListener("DOMContentLoaded", loadConfig);