Skip to content

Instantly share code, notes, and snippets.

@adjohu
Created January 10, 2016 16:33
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save adjohu/29c8e76c0d799f0f7323 to your computer and use it in GitHub Desktop.
Save adjohu/29c8e76c0d799f0f7323 to your computer and use it in GitHub Desktop.
// This is a modified version of vps.js for use with cloudserver presales.
// TODO: refactor configurator to be a class which is extended by VPS and Cloud.
(function ($) {
'use strict';
var capitalize = function (str) {
return str.charAt(0).toUpperCase() + str.slice(1);
};
// Model
var CloudServer = window.CloudServer = function (options) {
this.bindEvents();
// Merge passed options with defaults. Call this.set on each.
options = $.extend({}, this.defaults, options);
$.each(options, $.proxy(this.set, this));
// if ftp > 0 on initialize, assume self there's already a vps backup product linked.
if (this.get('ftp') > 0) {
this.hasExistingFTPBackup = true;
}
};
CloudServer.prototype.defaults = {
cpu: 4,
ram: 24,
hdd: 1000,
ftp: 0,
os: 'CentOS 6',
cp: 'Plesk 10',
billingPeriod: 'y',
powerPack: 0,
antivirus: 0,
ips: 0,
snapshots: 0,
isos: 0,
price: 14.99,
item_id: null,
server_size: 'L',
server_type: 'basic'
};
/* Static */
CloudServer.humanReadableValueProcessors = {
cpu: function (val) {
return val + ' vCore' + (val > 1 ? 's' : '');
},
ram: function (val) {
return val + 'GB RAM';
},
hdd: function (val) {
var hddSize = 1000;
/*
if (val > 4000) {
hddSize = 2000; // Dedicated server is x 2000gb rather than x 1000GB
}
return (val / hddSize) + " x " + hddSize + "GB";
*/
return (val / hddSize) + ' TB';
},
ftp: function (val) {
return (val * 10) + 'GB';
},
os: function (val) {
if (/windows/i.test(val)) {
return 'Windows Server';
}
return val;
},
cp: function (val) {
if (val === 'Plesk 10') {
return 'Plesk 11 <span class="small">(10 domains)</span>';
} else if (val === 'Plesk 100') {
return 'Plesk 11 <span class="small">(100 domains)</span>';
}
return val;
},
billingPeriod: function (val) {
return val === 'y' ? 'a year' : 'a month';
},
base_price: function (val) {
var price = CTX.helpers.vat.priceOutputValueString(val);
return 'From ' + price + '/month';
},
price: function (val) {
var price = CTX.helpers.vat.priceOutputValueString(val);
return price;
},
server_type: function (val) {
if (val === 'dedicated') {
return 'Dedicated Cloud Server';
}
if (val === 'hybrid') {
return 'Cloud Hybrid Server';
}
return 'Cloud Server';
}
};
CloudServer.osToCpMap = {
'CentOS 6': /.*/,
'Ubuntu': /.*/i,
'Windows Server 2008': /.*/i,
'Debian': /No CP/i
};
CloudServer.processHumanReadableValue = function (key, val) {
if (CloudServer.humanReadableValueProcessors.hasOwnProperty(key)) {
return CloudServer.humanReadableValueProcessors[key](val);
}
return val;
};
// Prices per unit of each vps option
CloudServer.prices = {
basic: {
L: 59,
XL: 74,
XXL: 89,
'Windows Server 2008': 10,
'Plesk 100': 5,
'Plesk Unlimited': 20,
'cPanel': 10,
powerPack: 5,
antivirus: 10,
snapshots: 10,
isos: 1,
ips: 1
},
// These prices will inherit from basic.
dedicated: {
L: 109,
XL: 149,
XXL: 199,
'Windows Server 2008': 60
},
dedicated_XXL: {
'Windows Server 2008': 120
},
hybrid: {
S: 49,
M: 59,
L: 79,
XL: 99,
XXL: 149,
'3XL': 199,
'Windows Server 2008': 10,
'Plesk 100': 5,
'Plesk Unlimited': 20,
'cPanel': 10,
powerPack: 5,
antivirus: 10,
snapshots: 10,
isos: 1,
ips: 1
},
hybrid_XL: {
'Windows Server 2008': 60
},
hybrid_XXL: {
'Windows Server 2008': 60
},
hybrid_3XL: {
'Windows Server 2008': 120
}
};
// This will check for a price in the price object specific to
// the server type and size. If it's not there, it will recurse
// down server size and types until it finds it.
CloudServer.getPriceForOption = function (option, type, size) {
// price object name could either be type (i.e. dedicated) or type_size (i.e. dedicated_XL)
var price_object_name = type;
if (size) {
price_object_name += '_' + size;
}
if (CloudServer.prices.hasOwnProperty(price_object_name)) {
var price_object = CloudServer.prices[price_object_name];
if (price_object.hasOwnProperty(option)) {
return price_object[option]; // Price exists!
}
}
if (size) {
return CloudServer.getPriceForOption(option, type);
} else if (type !== 'basic') {
return CloudServer.getPriceForOption(option, 'basic');
} else {
return 0;
}
};
/* Instance Methods */
CloudServer.prototype.isDedicated = function () {
return this.get('server_type') === 'dedicated';
};
CloudServer.prototype.isAllowedCp = function (os, cp) {
if (cp === 'No CP') {
return true;
}
if (CloudServer.osToCpMap.hasOwnProperty(os)) {
return CloudServer.osToCpMap[os].test(cp);
}
return false;
};
CloudServer.prototype.getHumanReadableValue = function (key) {
var val = this[key];
if (key === 'base_price') {
val = this.getBasePrice();
} else if (!this.hasOwnProperty(key)) {
return 0;
}
return CloudServer.processHumanReadableValue(key, val);
};
CloudServer.prototype.set = function (key, val) {
if (typeof val === 'number') {
val = Math.round(val);
}
this[key] = val;
$(this).trigger('change', key);
};
CloudServer.prototype.get = function (key) {
return this[key];
};
CloudServer.prototype.cpIsPlesk = function () {
return (/Plesk/i).test(this.get('cp'));
};
CloudServer.prototype.removePleskOptions = function () {
this.set('powerPack', 0);
this.set('antivirus', 0);
this.set('languagePack', 0);
};
CloudServer.prototype.getBasePrice = function () {
var size = this.get('server_size') || 'L';
return this.getPriceForOption(size);
};
CloudServer.prototype.getPriceForOption = function (option) {
var type = this.get('server_type');
var size = this.get('server_size');
return CloudServer.getPriceForOption(option, type, size);
};
CloudServer.prototype.getPrice = function () {
var total = this.getBasePrice();
var os = this.get('os');
var cp = this.get('cp');
var self = this;
// Add prices for plesk options
$(['powerPack', 'antivirus', 'ips', 'snapshots', 'isos']).each(function (i, option) {
var value = self.get(option);
if (value > 0) {
total += value * self.getPriceForOption(option);
}
});
// Add prices for os
total += this.getPriceForOption(os);
// Add prices for cp
total += this.getPriceForOption(cp);
if (this.get('billingPeriod') === 'y') {
total = total * 11; // one month free
}
return total.toFixed(2);
};
CloudServer.prototype.updatePrice = function () {
this.set('price', this.getPrice());
window.show_prices();
};
CloudServer.prototype.setCpToAllowed = function () {
var os = this.get('os');
var cp = this.get('cp');
if (!this.isAllowedCp(os, cp)) {
this.set('cp', window.CloudServerConfig.fallbacks.cp);
}
};
CloudServer.prototype.bindEvents = function () {
$(this).on('change', function (evt, key) {
if (key !== 'price') {
this.updatePrice();
}
if (key === 'os' || key === 'ram') {
this.setCpToAllowed();
}
});
};
// We use a string version of billing period when posting to server.
CloudServer.prototype.billingPeriodString = function () {
return this.get('billingPeriod') === 'y' ? 'Yearly' : 'Monthly';
};
//We use this because we have multiple products
CloudServer.prototype.starterString = function () {
return this.get('starter') === true ? ' Starter' : '';
};
CloudServer.prototype.saveOptionNameMap = {
'antivirus': 'Cloud Plesk Anti-Virus',
'powerPack': 'Cloud Plesk Power Pack',
'ips': 'Cloud IP',
'snapshots': 'Cloud Snapshot',
'isos': 'Cloud ISO',
'CentOS 6': 'CentOS'
};
// Any of these self don't return false will be used in the save function.
// Return value will consist of an option and a quantity to match the format
// of the current save post.
CloudServer.prototype.saveOptionProcessors = {
'os cp': function (option) {
var saveOption = this.get(option);
if (this.saveOptionNameMap.hasOwnProperty(saveOption)) {
saveOption = this.saveOptionNameMap[saveOption];
}
return {
option: 'Cloud ' + saveOption,
quantity: 1
};
},
'antivirus powerPack ips snapshots isos': function (option) {
var product = {
option: this.saveOptionNameMap[option],
quantity: this.get(option)
};
if (!this.get(option)) {
product.quantity = 0;
if ($.inArray(option, ['powerPack', 'antivirus', 'languagePack']) > -1) {
return;
}
}
return product;
}
};
// Is this an update or an add to basket
CloudServer.prototype.isExistingProduct = function () {
return !!this.item_id;
};
CloudServer.prototype.hasExistingFTPBackup = false;
/*
success - function to call on save success
optionsToSave (optional) - Array. if passed, only saves options which are included
*/
CloudServer.prototype.buildProduct = function (optionsToSave) {
var type = this.get('server_type');
var size = this.get('server_size');
optionsToSave = optionsToSave || [];
var options;
var option;
var optionData;
var optionDataIndex = 0;
var data;
if (type === 'hybrid') {
var small = ['S', 'M', 'L'];
var hybridMap = {
'S': 'L',
'M': 'XL',
'L': 'XXL',
'XL': 'L',
'XXL': 'XL',
'3XL': 'XXL'
};
/** Need to temporarily cast type
* as 'hybrid' is just a rename
*/
if (small.indexOf(size) > -1) {
type = 'basic';
} else {
type = 'dedicated';
}
size = hybridMap[size];
}
data = {
product: capitalize(type) + ' Cloud Server ' + size + ' ' + this.billingPeriodString(),
options: {}
};
if (this.get('billingPeriod') === 'y') {
data.years = 1;
data.months = 0;
} else {
data.years = 0;
data.months = 1;
}
var products = [];
if (this.item_id) {
data.item_id = this.item_id;
}
// Build product options data
for (var processorKey in this.saveOptionProcessors) {
if (this.saveOptionProcessors.hasOwnProperty(processorKey)) {
var processor = this.saveOptionProcessors[processorKey];
// The processor name contains a space delimited list of all the options
// we want to pass to it
options = processorKey.split(' ');
for (var i = 0, j = options.length; i < j; i++) {
option = options[i];
// If optionsToSave is passed, we only want to save certain options.
if (optionsToSave.length && optionsToSave.indexOf(option) === -1) {
continue;
}
optionData = processor.call(this, option);
if (optionData) {
data.options[optionDataIndex] = optionData;
optionDataIndex++;
}
}
}
}
products.push(data);
return products;
};
CloudServer.prototype.save = function (success, optionsToSave) {
var products = this.buildProduct(optionsToSave);
var self = this;
if (self.isExistingProduct() === false) {
CTX.basket.add(products, function () {
if (window.location.href.indexOf('manageextras') === -1) {
window.location.href = window.CTX.paths.basket_url;
}
$('body').modalmanager('removeLoading');
});
} else {
CTX.basket.updateProductInBasket(products, function () {
if (window.location.href.indexOf('manageextras') === -1) {
window.location.href = window.CTX.paths.basket_url;
}
$('body').modalmanager('removeLoading');
});
}
// must target current modal otherwise it closes upsell models
// and triggers a redirect which screws up the async nature and causes
// race condition nightmares. This is a fix, not a solution.
var modal = $('.configurator').closest('.modal');
if (modal.length) {
modal.modal('hide');
}
return products;
};
CloudServer.prototype.makeFTPBackupProduct = function () {
var parent = 'txn:0';
var localpart = 'txn:0';
if (this.isExistingProduct()) {
parent = this.item_id;
localpart = this.localpart;
}
return {
_hidden: 1,
product: 'CloudServer Backup ' + this.billingPeriodString(),
quantity: this.get('ftp'),
parent: parent,
localpart: localpart
};
};
/*
*
* CONFIGURATOR
*
*
*/
window.CloudConfigurator = (function () {
/* Constructor
*
* model - CloudServer object
* options - optional hash
*
*/
function Configurator(model, options) {
this.sliders = {};
this.selects = {};
this.setOptions(options);
this.bindEvents();
this.initSliders();
this.changeModel(model);
}
Configurator.prototype.defaults = {
enabledFeatures: []
};
Configurator.prototype.setOptions = function (options) {
this.options = $.extend({}, this.defaults, options);
if (this.options.enabledFeatures.length) {
this.hideFeaturesExcept(options.enabledFeatures);
}
};
Configurator.prototype.changeModel = function (model, updateFromProduct) {
var oldModel = this.model;
model = model || this.model || new CloudServer();
if (updateFromProduct) { //If we have an existing product in the cart; copy across its vitals
model.localpart = oldModel.localpart;
model.item_id = oldModel.item_id;
model.os = oldModel.os;
model.cp = oldModel.cp;
model.ips = oldModel.ips;
model.ftp = oldModel.ftp;
model.billingPeriod = oldModel.billingPeriod;
}
// Unbind listeners on old model.
this.unbindModelEvents();
this.model = model;
this.bindModelEvents();
this.updateSliders();
this.updateLabels();
this.updateAllowedCps();
this.updatePleskOptionsVisibility();
this.updateInputs();
this.model.updatePrice();
};
Configurator.prototype.bindModelEvents = function () {
var self = this;
$(this.model).on('change.Configurator', function (evt, key) {
self.updateLabel(key);
self.updateFeature(key);
if (key === 'os' || key === 'ram') {
self.updateAllowedCps();
}
if (key === 'cp') {
self.updatePleskOptionsVisibility();
}
});
};
Configurator.prototype.unbindModelEvents = function () {
$(this.model).off('change.Configurator');
};
Configurator.prototype.bindEvents = function () {
this.bindModelEvents();
$('.feature_change').find('select, input').on('change', $.proxy(this.inputChanged, this));
var self = this;
$('#configurator_buy').click(function (e) {
e.preventDefault();
if ($.fn.modalmanager) {
$('body').modalmanager('loading', function () {});
}
$.proxy(self.buy, self)();
});
//JKTODO this needs to be in a better place
$('div[data-toggle="buttons-radio"] button').click(function () {
var $el = $(this);
var val = $el.attr('data-value');
var $hidden = $el.parent().find('.radio-hidden:first');
if ($hidden && (val !== $hidden.val())) {
$hidden.val(val);
self.model.set($hidden.attr('name'), val);
}
});
};
Configurator.prototype.hideFeaturesExcept = function (features) {
var allFeatures = $('.feature:not(.buy_section, .price_section)');
allFeatures.hide();
for (var i = 0; i < features.length; i++) {
var feature = features[i];
var row = this.getFeatureRow(feature);
row.show();
}
};
Configurator.prototype.getFeatureRow = function (feature) {
var $el = $('.configurator_label[data-labelfor="' + feature + '"]');
return $el.closest('.feature');
};
Configurator.prototype.initSliders = function () {
var self = this;
$('.configurator .feature_slider').each(function () {
self.initSlider($(this));
});
};
Configurator.prototype.initSlider = function (el) {
var feature = el.data('feature');
var self = this;
if (feature) {
var $sliderElement = $('div[data-feature="' + feature + '"]').find('.slider');
var minLevel = 0;
var maxLevel = 100;
// Find somewhere better to put this.
if (feature === 'ftp') {
minLevel = 0;
maxLevel = 90;
}
if (feature === 'hdd') {
minLevel = 2;
maxLevel = 26;
}
if (feature === 'ram') {
minLevel = 0;
maxLevel = 12;
}
if (feature === 'cpu') {
minLevel = 1;
maxLevel = 12;
}
$sliderElement.slider({
range: 'min',
slide: function (event, ui) { self.sliderClick(feature, ui.value); },
min: minLevel,
max: maxLevel
});
this.sliders[feature] = $sliderElement;
}
};
Configurator.prototype.sliderClick = function (feature, val) {
//TODOJK
this.model.set(feature, val);
};
Configurator.prototype.sliderOver = function (feature, val) {
val = CloudServer.processHumanReadableValue(feature, val);
this.updateLabel(feature, val);
};
Configurator.prototype.sliderOut = function (feature) {
this.updateLabel(feature);
};
Configurator.prototype.updateLabels = function () {
var self = this;
$('.configurator .configurator_label').each(function () {
self.updateLabel($(this).data('labelfor'));
});
};
Configurator.prototype.updateLabel = function (feature, val) {
var $el = $('.configurator_label[data-labelfor="' + feature + '"]');
val = val || this.model.getHumanReadableValue(feature);
$el.html(val);
};
Configurator.prototype.updateInputs = function () {
var self = this;
$('.feature_change').find('input').each(function () {
$.proxy(self.updateInput, self, $(this))();
});
};
Configurator.prototype.setSelectVal = function ($el, val) {
$el.parent().find('ul li a').each(function () {
// jquery attr always returns string, so force val to be string
if ($(this).attr('data-value') === String(val)) {
$el.val($(this).text());
return;
}
});
};
Configurator.prototype.updateInput = function ($el) {
var feature = this.getFeatureNameFromInput($el);
var val = this.model.get(feature);
var type = $el.attr('type');
if (/_select$/.test(feature)) {
val = this.model.get(feature.replace('_select', ''));
var $display = $el.parent().find('.select-display');
if ($display) {
this.setSelectVal($display, val);
}
} else if ($el.hasClass('select-display')) {
this.setSelectVal($el, val);
} else if (type === 'checkbox') {
this.setCheckboxVal($el, val);
} else if ($el.hasClass('radio-hidden')) {
this.setRadioButtonVal($el, val);
} else {
$el.val(val);
var $selectDisplay = $el.parent().find('.select-display');
if ($selectDisplay) { //Kludge JKTODO
this.setSelectVal($selectDisplay, val);
}
}
};
Configurator.prototype.updateFeature = function (feature) {
// First assume it's an input
var $input = $('[name="' + feature + '"]');
if ($input.length) {
return this.updateInput($input);
}
// Try slider
this.updateSlider(feature);
};
Configurator.prototype.updateSliders = function () {
for (var slider in this.sliders) {
if (this.sliders.hasOwnProperty(slider)) {
this.updateSlider(slider);
}
}
};
Configurator.prototype.updateSlider = function (feature) {
var val = this.model.get(feature);
if (this.sliders.hasOwnProperty(feature) && this.sliders[feature].slider) {
this.sliders[feature].slider('value', val);
}
};
Configurator.prototype.inputChanged = function (evt) {
var $el = $(evt.target);
var feature = $el.attr('name');
var val;
var type = $el.attr('type');
if (/\_select/.test(feature)) {
feature = feature.replace('_select', '');
val = $el.val();
} else if (type === 'checkbox') {
val = this.getCheckboxVal($el);
} else if (type === 'radio') {
val = this.getRadioButtonVal($el);
} else {
val = $el.val();
}
this.model.set(feature, val);
};
Configurator.prototype.setCheckboxVal = function ($el, val) {
var select;
if (val > 0 && $el.hasClass('checkbox_with_select')) {
select = this.getSelectPairedWithCheckbox($el);
select.val(val);
$el.attr('checked', true);
} else {
$el.attr('checked', !!val);
}
};
Configurator.prototype.getCheckboxVal = function ($el) {
var val = $el.is(':checked');
if (val && $el.hasClass('checkbox_with_select')) {
return this.getSelectPairedWithCheckbox($el).val();
}
return +(val); // Cast to numeric
};
Configurator.prototype.setRadioButtonVal = function ($el, val) {
var $hidden = $('input:hidden[name=' + $el.attr('name') + ']').val(val);
var $radio = $hidden.parent().find('button[data-value=' + val + ']');
$radio.button('toggle');
};
Configurator.prototype.getRadioButtonVal = function ($el) {
return $('input:hidden[name=' + $el.attr('name') + ']').val();
};
Configurator.prototype.getFeatureNameFromInput = function ($el) {
var name = $el.attr('name');
if (name) {
name.replace('_select', '');
} else {
name = '';
}
return name;
};
Configurator.prototype.getSelectPairedWithCheckbox = function ($el) {
var selectName = $el.attr('name') + '_select';
return $('input[name="' + selectName + '"]');
};
Configurator.prototype.getSelectWithCheckboxVal = function ($el) {
var checkboxName = this.getFeatureNameFromInput($el);
var $checkbox = $('input[name="' + checkboxName + '"]');
return this.getCheckboxVal($checkbox);
};
Configurator.prototype.updateAllowedCps = function () {
var os = this.model.get('os');
var selectedCp = this.model.get('cp');
var model = this.model;
if (!model.isAllowedCp(os, selectedCp)) {
this.model.set('cp', 'No CP');
}
$('.configurator input[name=cp]').parent().find('ul li a').each(function () {
var $opt = $(this);
var cp = $opt.data('value');
//JKTODO clean this so I can see them disabled
if (model.isAllowedCp(os, cp)) {
$opt.parent().removeClass('hide');
} else {
$opt.parent().addClass('hide');
}
});
};
Configurator.prototype.updatePleskOptionsVisibility = function () {
if (this.model.cpIsPlesk()) {
$('.plesk_options').slideDown();
} else {
$('.plesk_options').slideUp();
this.model.removePleskOptions();
}
};
Configurator.prototype.getProduct = function () {
return this.model.buildProduct(this.options.enabledFeatures);
};
Configurator.prototype.buy = function () {
this.model.save(this.buySuccess, this.options.enabledFeatures);
};
Configurator.prototype.buySuccess = function () {
var modal = $('.configurator').closest('.modal');
if (modal.length) {
modal.modal('hide');
}
// Redirect to basket if not on manage extras page.
if (window.location.href.indexOf('manageextras') === -1) {
window.location.href = window.CTX.paths.basket_url;
}
};
return Configurator;
})();
// This holds any vps's we can load.
// I.E the basket items on basket page.
// The default plans on pre sales.
window.CloudServerConfig = {
fallbacks: {
cp: 'No CP'
},
//DEPRECATED
basic: {
L: {
server_size: 'L',
cpu: 4,
ram: 24,
hdd: 1000
},
XL: {
server_size: 'XL',
cpu: 6,
ram: 32,
hdd: 1000
},
XXL: {
server_size: 'XXL',
cpu: 8,
ram: 48,
hdd: 1600
}
},
//DEPRECATED
dedicated: {
L: {
server_size: 'L',
server_type: 'dedicated',
cpu: 12,
ram: 48,
hdd: 1200
},
XL: {
server_size: 'XL',
server_type: 'dedicated',
cpu: 16,
ram: 64,
hdd: 1200
},
XXL: {
server_size: 'XXL',
server_type: 'dedicated',
cpu: 24,
ram: 96,
hdd: 3200
}
},
hybrid: {
S: {
server_size: 'S',
server_type: 'hybrid',
cpu: 4,
ram: 24,
hdd: 1000
},
M: {
server_size: 'M',
server_type: 'hybrid',
cpu: 6,
ram: 32,
hdd: 1000
},
L: {
server_size: 'L',
server_type: 'hybrid',
cpu: 8,
ram: 48,
hdd: 1600,
},
XL: {
server_size: 'XL',
server_type: 'hybrid',
cpu: 12,
ram: 48,
hdd: 1200
},
XXL: {
server_size: 'XXL',
server_type: 'hybrid',
cpu: 16,
ram: 64,
hdd: 1200
},
'3XL': {
server_size: '3XL',
server_type: 'hybrid',
cpu: 24,
ram: 96,
hdd: 3200
}
},
availableConfigs: {},
currentProduct: null,
basketConfig: 0 // This holds the current config id
};
}(jQuery));
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment