app/assets/javascripts/pizzade.js
//= require jquery_ujs
//= require _both
//= require bootstrap/dropdown
//= require bootstrap/collapse
//= require _static_tools
//= require _guess_postcode
//= require _cookie
//= require _watchdog
var hipster = window.hipster = (function() {
'use strict';
var my = HIPSTER;
// CACHES ////////////////////////////////////////////////////////////
var _isShop = null;
var _isLoading = true;
var _runAfterLoad = [];
// PRIVATE ///////////////////////////////////////////////////////////
function getPostalCode() {
my.guessPostcode('#plzsearch_input');
}
function isShopPage() {
if(_isShop !== null) {
return _isShop;
}
_isShop = $('body:contains("Warenkorb")').length === 1;
return _isShop;
}
function isLoading() {
if(!_isLoading) {
return false;
}
_isLoading = $('label:contains("PLZ")').length === 0;
return _isLoading;
}
function getShaAddress() {
var prefix = 'hipsterpizza_odr_';
// keep in sync with basket_helper#contact_sha_address
var addr = '';
addr += localStorage[prefix + 'zipcode'] + ' ';
addr += localStorage[prefix + 'street'] + ' ';
addr += localStorage[prefix + 'street_no'];
addr = $.trim(addr).toLowerCase().replace(/[^a-z0-9]/g, '');
if(my.isBlank(addr)) {
return null;
}
return new jsSHA(addr, 'TEXT').getHash('SHA-512', 'HEX');
}
function getCartItemsJson() {
var data = [];
$('.cartitems').each(function(ind, elm) {
var prod = $(elm).find('.cartitems-title div').text();
var price = $(elm).find('.cartitems-itemsum .cartitems-sprice div').text();
price = my.textPriceToFloat(price);
if(my.isBlank(prod) || isNaN(price)) {
my.err('Couldn’t detect product properly, maybe the script is broken?');
return;
}
var extra = [];
// subitems = additional toppings, subsubitems = salad dressing in menus
// do not filter for the <a> element, as items part of a menu won’t be
// links.
var finder = '.cartitems-subitem .cartitems-subtitle, .cartitems-subsubitem .cartitems-subtitle';
$(elm).find(finder).each(function(ind, ingred) {
extra[ind] = $(ingred).text();
});
data[ind] = { 'price': price, 'prod': prod, 'extra': extra.sort() };
});
var deposit = my.textPriceToFloat($('.deposit div').text());
if(!isNaN(deposit) && deposit > 0) {
data[data.length] = { price: deposit, 'prod': 'Pfand', extra: [] };
}
return data;
}
function getCartItemsCount() {
return $('.cartitems').length;
}
function getUserNick() {
var nick = my.getCookie('nick');
do {
nick = window.prompt('Your Nick:', my.isBlank(nick) ? '' : nick);
// user clicked cancel
if(nick === null) {
return null;
}
} while(my.isBlank(nick));
my.setCookie('nick', nick);
return nick;
}
function navLinkIsSelected(navLink) {
// first is normal pizza.de clients, 2nd variant is Joey’s Pizza
return navLink.hasClass('activ') || navLink.find('span').hasClass('ausgewaehlt');
}
function elementHasText(el, text) {
var el = $(el);
return el.text() === text || el.attr('title') === text;
}
function findLinkWithText(text) {
// a row *may* link to the same item more than once. Don’t count these as
// ambiguous items.
var matches = $([]);
$('#framek tr').each(function() {
var candidates = $(this).find('a').filter(function() {
return elementHasText(this, text);
});
if(candidates.length === 0) {
return;
}
matches.push(candidates.first());
});
return matches;
}
function getPriceOfLastItem() {
var p = $('.cartitems:last .cartitems-itemsum .cartitems-sprice div');
return my.textPriceToFloat(p.text());
}
function getActiveSubPageText() {
return $('.navbars a.activ, .navbars span.ausgewaehlt').text();
}
function replay(items, finishCallback) {
// setup
my.log('replay: setup started');
$.fx.off = true;
$('body').addClass('wait');
var navLinks = $.makeArray($('.navbars a'));
var subNavLinks = [];
var isTopLevelLink = true;
var currentNav = null;
var errorMsgs = [];
var loadingWatchdog = my.Watchdog(30, process);
function preloadSubPages(arr) {
if(isMobileBrowser) {
return;
}
$.each(arr, function(ind, a) {
var handler = $(a).attr('onclick');
var url = handler.replace(/.*(framek[0-9.]+\.htm).*/, '$1');
// it’s enough to extract the filename since pizza.de rewrites
// the base href to what we need already.
$.get(url);
});
}
preloadSubPages(navLinks);
function getPossibleSubLinks() {
// TODO: does navigation-3-v8 exist?
// the currently active page has already been parsed when the main
// category page was loaded/clicked
subNavLinks = $.makeArray($('#navigation-2-v8 a:not(.firstactiv)'));
my.log('replay: “' + getActiveSubPageText() + '”: found ' + subNavLinks.length + ' subcategories');
preloadSubPages(subNavLinks);
}
// loads the next sub page in the nav links array.
function loadNextSubPage() {
// load sub nav links first
isTopLevelLink = subNavLinks.length === 0;
currentNav = $((isTopLevelLink ? navLinks : subNavLinks).shift());
my.log('replay: loading next page ' + $.trim(currentNav.text()), currentNav);
if(navLinkIsSelected(currentNav)) {
my.log('replay: skipping initially selected page, has been processed already.', currentNav);
window.setTimeout(loadNextSubPage, 5);
} else {
currentNav.click();
}
}
function orderDetailsNextStepElements() {
return $('.shop-dialog a:contains("nächster Schritt"), .shop-dialog a:contains("Extras")');
}
function orderDetailsHasMoreSteps() {
return orderDetailsNextStepElements().length > 0;
}
function orderDetailsGotoNextStep() {
var elem = orderDetailsNextStepElements().first();
elem.click().remove();
}
// Closes order details (like extra ingredients or menu items) if present.
function orderDetailsClose() {
$('.shop-dialog:visible a:contains("in den Warenkorb"):first').click();
}
function orderDetailsRemoveAutoAddedExtras(extras) {
if(extras.length === 0) {
return extras;
}
// See if they were included by pizza.de magic and assume they are included
var completeItem = $('.shop-dialog .dlg-head').text();
extras = $.grep(extras, function(extra) {
var found = completeItem.indexOf(extra) >= 0;
if(found) {
my.log('replay: | removing subitem ' + extra + ' because it appears it was auto-added.');
return false;
}
return true;
});
return extras;
}
function orderDetailsAddErrorsForMissingExtras(extras, errmsg) {
$.each(extras, function(ind, extra) {
my.log('replay: | missing extra: "' + extra + '"');
errorMsgs.push(errmsg + ' EXTRA NOT FOUND: ' + extra);
});
}
// searches current sub page and adds found items to basket. The
// items get removed from the "to go" list
function addItemsToBasket() {
my.log('replay: searching page “' + getActiveSubPageText() + '” for items');
items = $.grep(items, function (item, ind) {
// Deposit is added automatically when selecting the correct products
if(item.prod === 'Pfand') {
return false;
}
var link = findLinkWithText(item.prod);
if(link.length === 0) {
// not found; keep in queue
return true;
}
if(link.length >= 2) {
errorMsgs.push('ITEM #' + ind + ' AMBIGUOUS: ' + item.prod);
// keep item, so it may be added manually later
return true;
}
my.log('replay: found item “' + item.prod + '”');
my.log('replay: | searching for extras: ' + item.extra.join(' | '));
var errmsg = 'product='+item.prod+' | ';
// exactly one link found. Add it to cart or open extra
// ingredients popup. If an item can't have extra ingredients
// this will immediately put the item in the cart.
link.click();
// add extra ingredients, if any.
var lookAgain = false;
do {
item.extra = $.grep(item.extra, function(extra) {
// .shop-dialog == the popup
// .dlg-nodes == the "add items part". Required if
// an ingredient should be added multiple times. Otherwise
// the remove item link would be catched as well.
// .
var ingred = $('.shop-dialog .dlg-nodes a:contains('+extra+')');
if(ingred.length === 0) {
return true; // keep extra item for later
}
if(ingred.length >= 2) {
console.warn('replay: | found two links for "' + extra + '"');
errorMsgs.push(errmsg + ' EXTRA AMBIGUOUS: ' + extra);
return false; // remove item from list
}
ingred.click();
my.log('replay: | added extra "' + extra + '"');
// Since we found one item, already checked extra may have become
// available. This happens in multi-step-menus.
lookAgain = true;
return false;
});
item.extra = orderDetailsRemoveAutoAddedExtras(item.extra);
if(item.extra.length === 0) {
break;
}
if(lookAgain || orderDetailsHasMoreSteps()) {
my.log('replay: | missing extra, going to next step. Extras: ' + item.extra.join(', '));
orderDetailsGotoNextStep();
}
} while(lookAgain);
orderDetailsAddErrorsForMissingExtras(item.extra, errmsg);
orderDetailsClose();
// comparing prices as sanity check
if(getPriceOfLastItem() !== item.price) {
var msg = errmsg + 'Prices do not match. Expected: ' + item.price + ' Actual price: ' + getPriceOfLastItem();
console.warn(msg);
errorMsgs.push(msg);
}
// remove item from list
return false;
});
my.log('ITEM IN BASKET COUNT: ' + getCartItemsCount());
}
// this function does the actual work of finding the items and adding
// them to the basket. Because the category sub pages are loaded by
// asynchronous JavaScript magic it's not possible to simply loop
// over all categories. Instead, this function is called once to load
// a new subpage and exit. It is called again by listening to changes
// in the webpage (see below). In this call it iterates over all items
// which still need to be found and adds them to the basket. It also
// resets the current category and executes again in a bit. This
// repeats until all items are found or there are no more sub pages.
function process() {
var endOfCats = navLinks.length === 0 && subNavLinks.length === 0;
if(items.length === 0 || (currentNav === null && endOfCats)) {
return tearDown();
}
if(currentNav === null) {
loadNextSubPage();
loadingWatchdog.begin();
} else {
loadingWatchdog.end();
// reset sub page
currentNav = null;
addItemsToBasket();
// check if there are any subpages which also need processing
// before going to the next category. Only do this on top level
// links, otherwise this would cause infinite loops.
if(isTopLevelLink) {
getPossibleSubLinks();
}
// continue with next step
process();
}
}
function missingItemsToErrors() {
if(items.length > 0) {
var list = $.map(items, function(item) {
var m = item.prod;
if(item.extra.length > 0) {
m += ' + ' + item.extra.join(' + ');
}
return m;
}).join('\n – ');
errorMsgs.push('Missing Items:\n – ' + list);
}
}
function checkFinalSum() {
var should = parseFloat(window.hipsterReplayFinalSum);
var have = my.textPriceToFloat($('.total').text());
if(should !== have) {
errorMsgs.push('Final sum does not match. Should be ' + should + '€, but have ' + have + '€. Check for missing products.');
}
}
function tearDown() {
my.log('replay: tear down');
missingItemsToErrors();
checkFinalSum();
$('#inhalt').unbind('content_ready', process);
loadingWatchdog.end();
// avoid content changes on insta mode because the form is submitted
// immediately anyway.
if(window.hipsterReplayMode !== 'insta') {
$('body').removeClass('wait');
$('#hipster-progress').hide();
setSubmitButtonState(true);
$.fx.off = false;
} else {
// allow form submission
getSubmitButton().enable(true);
}
if(typeof finishCallback === 'function') {
my.log('replay: running callback');
finishCallback(errorMsgs);
}
}
// listen to the same event as pizza.de for content loading
$('#inhalt').bind('content_ready', process);
// start processing
currentNav = 'currentlyLoadedPage';
process();
}
function getSubmitButton() {
return $('#hipsterOrderSubmitButton');
}
function setSubmitButtonState(enabled) {
var btn = getSubmitButton();
if(enabled) {
if(btn.is(':disabled')) {
btn.attr('class', 'btn btn-primary navbar-btn').enable(true);
}
} else {
if(btn.is(':enabled')) {
btn.attr('class', 'btn btn-link navbar-btn').enable(false);
}
}
}
function runAfterLoads() {
for(var i = 0; i < _runAfterLoad.length; i++) {
_runAfterLoad[i]();
}
_runAfterLoad = null;
}
function setupMutationObserver() {
var MutationObserver = window.MutationObserver || window.WebKitMutationObserver;
var waitForLoad = new MutationObserver(function(mutations, observer) {
if(isLoading()) {
return;
}
observer.disconnect();
runAfterLoads();
});
waitForLoad.observe(window.document, { childList: true, subtree: true });
}
function setupLegacyObserver() {
var check = function() {
if(isLoading()) {
return;
}
$('body').unbind('DOMSubtreeModified', check);
// wait until DOMSubtreeModified event is over to match mutation
// observer behaviour. If not done so, events might still fire for
// *this* modification, instead of the next one.
window.setTimeout(runAfterLoads, 0);
};
$('body').bind('DOMSubtreeModified', check);
}
function restorePrefilledAddress(elm) {
elm = $(elm);
var field = elm.attr('name').replace(/^odr_/, '');
var v = window.hipsterPrefillAddress[field];
if(typeof v === 'undefined' || v === null) {
return;
}
elm.val(v);
}
function restoreLocalStorage(elm) {
elm = $(elm);
var v = localStorage['hipsterpizza_' + elm.attr('name')];
if(my.isBlank(v)) {
return;
}
if(elm.attr('type') === 'radio') {
$('input[name="'+elm.attr('name')+'"][value="'+v+'"]').click();
} else {
elm.val(v);
}
}
function setLocalStorage(elm) {
elm = $(elm);
var v = elm.val();
if(v === '') {
v = null;
}
my.log('storing: ' + elm.attr('name') + ' = ' + v);
localStorage['hipsterpizza_' + elm.attr('name')] = v;
}
try {
setupMutationObserver();
} catch(error) {
my.log('Using legacy observer (DomSubtreeModified) because MutationObserver seems broken.');
setupLegacyObserver();
}
// PUBLIC ////////////////////////////////////////////////////////////
return {
hideOrderFieldsAppropriately: function() {
if(my.getCurrentMode() === 'pizzade_basket_submit') {
my.log('Not hiding address fields because we want to submit the group basket');
return;
}
$('body').addClass('hideOrderFields');
},
runAfterLoad: function(func) {
if(!isLoading()) {
func();
return;
}
_runAfterLoad.push(func);
},
getShopName: function() {
if(!isShopPage()) {
return null;
}
return $('title').text();
},
getShopFaxNumber: function() {
var n = window.cart.config.store.fax;
n = n.replace(/[^0-9+]/g, '');
n = n.replace(/^0/, '+49');
return n;
},
detectAndSetShop: function() {
var button = $('#hipsterShopChooser');
button.enable();
button.attr('class', 'btn btn-primary navbar-btn');
var hidden = $('#hipsterShopCanonicalUrl');
hidden.val($('link[rel=canonical]').attr('href'));
hidden = $('#hipsterShopName');
hidden.val(hipster.getShopName());
hidden = $('#hipsterShopFaxNumber');
hidden.val(hipster.getShopFaxNumber());
hidden = $('#hipsterShopUrlParams');
hidden.val(window.location.search);
button.show();
if(window.hipsterSubmitAfterShopDetect) {
button.click();
}
},
// Disables annoying popups for common users like “shop closed, preorder?”.
// This also disables the PLZ/delivery area selector popup. The popup is
// required to properly set up pizza.de for submission. If the
// shop_url_params were set properly, the “popup” only contains a JavaScript
// snippet which sets the required values automatically. See issue #23
// starting from this comment:
// https://github.com/breunigs/hipsterpizza/issues/23#issuecomment-60237891
disableAnnoyingPopups: function() {
if(!window.cart) {
return;
}
if(my.getCurrentMode() === 'pizzade_basket_submit') {
my.log('Not hiding annoying popups because we want to submit the group basket');
return;
}
// URLs without &knddomain=1 switch
window.cart.check4DeliveryArea = function() {};
// URLs with that switch
window.cart.config.behavior.checkDeliveryAreaOnCustDomains = 0;
},
bindSubmitButton: function() {
var form = getSubmitButton().parents('form');
form.submit(function() {
var items = getCartItemsJson();
if(items.length === 0) {
window.alert('You need to select at least one item.');
return false;
}
$('#hipsterOrderJson').val(JSON.stringify(items));
// do not ask for user’s nick if editing an order
if(my.getCookie('mode') === 'pizzade_order_edit') {
return true;
}
var nick = getUserNick();
if(nick === null) {
// user clicked cancel in dialog, abort
return false;
}
$('#hipsterOrderNick').val(nick);
});
},
replayData: function() {
var data = window.hipsterReplayData;
var mode = window.hipsterReplayMode;
if(typeof data === 'undefined' || data === null) {
return;
}
switch(mode) {
case 'check':
my.log('Replaying with error checking');
replay(data, function(err) {
if(err.length === 0) {
return;
}
window.alert('There have been errors replaying the data: \n– ' + err.join('\n– '));
});
break;
case 'nocheck':
replay(data);
break;
case 'insta':
// if a nickname is already set, simply re-use it without asking.
var curNick = my.getCookie('nick');
if(!my.isBlank(curNick)) {
getUserNick = function() { return curNick; };
}
replay(data, function() { getSubmitButton().click(); });
break;
default:
my.err('Invalid replay mode, no action taken');
}
},
attachAddressFieldListener: function() {
if(!localStorage || window.hipsterPrefillAddress) {
return;
}
// pizza.de replaces the whole sidebar when adding/removing items.
// Therefore this broad delegate is needed.
$('body').on('change', 'form#bestellform input, form#bestellform textarea', function() {
setLocalStorage(this);
});
},
runItemCountChecker: function() {
if(!isShopPage() || window.hipsterReplayMode === 'insta') {
return;
}
var ca = my.getCurrentMode();
if(ca !== 'pizzade_order_new' && ca !== 'pizzade_order_edit') {
return;
}
window.setInterval(function() {
setSubmitButtonState(getCartItemsCount() !== 0);
}, 1000);
},
restoreAddressFields: function() {
if(!localStorage) {
return;
}
$('form#bestellform input, form#bestellform textarea').each(function(idx, elm) {
if(window.hipsterPrefillAddress) {
restorePrefilledAddress(elm);
} else {
restoreLocalStorage(elm);
}
});
},
attachShaAddress: function() {
$('#hipsterSetSubmitTime').on('submit', function() {
var addr = getShaAddress();
if(addr !== null) {
$('#hipsterShaAddress').val();
}
});
},
autoFillPostalCode: function() {
if(window.location.pathname === '/pizzade_root') {
getPostalCode();
}
}
};
})();
hipster.disableAnnoyingPopups();
hipster.autoFillPostalCode();
hipster.runAfterLoad(function() {
'use strict';
hipster.bindSubmitButton();
hipster.replayData();
hipster.detectAndSetShop();
hipster.hideOrderFieldsAppropriately();
hipster.restoreAddressFields();
hipster.attachAddressFieldListener();
hipster.attachShaAddress();
hipster.runItemCountChecker();
});