diff --git a/order.js b/order.js deleted file mode 100644 index a49d42e..0000000 --- a/order.js +++ /dev/null @@ -1,190 +0,0 @@ -/** - * @license RequireJS order 1.0.5 Copyright (c) 2010-2011, The Dojo Foundation All Rights Reserved. - * Available via the MIT or new BSD license. - * see: http://github.com/jrburke/requirejs for details - */ -/*jslint nomen: false, plusplus: false, strict: false */ -/*global require: false, define: false, window: false, document: false, - setTimeout: false */ - -//Specify that requirejs optimizer should wrap this code in a closure that -//maps the namespaced requirejs API to non-namespaced local variables. -/*requirejs namespace: true */ - -(function () { - - //Sadly necessary browser inference due to differences in the way - //that browsers load and execute dynamically inserted javascript - //and whether the script/cache method works when ordered execution is - //desired. Currently, Gecko and Opera do not load/fire onload for scripts with - //type="script/cache" but they execute injected scripts in order - //unless the 'async' flag is present. - //However, this is all changing in latest browsers implementing HTML5 - //spec. With compliant browsers .async true by default, and - //if false, then it will execute in order. Favor that test first for forward - //compatibility. - ohImBadVar3 = 'fuuuu'; - var testScript = typeof document !== "undefined" && - typeof window !== "undefined" && - document.createElement("script"), - - supportsInOrderExecution = testScript && (testScript.async || - ((window.opera && - Object.prototype.toString.call(window.opera) === "[object Opera]") || - //If Firefox 2 does not have to be supported, then - //a better check may be: - //('mozIsLocallyAvailable' in window.navigator) - ("MozAppearance" in document.documentElement.style))), - - //This test is true for IE browsers, which will load scripts but only - //execute them once the script is added to the DOM. - supportsLoadSeparateFromExecute = testScript && - testScript.readyState === 'uninitialized', - - readyRegExp = /^(complete|loaded)$/, - cacheWaiting = [], - cached = {}, - scriptNodes = {}, - scriptWaiting = []; - - //Done with the test script. - testScript = null; - - //Callback used by the type="script/cache" callback that indicates a script - //has finished downloading. - function scriptCacheCallback(evt) { - var node = evt.currentTarget || evt.srcElement, i, - moduleName, resource; - - if (evt.type === "load" || readyRegExp.test(node.readyState)) { - //Pull out the name of the module and the context. - moduleName = node.getAttribute("data-requiremodule"); - - //Mark this cache request as loaded - cached[moduleName] = true; - - //Find out how many ordered modules have loaded - for (i = 0; (resource = cacheWaiting[i]); i++) { - if (cached[resource.name]) { - resource.req([resource.name], resource.onLoad); - } else { - //Something in the ordered list is not loaded, - //so wait. - break; - } - } - - //If just loaded some items, remove them from cacheWaiting. - if (i > 0) { - cacheWaiting.splice(0, i); - } - - //Remove this script tag from the DOM - //Use a setTimeout for cleanup because some older IE versions vomit - //if removing a script node while it is being evaluated. - setTimeout(function () { - node.parentNode.removeChild(node); - }, 15); - } - } - - /** - * Used for the IE case, where fetching is done by creating script element - * but not attaching it to the DOM. This function will be called when that - * happens so it can be determined when the node can be attached to the - * DOM to trigger its execution. - */ - function onFetchOnly(node) { - var i, loadedNode, resourceName; - - //Mark this script as loaded. - node.setAttribute('data-orderloaded', 'loaded'); - - //Cycle through waiting scripts. If the matching node for them - //is loaded, and is in the right order, add it to the DOM - //to execute the script. - for (i = 0; (resourceName = scriptWaiting[i]); i++) { - loadedNode = scriptNodes[resourceName]; - if (loadedNode && - loadedNode.getAttribute('data-orderloaded') === 'loaded') { - delete scriptNodes[resourceName]; - require.addScriptToDom(loadedNode); - } else { - break; - } - } - - //If just loaded some items, remove them from waiting. - if (i > 0) { - scriptWaiting.splice(0, i); - } - } - - define({ - version: '1.0.5', - - load: function (name, req, onLoad, config) { - var hasToUrl = !!req.nameToUrl, - url, node, context; - - //If no nameToUrl, then probably a build with a loader that - //does not support it, and all modules are inlined. - if (!hasToUrl) { - req([name], onLoad); - return; - } - - url = req.nameToUrl(name, null); - - //Make sure the async attribute is not set for any pathway involving - //this script. - require.s.skipAsync[url] = true; - if (supportsInOrderExecution || config.isBuild) { - //Just a normal script tag append, but without async attribute - //on the script. - req([name], onLoad); - } else if (supportsLoadSeparateFromExecute) { - //Just fetch the URL, but do not execute it yet. The - //non-standards IE case. Really not so nice because it is - //assuming and touching requrejs internals. OK though since - //ordered execution should go away after a long while. - context = require.s.contexts._; - - if (!context.urlFetched[url] && !context.loaded[name]) { - //Indicate the script is being fetched. - context.urlFetched[url] = true; - - //Stuff from require.load - require.resourcesReady(false); - context.scriptCount += 1; - - //Fetch the script now, remember it. - node = require.attach(url, context, name, null, null, onFetchOnly); - scriptNodes[name] = node; - scriptWaiting.push(name); - } - - //Do a normal require for it, once it loads, use it as return - //value. - req([name], onLoad); - } else { - //Credit to LABjs author Kyle Simpson for finding that scripts - //with type="script/cache" allow scripts to be downloaded into - //browser cache but not executed. Use that - //so that subsequent addition of a real type="text/javascript" - //tag will cause the scripts to be executed immediately in the - //correct order. - if (req.specified(name)) { - req([name], onLoad); - } else { - cacheWaiting.push({ - name: name, - req: req, - onLoad: onLoad - }); - require.attach(url, null, name, scriptCacheCallback, "script/cache"); - } - } - } - }); -}()); \ No newline at end of file diff --git a/utilitybelt/__init__.py b/utilitybelt/__init__.py new file mode 100644 index 0000000..3bfbec1 --- /dev/null +++ b/utilitybelt/__init__.py @@ -0,0 +1,6 @@ +try: + import pkg_resources + pkg_resources.declare_namespace(__name__) +except ImportError: + import pkgutil + __path__ = pkgutil.extend_path(__path__, __name__) diff --git a/utilitybelt/apps/Frontend_AU/config.js b/utilitybelt/apps/Frontend_AU/config.js new file mode 100644 index 0000000..479b8ec --- /dev/null +++ b/utilitybelt/apps/Frontend_AU/config.js @@ -0,0 +1,45 @@ +/* + * Config file + * We use RequireJS as a module loader http://requirejs.org/docs/api.html + */ +var requireBaseUrl = '/utilitybelt/utilitybelt'; + +require.config({ + baseUrl: requireBaseUrl, + waitSeconds: 30 +}); + +require(['core/config'], function(config) { + + require(config.coreLibs, function() { + + config.coreInit.apply(core, arguments); + + require([ + /* Inlcude application files */ + 'order!apps/Frontend_AU/namespaces', + 'order!apps/Frontend_AU/config/messages', + 'order!apps/Frontend_AU/pages/Page', + 'order!apps/Frontend_AU/pages/Menu', + 'order!apps/Frontend_AU/pages/Checkout', + 'order!apps/Frontend_AU/pages/RestaurantList', + 'order!apps/Frontend_AU/pages/RestaurantListOpen', + 'order!apps/Frontend_AU/pages/LoggedIn', + 'order!apps/Frontend_AU/pages/Confirm', + 'order!apps/Frontend_AU/pages/LandingPage', + 'order!core/tests/fixtures/Locations' + ], function() { + core.messages.au = _.extend(core.messages.en, core.messages.au); + if (typeof(lh_data) == 'undefined') { + lh_data = {}; + } + if (!lh_data.current_page) { + lh_data.current_page = 'app.Home'; + } + core.Router(lh_data.current_page, lh_data); + }); + }); + +}); + +var APP_LANGUAGE = 'au'; diff --git a/utilitybelt/apps/Frontend_AU/config/messages.js b/utilitybelt/apps/Frontend_AU/config/messages.js new file mode 100644 index 0000000..ebd0383 --- /dev/null +++ b/utilitybelt/apps/Frontend_AU/config/messages.js @@ -0,0 +1,5 @@ + +/** AU application messages */ + +core.messages.au = { +}; diff --git a/utilitybelt/apps/Frontend_AU/menu_page.html b/utilitybelt/apps/Frontend_AU/menu_page.html new file mode 100644 index 0000000..c6a9f70 --- /dev/null +++ b/utilitybelt/apps/Frontend_AU/menu_page.html @@ -0,0 +1,323 @@ + + + + + Title of the document + + + + + + + + + + +
+
+
+ +
+
+
+
+
+
+ +
+
+

Domino's Pizza

+

Schumannstraße, 10117 Berlin

+
+ +
+ +
+
+
+
+
+
+ + + + 5.0/5 +

13 Bewertungen

+
+
+ +
+
+
+ + +
+
+
+ +
+ + + + \ No newline at end of file diff --git a/utilitybelt/apps/Frontend_AU/namespaces.js b/utilitybelt/apps/Frontend_AU/namespaces.js new file mode 100644 index 0000000..d7721fc --- /dev/null +++ b/utilitybelt/apps/Frontend_AU/namespaces.js @@ -0,0 +1,4 @@ +/** Application namespaces */ + +var app = {}; +app.messages = {}; diff --git a/utilitybelt/apps/Frontend_AU/pages/Checkout.js b/utilitybelt/apps/Frontend_AU/pages/Checkout.js new file mode 100644 index 0000000..4e3b896 --- /dev/null +++ b/utilitybelt/apps/Frontend_AU/pages/Checkout.js @@ -0,0 +1,124 @@ +/** + * Order checkout page. + * + * @handle authorize:grant Resaves the order for the logged in user and + * reloads the page with the new order id + */ +core.define('app.Checkout', { + extend: 'app.Page', + plugins: { + 'form': 'jqTransform' + }, + saveActiveAddress: function(order, activeAddress) { + var deliveryAddress = order.get('delivery_address').get('address'); + var fullAddress = { + name: deliveryAddress.get('name'), + lastname: deliveryAddress.get('lastname'), + state: deliveryAddress.get('state'), + suburb: deliveryAddress.get('suburb'), + zipcode: deliveryAddress.get('zipcode'), + street_number: deliveryAddress.get('street_number'), + street_name: deliveryAddress.get('street_name'), + door: deliveryAddress.get('unit_number'), + phone: deliveryAddress.get('phone') + } + var address = $.extend({}, activeAddress, fullAddress); + core.runtime.persist('active_address', address, true); + }, + + options : { + throbberDelay: 1 + }, + + initialize: function() { + var page = this, + restaurant = this.options.restaurant, + user = this.options.user, + cartCollection = new core.collections.Cart(), + orderBootstrap = lh_order_data, + restaurantUrl = this.options.restaurant_url; + + // init the order + var order = new core.models.Order(orderBootstrap); + order.resetCoupon(); //upon page refresh, the payment widget view is rendered preemptively by the CMS. Do not use coupon state information from the API and force an empty coupon. + order.setRestaurant(new core.models.Restaurant({ + id: restaurant.id, + delivery_fees: restaurant.delivery_fees, + min_order_value: restaurant.min_order_value + })); + + var activeAddress = core.runtime.fetch('active_address', true); + page.options.el = 'form.orderform'; + page.options.activeAddress = activeAddress; + page.form = new core.views.CheckoutForm(page.options); + page.form.disableSubmit(); + + if (this.options.user.isAuthorized()) { + if (activeAddress) { + page.form.prefillFromActiveAddress(); + } + order.on('finalized', _.bind(this.saveActiveAddress, this, order, activeAddress)); + } + + var cartView; + cartCollection.order = order; + + var paymentView = new core.views.Payment({ + el: '.newpayment', + basePrice: order.get('price_details').get('price'), + payment_methods: lh_data.restaurant.payment_methods, + order: order + }); + paymentView.on('validateCoupon', _.bind(page.form.updateOrder, page.form)); + + // replace the authorize handler to transfer the order + user.on('authorize:grant', function(user, token) { + var noReload = order.ownedBy(user); + order.changeOwner(user, {success: function(order) { + if (noReload) { + cartCollection.save(); + } + else { + window.location.href = core.utils.formatURL('/checkout/' + order.id); + } + }}); + }); + user.on('authorize:anonym', function(user) { + // unbind this handler to avoid retriggering if a parallel query fails. + user.off('authorize:anonym'); + order.changeOwner(user, {success: function(order) { + window.location.href = core.utils.formatURL('/checkout/' + order.id); + }}); + }); + + order.setUser(this.options.user); + cartCollection.fetch({ + success: function(order, response) { + order.resetCoupon(); //after fetching the model, the payment view is rendered by the widget. Must reset the coupon state once again. + cartView = new core.views.ReadOnlyCartBox({ + title: jsGetText('shopping_cart'), + el: 'section.default-box', + editLink: core.utils.formatURL(restaurantUrl), + cart: { + readOnly: true, + collection: cartCollection + }, + fixable: { + marginTop: '10px', + hideEl: '.TestimonialRestaurant' + } + }); + // Select Order Time: Now as default + if (page.form.setOrder(order)) { + page.form.$('input[name=delivery_time][value=now]').trigger('click'); + page.form.enableSubmit(); + } + order.on("change:price_details", function(){ + cartView.render(); + }); + } + }); + + app.Checkout.__super__.initialize.call(this); + } +}); diff --git a/utilitybelt/apps/Frontend_AU/pages/Confirm.js b/utilitybelt/apps/Frontend_AU/pages/Confirm.js new file mode 100644 index 0000000..e8b5cf3 --- /dev/null +++ b/utilitybelt/apps/Frontend_AU/pages/Confirm.js @@ -0,0 +1,19 @@ +core.define('app.Confirm', { + extend: 'app.Page', + + initialize: function() { + var order = new core.models.Order(lh_order_data); + this.options.user.on('authorize:grant', function(user, token) { + window.location.href = core.utils.formatURL('/'); + }); + + var confirmationView = new core.views.Confirmation({ + order: order, + exit_poll: lh_data['exit_poll'], + is_new_customer: lh_data['is_new_customer'], + el: '.OrderStatusHero' + }); + + app.Confirm.__super__.initialize.call(this); + } +}); diff --git a/utilitybelt/apps/Frontend_AU/pages/LandingPage.js b/utilitybelt/apps/Frontend_AU/pages/LandingPage.js new file mode 100644 index 0000000..297823c --- /dev/null +++ b/utilitybelt/apps/Frontend_AU/pages/LandingPage.js @@ -0,0 +1,37 @@ +/** + * Restaurant list page. + * + * @handle authorize:grant Reloads the page + */ +core.define('app.LandingPage', { + extend : 'app.Page', + + events : { + }, + + plugins : { + }, + + /** + * Initializes Active Address widget. + * + * @handle authorize:grant Reloads the page on successful login + */ + initialize : function() { + + this.activeAddress = new core.views.ActiveAddress({ + el: 'section.activeAddress', + marginTarget: '.landing_page', + closeable: false + }); + + this.activeAddress.showSearchWidget(); + + this.options.user.on('authorize:grant', function(user, token) { + window.location.reload(); + }); + + app.LandingPage.__super__.initialize.call(this); + + } +}); diff --git a/utilitybelt/apps/Frontend_AU/pages/LoggedIn.js b/utilitybelt/apps/Frontend_AU/pages/LoggedIn.js new file mode 100644 index 0000000..bc23174 --- /dev/null +++ b/utilitybelt/apps/Frontend_AU/pages/LoggedIn.js @@ -0,0 +1,88 @@ +/** + * Logged in page. + * + * @handle authorize:revoke Reloads the page + */ +core.define('app.LoggedIn', { + extend : 'app.Page', + + events : { + }, + + plugins : { + }, + + /** + * Constructor + * + * @handle authorize:grant Reloads the page on successful login + */ + initialize: function() { + app.LoggedIn.__super__.initialize.call(this); + this.initWidgets(); + this.options.user.on('authorize:grant authorize:revoke', function(user, token) { + window.location.reload(); + }); + }, + + /** + * Initialise Location Search widget and register event listeners + */ + initWidgets: function() { + var me = this, + ls = { + renderToEl : $('.search_placeholder'), + showCloseButton: false, + wide: true, + show: true + }; + this.locationSearchWidget = new core.views.LocationSearch(ls); + + this.locationSearchWidget.on("locationFound", _.bind(this.locationFound, this)); + this.locationSearchWidget.on("locationNotFound", _.bind(this.showError, this)); + }, + + /** + * Location Found event handler + */ + locationFound: function(ev) { + var me = this; + if (ev && ev.collection) { + if (ev.collection.length > 1) { + if (me.multiLocationLB && me.multiLocationLB.isShown()) { + return; + } + me.multiLocationLB = new core.views.MultipleLocationLightbox({ + locations: ev.collection, + position: "absolute", + searchTerm: ev.searchTerm + }); + me.multiLocationLB.on('location:selected', function(item) { + me.multiLocationLB.hide(); + core.utils.redirectLocation(item); + }); + me.multiLocationLB.show(); + } else if (ev.collection.length == 1) { + core.utils.redirectLocation(ev.collection.first()); + } + } + }, + + /** + * Show errors + */ + showError : function() { + var messageLightbox = new core.views.Lightbox({ + title : jsGetText("search_not_found_title"), + content : jsGetText("search_not_found_message"), + template : "Lightbox/small.html" + }); + var me = this; + messageLightbox.on("hide", function(){ + me.locationSearchWidget.__disabled = false; + }); + this.locationSearchWidget.__disabled = true; + messageLightbox.show(); + } + +}); diff --git a/utilitybelt/apps/Frontend_AU/pages/Menu.js b/utilitybelt/apps/Frontend_AU/pages/Menu.js new file mode 100644 index 0000000..7eb90b7 --- /dev/null +++ b/utilitybelt/apps/Frontend_AU/pages/Menu.js @@ -0,0 +1,262 @@ +/** + * Restaurant menu page. + * + * @handle authorize:grant Resaves the order for the logged in user and + * reloads the page with the new order id + */ +core.define('app.Menu', { + extend: 'app.Page', + events: { + "click .menu-cart div.allround.cursor": 'showFlavorsOrAddToCart', + 'click .rating': 'showReviewsTab' + }, + plugins: { + '.tabs': 'tabs', + '.vertical-tabs': [ 'vtabs', { + selectClass: 'select', + panelSelector: '.vertical-tabs-panels > div' + } ] + }, + + initialize: function() { + app.Menu.__super__.initialize.call(this); + var menu = this, + restaurant = this.options.restaurant, + cartCollection = new core.collections.Cart(); + // init the order + + var active_address = this.options.active_address; + if (active_address) { + var activeAddressModel = new core.models.Address(active_address); //add necessary fields to save active address through the API + } + + var deliveryAddress = new core.models.DeliveryAddress({ + user_id: this.options.user_id, + address: activeAddressModel + }); + + var order = new core.models.Order({ + general: new core.models.OrderGeneral({restaurant_id: restaurant.id, user_id: this.options.user_id}), + delivery_address: deliveryAddress + }); + this.order = order; + + this.order.setRestaurant(new core.models.Restaurant({ + id: restaurant.id, + delivery_fees: restaurant.delivery_fees, + min_order_value: restaurant.min_order_value + })); + + var allItems = []; + _.each(lh_data.restaurant.menu.sections, function(section){ + _.each(section.items, function(item){ + allItems.push(item); + }); + }); + this.allItems = allItems; + + var cartView; + cartCollection.order = menu.order; + + this.cart = { + collection: cartCollection, + view: cartView + }; + // build a set of available items + this.items = {}; + _.each(restaurant.menu.sections, function(section) { + _.each(section.items, function(item) { + item.sectionId = section.id; + menu.items[item.id] = item; + }); + }); + + this.options.user.on('authorize:grant', function(user, token) { + var noReload = menu.order.ownedBy(user); + menu.order.changeOwner(user, { + success: function(order) { + order.idle = false; + if (noReload) + cartCollection.save(); + else + window.location.reload(); + } + }); + }); + this.options.user.on('authorize:anonym', function(user) { + menu.order.changeOwner(user, { + success: function(order) { + order.idle = false; + window.location.reload(); + } + }); + }); + + this.flb = new core.views.FlavorsLightbox(); + + // link the order to the cart when loaded + cartCollection.fetch({ + success: function(order, response) { + order.resetCoupon(); //If coming back from the checkout page after entering a coupon, the order model contains the coupon information. At this stage the coupon must be null. + + cartView = new core.views.CartBox({ + title: jsGetText('shopping_cart'), + el: '.shopping-cart', + paymentIcons: [ + { css: 'cashPaymentIcons', tooltip: 'Barzahlung' }, + { css: 'onlinePaymentIcons', tooltip: 'Onlinezahlung' } + ], + cart: { + checkout: function() { + window.location.href = core.utils.formatURL('/checkout/' + menu.order.id + '/'); + }, + preorder: !restaurant.general.open, + collection: cartCollection + }, + fixable: { + marginTop: '10px', + hideEl: '.TestimonialRestaurant' + } + }); + + cartCollection.on('change reset', function() { + order.resetCoupon(); //never display coupon discounts in the cart on the menu page, no matter what the status of the order is on the server. + }); + } + }); + + this.searchZip = new core.views.ZipCodeLightbox(); + this.searchZip.on("locationFound", function(event){ + menu.handleSearchFoundZipBox(event); + }); + this.searchZip.on("locationNotFound", function(searchValue){ + core.utils.trackingLogger.logError('search_error_nomatch_menu', searchValue); + menu.showError(); + }); + this.searchZip.on("location:fetchError", function(){ + menu.showError(); + }); + + this.chooseBox = new core.views.ChooseLightbox(); + this.chooseBox.on("show:search", function(){ + $.cookie("active_address", "", {expires: -1}); + menu.chooseBox.hide(); + menu.searchZip.show(lh_data.restaurant.general.name); + }); + + this.checkShowZipSearch(); + }, + + /** + * Called when a user selects an item from the menu. If the item has flavors options, the flavors lightbox is displayed. + * Otherwise, the item is added to the cart. + * @param {Object} evt The event object + */ + showFlavorsOrAddToCart: function(evt) { + var $target = $(evt.currentTarget); + var itemId = $target.find('a[data-item-id]').attr('data-item-id'); + + var selectedItemObj = _.find(this.allItems, function(el){ + return el.id == itemId; + }); + + if(!selectedItemObj) { + throw 'inexisting-item'; + } + + var selectedItem = new core.models.Item(selectedItemObj); + this.flb.hide(); + this.flb = new core.views.FlavorsLightbox(); + + if(selectedItem.getSubItems()) { + this.flb.show(selectedItem); + var me = this; + this.flb.on("flavorsChosen", function(flavorsObj) { + me.cart.collection.add(flavorsObj.selectedItemModel); + me.checkShowZipSearch.call(me); + }); + } else { + this.cart.collection.add(selectedItem); + this.checkShowZipSearch(); + } + }, + + checkShowZipSearch: function(){ + if ($.cookie("active_address") == null) { + this.searchZip.show(lh_data.restaurant.general.name); + } + }, + + handleSearchFoundZipBox: function(event){ + var me = this; + this.searchZip.hide(); + if (event.collection) { + if (event.collection.length > 1) { + if (this.multiLocationLB && this.multiLocationLB.isShown()) { + return; + } + this.multiLocationLB = new core.views.MultipleLocationLightbox({ + locations : event.collection, + position : "absolute", + searchTerm: event.searchTerm + }); + this.multiLocationLB.on('location:selected', function(location) { + me.multiLocationLB.hide(); + me.doesRestaurantFit.call(me, location, lh_data.restaurant_id); + }); + this.multiLocationLB.show(); + } else if (event.collection.length == 1) { + me.doesRestaurantFit.call(me, event.collection.first(), lh_data.restaurant_id); + } + } + }, + + doesRestaurantFit: function(location, restaurantId){ + this.restaurantModel = new core.models.Restaurant(); + this.activeAddress = location; + var addressModel = new core.models.Address(location.get('address')); + this.restaurantModel.set("address", addressModel); //TODO this is just for simulating the error code + this.restaurantModel.set("id", restaurantId); + var me = this; + this.restaurantModel.fetch({ + success: function(){ + me.gotResponse.apply(me, arguments); + } + }); + }, + + gotResponse: function(model, response){ + var fees = model.get("delivery_fees"); + if (fees.length == 0) { + core.utils.trackingLogger.logError("search_error_no_delivery_to", [this.restaurantModel.id, this.activeAddress.get('address').suburb, this.activeAddress.get('address').zipcode].join(',')); + this.chooseBox.setRestaurantId(this.restaurantModel.get("id")); + this.chooseBox.setAddress(this.activeAddress); + this.chooseBox.show(model.get("general").name); + return; + } + //reload the current page with setting the cookie for the current address + core.utils.redirectRestaurant(model.get("general").slug, this.activeAddress); + }, + + /** + * shows the error in a lightbox when we have an issue finding a location + */ + showError : function() { + var me = this; + var messageLightbox = new core.views.Lightbox({ + title : jsGetText("search_not_found_title"), + content : jsGetText("search_not_found_message"), + template : "Lightbox/small.html" + }); + messageLightbox.on("hide", function(){ + me.searchZip.__disabled = false; + }); + me.searchZip.__disabled = true; + messageLightbox.show(); + }, + + showReviewsTab: function() { + this.$('.tabs').tabs('option', 'selected', 'reviews_tab'); + } + +}); diff --git a/utilitybelt/apps/Frontend_AU/pages/Page.js b/utilitybelt/apps/Frontend_AU/pages/Page.js new file mode 100644 index 0000000..4d0887d --- /dev/null +++ b/utilitybelt/apps/Frontend_AU/pages/Page.js @@ -0,0 +1,14 @@ + +/** Basic application page class, contains features which should be on every page */ + +core.define('app.Page', { + + extend: 'core.Page', + + initialize: function() { + + app.Page.__super__.initialize.call(this); + + } + +}); diff --git a/utilitybelt/apps/Frontend_AU/pages/RestaurantList.js b/utilitybelt/apps/Frontend_AU/pages/RestaurantList.js new file mode 100644 index 0000000..756fdde --- /dev/null +++ b/utilitybelt/apps/Frontend_AU/pages/RestaurantList.js @@ -0,0 +1,87 @@ +/** + * Restaurant list page. + * + * @handle authorize:grant Reloads the page + */ +core.define('app.RestaurantList', { + extend : 'app.Page', + + events : { + }, + + plugins : { + }, + + options : { + search_closeable: true + }, + + /** + * Initializes Active Address widget. + * + * @handle authorize:grant Reloads the page on successful login + */ + initialize : function() { + var me = this; + this.activeAddress = new core.views.ActiveAddress({ + el: 'section.activeAddress', + marginTarget: 'section.filter, .landing_page', + closeable: this.options.search_closeable, + activeAddress: core.runtime.fetch('active_address', true) + }); + + this.filter = new core.views.FilterForm({ + categories: new core.collections.Category([ + {name: 'thai'}, + {name: 'pizza'}, + {name: 'indian'}, + {name: 'chinese'}, + {name: 'italian'}, + {name: 'asian'}, + {name: 'seafood'}, + {name: 'burgers-and-grill'}, + {name: 'dietary-options'}, + {name: 'lebanese-and-turkish'}, + {name: 'japanese-and-sushi'}, + {name: 'other'} + ]), + implicit_categories: this.options.implicit_categories, + options: new core.collections.Option([ + {name: 'online_payment'}, + {name: 'box'} + ]), + el: 'section.filter', + fixable: { + marginTop: '0px', //let the filter back stick to the top when scrolling + hooks: { + scrollingStart: function() { + var $zipBox = $('.search_placeholder .zipbox'); + var zipBoxHeight = $zipBox.outerHeight() + 20; + + this.$el.css('z-index', 1); + this.initialMarginTop = ''; + + if ($zipBox.is(':hidden')) { + this.offsetTop = this.initialOffsetTop; + } else { + this.offsetTop = this.initialOffsetTop + zipBoxHeight; + } + this.$('.filter-form-wrapper').removeClass("margin-top"); //no top-margin when sticking to the top + }, + scrollPositionDocked: function() { + this.$el.css('z-index', 0); + this.$('.filter-form-wrapper').addClass("margin-top"); + } + } + } + }); + + this.activeAddress.locationSearchWidget.$el.addClass('search-results'); + + this.options.user.on('authorize:grant', function(user, token) { + window.location.reload(); + }); + + app.RestaurantList.__super__.initialize.call(this); + } +}); diff --git a/utilitybelt/apps/Frontend_AU/pages/RestaurantListOpen.js b/utilitybelt/apps/Frontend_AU/pages/RestaurantListOpen.js new file mode 100644 index 0000000..164cdbd --- /dev/null +++ b/utilitybelt/apps/Frontend_AU/pages/RestaurantListOpen.js @@ -0,0 +1,28 @@ +/** + * Restaurant list page that has a search widget open. + * + * @handle authorize:grant Reloads the page + */ +core.define('app.RestaurantListOpen', { + extend : 'app.RestaurantList', + + events : { + }, + + plugins : { + }, + + options : { + search_closeable: false + }, + + /** + * Initializes Active Address widget. + * + * @handle authorize:grant Reloads the page on successful login + */ + initialize : function() { + app.RestaurantListOpen.__super__.initialize.call(this); + this.activeAddress.showSearchWidget(); + } +}); diff --git a/utilitybelt/apps/Frontend_AU/utils/1 b/utilitybelt/apps/Frontend_AU/utils/1 new file mode 100644 index 0000000..e69de29 diff --git a/utilitybelt/apps/UB_docs/config.js b/utilitybelt/apps/UB_docs/config.js new file mode 100644 index 0000000..20555d9 --- /dev/null +++ b/utilitybelt/apps/UB_docs/config.js @@ -0,0 +1,63 @@ + +/* + * Sample config file + * We use RequireJS as a module loader http://requirejs.org/docs/api.html + */ +var requireBaseUrl = "/utilitybelt"; +require.config({ + baseUrl: requireBaseUrl + '/utilitybelt' +}); + +require([ + +/* Include Web UI library */ + 'core/config' +], function(core) { + + require([ + +/* Inlcude application files */ + + 'order!apps/UB_docs/namespaces' + , 'order!apps/UB_docs/config/messages' + , 'order!apps/UB_docs/pages/Page' + , 'order!core/tests/fixtures/Locations' + + ], function() { + + core.Proxy('local'); + core.messages.de = _.extend(core.messages.de, docs.messages.de); + +/* Call router, which will create the instance of the relevant page */ + + var AppRouter = Backbone.Router.extend({ + routes: { + "*actions": "defaultRoute", + ":actions/:tab": "defaultRoute" + }, + defaultRoute: function( url, tab ){ + new docs.Page({ url: url || 'core.views.Lightbox', tab: tab || 'documentation' }); +/* + if (!url) + url = 'core.views.Lightbox'; + var class_name = 'docs.' + url.replace(/\./gi, '_'); + try { + eval('new ' + class_name + '({ url: url })'); + } + catch (ex) { + console.error('Cannot instantiate ' + class_name); + } +*/ + } + }); + + core.app_router = new AppRouter(); + + Backbone.history.start(); + +// core.Router('docs.Home'); + + }) +}); + +var APP_LANGUAGE = 'de'; diff --git a/utilitybelt/apps/UB_docs/config/messages.js b/utilitybelt/apps/UB_docs/config/messages.js new file mode 100644 index 0000000..bb68689 --- /dev/null +++ b/utilitybelt/apps/UB_docs/config/messages.js @@ -0,0 +1,5 @@ + +/** Doc application messages */ + +docs.messages.de = { +}; diff --git a/utilitybelt/apps/UB_docs/data/Classes.json b/utilitybelt/apps/UB_docs/data/Classes.json new file mode 100644 index 0000000..f1b7f86 --- /dev/null +++ b/utilitybelt/apps/UB_docs/data/Classes.json @@ -0,0 +1,30 @@ +[ + { "category": "Box", "items": [ + { "className": "core.views.Box", "title": "Basic box" }, + { "className": "core.views.CartBox", "title": "Cart box" }, + { "className": "core.views.ReadOnlyCartBox", "title": "Cart box (read only)" } + ] }, + + { "category": "Lightboxes", "items": [ + + { "className": "core.views.Lightbox", "title": "Basic lightbox" }, + { "className": "core.views.UserAddressListLightbox", "title": "User addresses" }, + { "className": "core.views.UserAddressLightbox", "title": "New address form in the lightbox" }, + { "className": "core.views.LoginLightbox", "title": "Login form in the lightbox" }, + { "className": "core.views.MultipleAddressLightbox", "title": "Multiple Addresses" }, + { "className": "core.views.FlavorsLightbox", "title": "Flavors Lightbox" } + ] }, + + { "category": "Forms", "items": [ + { "className": "core.views.Form", "title": "Basic form" }, + { "className": "core.views.UserAddressForm", "title": "New address form" }, + { "className": "core.views.LoginForm", "title": "Login form" } + ] }, + + { "category": "Views", "items": [ + { "className": "core.View", "title": "Basic view" }, + { "className": "core.views.UserAddressList", "title": "List of addresses" }, + { "className": "core.views.Cart", "title": "Cart" }, + { "className": "core.views.AddressSearch", "title": "AddressSearch" } + ] } +] diff --git a/utilitybelt/apps/UB_docs/data/core.View.json b/utilitybelt/apps/UB_docs/data/core.View.json new file mode 100644 index 0000000..996646f --- /dev/null +++ b/utilitybelt/apps/UB_docs/data/core.View.json @@ -0,0 +1,84 @@ +[ + { + "tags": [ + { + "type": "param", + "types": [ + "String" + ], + "name": "template", + "description": "Template code" + } + ], + "description": { + "full": "

Basic core View, all views (widgets) should extend it

", + "summary": "

Basic core View, all views (widgets) should extend it

", + "body": "" + }, + "isPrivate": false, + "ignore": false, + "code": "core.define('core.View', {\r\n\r\n extend: 'Backbone.View'," + }, + { + "tags": [], + "description": { + "full": "

Runs on widget's render, triggers 'render' event

", + "summary": "

Runs on widget's render, triggers 'render' event

", + "body": "" + }, + "ignore": false, + "code": "render: function(arg1, arg2) {\r\n\r\n var me = this;\r\n setTimeout( function() {\r\n me.trigger('render');\r\n }, 1);\r\n\r\n }," + }, + { + "tags": [ + { + "type": "param", + "types": [ + "jQuery" + ], + "name": "ct", + "description": "Jquery container to render to" + } + ], + "description": { + "full": "

Renders widget to specified cotainer

", + "summary": "

Renders widget to specified cotainer

", + "body": "" + }, + "isPrivate": false, + "ignore": false, + "code": "renderTo: function(ct) {\r\n var me = this;\r\n this.on('render', function() {\r\n ct.empty().append(me.$el);\r\n });\r\n }," + }, + { + "tags": [ + { + "type": "param", + "types": [ + "Class" + ], + "name": "type", + "description": "Class name which should be added\r" + }, + { + "type": "param", + "types": [ + "Object" + ], + "name": "config", + "description": "Class config options\r" + }, + { + "type": "returns", + "string": "added element" + } + ], + "description": { + "full": "

Adds an instance of the \"type\" widget into the body (container returned by the getBody() method)

", + "summary": "

Adds an instance of the \"type\" widget into the body (container returned by the getBody() method)

", + "body": "" + }, + "isPrivate": false, + "ignore": false, + "code": "addItem: function(type, config) {\r\n if (this.getBody) {\r\n var $body = this.getBody();\r\n var new_el = new type(config);\r\n $body.append(new_el.$el); \r\n return new_el;\r\n }\r\n },\r\n\r\n demo: function() {\r\n return $('

See core.views.UserAddressList

')\r\n }\r\n\r\n});" + } +] \ No newline at end of file diff --git a/utilitybelt/apps/UB_docs/data/core.views.Box.json b/utilitybelt/apps/UB_docs/data/core.views.Box.json new file mode 100644 index 0000000..b744c6f --- /dev/null +++ b/utilitybelt/apps/UB_docs/data/core.views.Box.json @@ -0,0 +1,150 @@ +[ + { + "tags": [ + { + "type": "title", + "string": "Basic Box" + }, + { + "type": "param", + "types": [ + "String" + ], + "name": "title", + "description": "Title" + }, + { + "type": "param", + "types": [ + "String" + ], + "name": "content", + "description": "Content" + }, + { + "type": "param", + "types": [ + "String" + ], + "name": "tempate", + "description": "Path to template" + } + ], + "description": { + "full": "

Basic Box with the configurable header, footer, and content

\n\n

Example

\n\n
var bb = new core.views.Box({ title: 'Some awesome title', content: 'Awesomest content', el: $('#box-container') });\nbb.show();\n
", + "summary": "

Basic Box with the configurable header, footer, and content

", + "body": "

Example

\n\n
var bb = new core.views.Box({ title: 'Some awesome title', content: 'Awesomest content', el: $('#box-container') });\nbb.show();\n
" + }, + "isPrivate": false, + "ignore": false, + "code": "core.define('core.views.Box', {\n\n extend: 'core.View',\n\n className: \"Box\",\n\n template: 'Box/base.html'," + }, + { + "tags": [ + { + "type": "constructor", + "string": "" + } + ], + "description": { + "full": "

Initialize the widget

", + "summary": "

Initialize the widget

", + "body": "" + }, + "isPrivate": false, + "ignore": false, + "code": "initialize: function() {\n\n var template = this.options.template || this.template;\n\n var me = this;\n core.utils.getTemplate(template, function(tpl) {\n me.template = tpl;\n me.render(tpl);\n });\n },\n\n render: function(tpl) {\n tpl = tpl || this.template;\n this.el = _.template(tpl, { some: 'here' });\n this.appendElement(this.el);\n\n var title = this.title || this.options.title || this.$el.attr('header');\n if (title" + }, + { + "tags": [], + "description": { + "full": "

& $('.title-container', this.$el).length < 1

", + "summary": "

& $('.title-container', this.$el).length < 1

", + "body": "" + }, + "ignore": false, + "code": "{\n this.setTitle(title);\n }\n\n var content = this.options.content;\n if (content) {\n this.setContent(content);\n }\n\n core.views.Box.__super__.render.call(this);\n }," + }, + { + "tags": [], + "description": { + "full": "

Method for appending the final mark-up to DOM. By default, adds it to the this.el
Should be overriden for Lightbox-alike widgets (which render themself to the ).

", + "summary": "

Method for appending the final mark-up to DOM. By default, adds it to the this.el
Should be overriden for Lightbox-alike widgets (which render themself to the ).

", + "body": "" + }, + "ignore": false, + "code": "appendElement: function(el) {\n this.$el.empty().append(el);\n }," + }, + { + "tags": [ + { + "type": "param", + "types": [ + "String" + ], + "name": "title", + "description": "Lightbox Title" + } + ], + "description": { + "full": "

Set title for the Lightbox

", + "summary": "

Set title for the Lightbox

", + "body": "" + }, + "isPrivate": false, + "ignore": false, + "code": "setTitle: function(title) {\n this.$('.title-container').html(title);\n }," + }, + { + "tags": [ + { + "type": "param", + "types": [ + "String" + ], + "name": "content", + "description": "Lightbox Content" + } + ], + "description": { + "full": "

Set content for the Lightbox

", + "summary": "

Set content for the Lightbox

", + "body": "" + }, + "isPrivate": false, + "ignore": false, + "code": "setContent: function(content) {\n this.getBody().html(content);\n }," + }, + { + "tags": [ + { + "type": "returns", + "string": "jQuery" + } + ], + "description": { + "full": "

Returns body element

", + "summary": "

Returns body element

", + "body": "" + }, + "isPrivate": false, + "ignore": false, + "code": "getBody: function() {\n return this.$('.body-box');\n }," + }, + { + "tags": [ + { + "type": "returns", + "string": "jQuery" + } + ], + "description": { + "full": "

Adds link to title container and return its element

", + "summary": "

Adds link to title container and return its element

", + "body": "" + }, + "isPrivate": false, + "ignore": false, + "code": "addTitleLink: function(html) {\n var $title_link = this.$('.top-box .title-link');\n return $title_link.append(html);\n },\n\n demo: function() {\n var bb = new core.views.Box({ title: 'Box title' });\n return bb;\n }\n});" + } +] \ No newline at end of file diff --git a/utilitybelt/apps/UB_docs/data/core.views.Cart.json b/utilitybelt/apps/UB_docs/data/core.views.Cart.json new file mode 100644 index 0000000..cd1c19c --- /dev/null +++ b/utilitybelt/apps/UB_docs/data/core.views.Cart.json @@ -0,0 +1,137 @@ +[ + { + "tags": [ + { + "type": "param", + "types": [ + "Cart" + ], + "name": "cart", + "description": "The linked Cart item" + } + ], + "description": { + "full": "

View for displaying a cart

\n\n

Example

\n\n
var cart = new core.views.Cart();\ncart.renderTo($('#some_element'));\n
", + "summary": "

View for displaying a cart

", + "body": "

Example

\n\n
var cart = new core.views.Cart();\ncart.renderTo($('#some_element'));\n
" + }, + "isPrivate": false, + "ignore": false, + "code": "core.define('core.views.Cart', {\n extend: 'core.View',\n className: 'Cart',\n template: [],\n events: {'change select': 'sumUpdate'},\n plugins: {\n 'select': ['spinner', {cycle: false}]\n }," + }, + { + "tags": [], + "description": { + "full": "

Constructor, sets collection and templates

", + "summary": "

Constructor, sets collection and templates

", + "body": "" + }, + "ignore": false, + "code": "initialize: function() {\n this.collection = this.options.collection || new core.collections.Cart();\n this.collection.bindTo(this);\n var me = this;\n core.utils.getTemplate('Cart/Cart.html', function(tplMain) {\n me.template = [_.template(tplMain)];\n core.utils.getTemplate('Cart/CartItem.html', function(tplSub) {\n me.template.push(_.template(tplSub));\n core.utils.getTemplate('Cart/CartSum.html', function(tplSum) {\n me.template.push(_.template(tplSum));\n // TODO use a proper load to trigger change (and rendering)\n me.collection.trigger('change');\n });\n });\n });\n this.collection.on('quantity:increase quantity:decrease item:add', function(item, delta) {\n me.renderItem(item.toJSON());\n me._renderSum();\n });\n this.collection.on('item:remove', function(item, delta) {\n if (this.length) {\n me.unRenderItem(item.get('id'));\n me._renderSum();\n }\n });\n this.collection.on('empty', function() { me.onCartEmpty(); });\n }," + }, + { + "tags": [], + "description": { + "full": "

Renders widget using the collection and the templates

", + "summary": "

Renders widget using the collection and the templates

", + "body": "" + }, + "ignore": false, + "code": "render: function() {\n var me = this,\n collection = me.collection,\n template = me.template;\n // cancel view autorefresh on collection change event\n // TODO make this part optional in the parent class\n collection.off('reset change');\n var records = collection.toJSON();\n me.$el.empty().append(template[0]());\n if (collection.length) {\n this._renderSum();\n _.each(records, function(item) {\n me.renderItem(item);\n });\n } else {\n me.onCartEmpty();\n }\n return core.views.Cart.__super__.render.call(this);\n }," + }, + { + "tags": [ + { + "type": "param", + "types": [ + "Object" + ], + "name": "item", + "description": "A hash (produced by CartItem.toJSON())" + }, + { + "type": "return", + "types": [ + "core.views.Cart" + ], + "description": "this" + } + ], + "description": { + "full": "

Refreshes or adds a new item to the cart.

", + "summary": "

Refreshes or adds a new item to the cart.

", + "body": "" + }, + "isPrivate": false, + "ignore": false, + "code": "renderItem: function(item) {\n var markup = this.template[1]({\n cartItem: item,\n currency: this.collection.currencySymbol\n });\n var current = this.$('.' + item.id);\n if (current.length) {\n current.find('select').nextAll().replaceWith(\n $(markup).find('select').nextAll()\n );\n } else {\n this.$('ul li.sum').before(markup);\n }\n return this;\n }," + }, + { + "tags": [ + { + "type": "return", + "types": [ + "core.views.Cart" + ], + "description": "this" + } + ], + "description": { + "full": "

Refreshes or adds a sum row to the cart.

", + "summary": "

Refreshes or adds a sum row to the cart.

", + "body": "" + }, + "isPrivate": false, + "ignore": false, + "code": "_renderSum: function() {\n var markup = this.template[2]({\n sum: this.collection.sum,\n currency: this.collection.currencySymbol\n });\n var current = this.$('ul li.sum');\n if (current.length)\n current.replaceWith(markup);\n else\n this.$('ul li:last').before(markup);\n return this;\n },\n\n unRenderItem: function(id) {\n this.$('ul li.' + id).remove();\n if (!this.collection.length) {\n this.onCartEmpty();\n }\n }," + }, + { + "tags": [], + "description": { + "full": "

Runs when cart is empty.

", + "summary": "

Runs when cart is empty.

", + "body": "" + }, + "ignore": false, + "code": "onCartEmpty: function() {\n this.$('ul li').not(':last').remove();\n var cartItems = this.$('ul');\n $('
  • ').addClass('span-12').text(jsGetText(\"cart_empty\")).prependTo(cartItems);\n }," + }, + { + "tags": [ + { + "type": "param", + "types": [ + "Object" + ], + "name": "e", + "description": "A jQuery event with a current target having a \"name\" attribute" + } + ], + "description": { + "full": "

    Cart item change handler.
    Its only business is to identify the modified element
    and ask the collection to update occordingly.
    Everything else is done by the handlers bound to the collection.

    ", + "summary": "

    Cart item change handler.
    Its only business is to identify the modified element
    and ask the collection to update occordingly.
    Everything else is done by the handlers bound to the collection.

    ", + "body": "" + }, + "isPrivate": false, + "ignore": false, + "code": "sumUpdate: function(e) {\n var select = $(e.currentTarget),\n q = select.val(),\n itemId = select.attr('name').split('|').pop();\n this.collection.update(itemId, q);\n }," + }, + { + "tags": [ + { + "type": "return", + "types": [ + "core.views.Cart" + ], + "description": "The view used to render a cart" + } + ], + "description": { + "full": "

    This demo code uses some fixtures to render a cart containing some
    items.

    ", + "summary": "

    This demo code uses some fixtures to render a cart containing some
    items.

    ", + "body": "" + }, + "isPrivate": false, + "ignore": false, + "code": "demo: function() {\n var items = [\n {\n \"description\": \"inkl. 0,15\\u20ac Pfand\",\n \"sizes\": [{\"price\": 5, \"name\": \"L\"}],\n \"pic\": \"\",\n \"main_item\": true,\n \"sub_item\": false,\n \"id\": \"mp1\",\n \"name\": \"Fanta*1,3,5,7 0,5L \"\n }, {\n \"description\": \"inkl. 0,15\\u20ac Pfand\",\n \"sizes\": [{\"price\": 20, \"name\": \"XL\"}],\n \"pic\": \"\",\n \"main_item\": true,\n \"sub_item\": false,\n \"id\": \"mp2\",\n \"name\": \"Pain saucisse\"\n }\n ];\n var cart = new core.collections.Cart();\n for (var i=0, bound=items.length; iShopping Cart container, for inline display.

    ", + "summary": "

    Shopping Cart container, for inline display.

    ", + "body": "" + }, + "ignore": false, + "code": "core.define('core.views.CartBox', {\n extend: 'core.views.Box',\n className: \"CartBox\",\n\n initialize: function() {\n core.views.CartBox.__super__.initialize.call(this);\n var me = this;\n this.on('render', function() {\n me.addClearAllButton();\n me.addPaymentIcons(this.options.paymentIcons);\n me.addItem(core.views.Cart, this.options.cart || {});\n });\n }," + }, + { + "tags": [ + { + "type": "returns", + "string": "jQuery" + } + ], + "description": { + "full": "

    Adds 'clear cart' button

    ", + "summary": "

    Adds 'clear cart' button

    ", + "body": "" + }, + "isPrivate": false, + "ignore": false, + "code": "addClearAllButton: function(el) {\n var html = '
    ';\n var $btn = $(html);\n var $title = $('.top-box', this.$el);\n $btn.on('click', _.bind(this.onClearBtnClick, this));\n return $btn.appendTo($title);\n }," + }, + { + "tags": [], + "description": { + "full": "

    Adds payment icons

    ", + "summary": "

    Adds payment icons

    ", + "body": "" + }, + "ignore": false, + "code": "addPaymentIcons: function(icons) {\n var $footer = this.$('section > div:last');\n _.each(icons, function(icon) {\n $('
    ').addClass(icon.css).attr('tooltip', icon.tooltip).appendTo($footer);\n });\n }," + }, + { + "tags": [], + "description": { + "full": "

    Handler for the 'clear cart' button

    ", + "summary": "

    Handler for the 'clear cart' button

    ", + "body": "" + }, + "ignore": false, + "code": "onClearBtnClick: function() {\n this.getItem().collection.empty();\n },\n\n demo: function() {\n var items = [\n {\n \"description\": \"inkl. 0,15\\u20ac Pfand\",\n \"sizes\": [{\"price\": 5, \"name\": \"L\"}],\n \"pic\": \"\",\n \"main_item\": true,\n \"sub_item\": false,\n \"id\": \"mp1\",\n \"name\": \"Fanta*1,3,5,7 0,5L \"\n }, {\n \"description\": \"inkl. 0,15\\u20ac Pfand\",\n \"sizes\": [{\"price\": 20, \"name\": \"XL\"}],\n \"pic\": \"\",\n \"main_item\": true,\n \"sub_item\": false,\n \"id\": \"mp2\",\n \"name\": \"Pain saucisse\"\n }\n ];\n var cart = new core.collections.Cart();\n for (var i=0, bound=items.length; iLightbox which displays a list of flavors for a menu item

    \n\n

    Examples

    \n\n
    var lb = new core.views.FlavorsLightbox();\nlb.show(item);\n
    ", + "summary": "

    Lightbox which displays a list of flavors for a menu item

    ", + "body": "

    Examples

    \n\n
    var lb = new core.views.FlavorsLightbox();\nlb.show(item);\n
    " + }, + "isPrivate": false, + "ignore": false, + "code": "core.define('core.views.FlavorsLightbox', {\n\n extend: 'core.views.Lightbox',\n\n templateFlavor: \"Flavors/flavors.html\",\n\n events: {\n \"click .top-box .close-icon\": \"hide\",\n \"click .button\": \"chooseFlavors\",\n \"click input[data-flavor-id]\": 'recalculatePrice'\n },\n\n className: 'FlavorsLightbox',\n\n plugins: {\n 'section': 'jqTransform'\n },\n\n validate: function() {\n //TODO check first of all the Model Validation and also the data validation inside the model ???\n //and display the error on the DOM\n var forms = this.$el.find(\"form\");\n for(var i = 0, len = forms.length; i < len; i++) {\n console.log(forms[i]);\n }\n }," + }, + { + "tags": [ + { + "type": "constructor", + "string": "" + }, + { + "type": "param", + "types": [ + "Object" + ], + "name": "options", + "description": "Options" + } + ], + "description": { + "full": "

    Constructor

    ", + "summary": "

    Constructor

    ", + "body": "" + }, + "isPrivate": false, + "ignore": false, + "code": "initialize: function(options) {\n var me = this;\n core.utils.getTemplate(me.templateFlavor, function(tpl) {\n me.templateFlavor = tpl;\n });\n core.views.FlavorsLightbox.__super__.initialize.call(this, options);\n },\n addItem: function(type, config, place) {\n }," + }, + { + "tags": [ + { + "type": "param", + "types": [ + "Object" + ], + "name": "item", + "description": "The data structure (with flavors information) for the menu item" + } + ], + "description": { + "full": "

    Display the flavors lightbox for a menu item

    ", + "summary": "

    Display the flavors lightbox for a menu item

    ", + "body": "" + }, + "isPrivate": false, + "ignore": false, + "code": "show: function(item) {\n this.item = item;\n //TODO: has to be fixed when using backbone again\n //this.basePrice = this.item.get('sizes').at(0).get('price');\n this.basePrice = item.sizes[0].price;\n\n //add all flavors to a view-wide colleciton so they can easily be retrieved by ID\n this.flvUtils = new core.utils.flavorUtils(item);\n this.allFlavors = this.flvUtils.getFlavorsForItem(item.id);\n\n this.itemJSON = item;\n this.title = this.itemJSON.name;\n this.options.content = _.template(this.templateFlavor)(this.itemJSON);\n core.views.FlavorsLightbox.__super__.show.call(this);\n\n this.setDisplayedPrice(core.utils.formatPrice(this.basePrice));\n }," + }, + { + "tags": [], + "description": { + "full": "

    Validates the flavors chosen for the item. If the validation is successful, the item is added to the cart.
    Otherwise, the invalid sections are highlighted.

    ", + "summary": "

    Validates the flavors chosen for the item. If the validation is successful, the item is added to the cart.
    Otherwise, the invalid sections are highlighted.

    ", + "body": "" + }, + "ignore": false, + "code": "chooseFlavors: function() {\n var clonedItem = {};\n jQuery.extend(true, clonedItem, this.itemJSON);\n var itemModel = this.createCartItem(this.getSelectedFlavors(), clonedItem);\n\n //VALIDATION\n var error = itemModel.validate();\n\n this.trigger(\"flavorsChosen\", {\n selectedItemModel: itemModel\n });\n this.hide();\n }," + }, + { + "tags": [ + { + "type": "param", + "types": [ + "Array" + ], + "name": "flavorsArray", + "description": "An array of ID strings of flavors to be included in the returned Item model" + }, + { + "type": "param", + "types": [ + "Object" + ], + "name": "selectedItem", + "description": "The object data structure for the menu item" + } + ], + "description": { + "full": "

    Creates a core.models.Item model for an item object. The resulting model contains only the flavors specified in flavorsArray.

    ", + "summary": "

    Creates a core.models.Item model for an item object. The resulting model contains only the flavors specified in flavorsArray.

    ", + "body": "" + }, + "isPrivate": false, + "ignore": false, + "code": "createCartItem: function(flavorsArray, selectedItem) {\n //TODO support arbitrary depth flavor nesting !!\n for(var i = 0, subItems = selectedItem.flavors.items; i < subItems.length; i++) {\n var subItem = subItems[i];\n for(var j = 0, subSubItems = subItem.flavors.items; j < subSubItems.length; j++) {\n var subSubItem = subSubItems[j];\n var subSubId = subSubItem.id;\n if(!_.include(flavorsArray, subSubId)) {\n subItem.flavors.items = _.difference(subItem.flavors.items, [subSubItem])\n }\n }\n }\n //all unselected flavors have been removed from the selectedItem.\n var selectedItemModel = new core.models.Item(selectedItem);\n return selectedItemModel;\n }," + }, + { + "tags": [ + { + "type": "param", + "types": [ + "Object" + ], + "name": "evt", + "description": "The event object" + } + ], + "description": { + "full": "

    Schedule this.calculateTotalPrice() for asynchronous execution, allowing the UI to update state

    ", + "summary": "

    Schedule this.calculateTotalPrice() for asynchronous execution, allowing the UI to update state

    ", + "body": "" + }, + "isPrivate": false, + "ignore": false, + "code": "recalculatePrice: function(evt) {\n var me = this;\n setTimeout(function() {\n me.calculateTotalPrice();\n }, 1);\n\n }," + }, + { + "tags": [ + { + "type": "return", + "types": [ + "Array" + ], + "description": "An array of stirng, one for each ID" + } + ], + "description": { + "full": "

    Returns the ID's of flavors that have been selected

    ", + "summary": "

    Returns the ID's of flavors that have been selected

    ", + "body": "" + }, + "isPrivate": false, + "ignore": false, + "code": "getSelectedFlavors: function() {\n var checkedInputs = this.$el.find(\"form input:checked\");\n var flavorIds = _.map(checkedInputs, function(input) {\n return $(input).data('flavor-id') + '';\n });\n return flavorIds;\n }," + }, + { + "tags": [], + "description": { + "full": "

    Updates the total price displayed in the view.

    ", + "summary": "

    Updates the total price displayed in the view.

    ", + "body": "" + }, + "ignore": false, + "code": "calculateTotalPrice: function() {\n var selectedFlavors = this.$el.find(\"form input:checked\");\n var me = this;\n var newPrice = this.basePrice;\n\n var flvUtils = new core.utils.flavorUtils();\n\n _.each(selectedFlavors, function(elem) {\n var id = $(elem).data('flavor-id') + '';\n var selectedFlavor = this.flvUtils.getItem(id);\n if(selectedFlavor != null && selectedFlavor.sizes != null && selectedFlavor.sizes.length != null && selectedFlavor.sizes.length > 0) {\n var flavorPrice = selectedFlavor.sizes[0].price;\n newPrice += flavorPrice;\n }\n }, me);\n this.setDisplayedPrice(core.utils.formatPrice(newPrice));\n }," + }, + { + "tags": [ + { + "type": "param", + "types": [ + "String" + ], + "name": "price", + "description": "The price to be set" + } + ], + "description": { + "full": "

    Sets the displayed price to a specific value

    ", + "summary": "

    Sets the displayed price to a specific value

    ", + "body": "" + }, + "isPrivate": false, + "ignore": false, + "code": "setDisplayedPrice: function(price) {\n this.$el.find(\".total_price h3.right\").text(price);\n },\n\n demo: function() {\n var demoItem = {\n \"flavors\": {\n \"items\": [{\n \"flavors\": {\n \"items\": [{\n \"description\": \"\",\n \"sizes\": [{\n \"price\": 0.60,\n \"name\": \"normal\"\n }],\n \"pic\": \"\",\n \"main_item\": false,\n \"sub_item\": true,\n \"id\": \"1131061\",\n \"name\": \"Balsamico\"\n }, {\n \"description\": \"\",\n \"sizes\": [{\n \"price\": 0.60,\n \"name\": \"normal\"\n }],\n \"pic\": \"\",\n \"main_item\": false,\n \"sub_item\": true,\n \"id\": \"1131062\",\n \"name\": \"Caesar*1\"\n }],\n \"id\": \"76666\",\n \"structure\": \"0\"\n },\n \"description\": \"\",\n \"sizes\": [],\n \"pic\": \"\",\n \"main_item\": false,\n \"sub_item\": false,\n \"id\": \"76666\",\n \"name\": \"Extra Dressing\"\n }, {\n \"flavors\": {\n \"items\": [{\n \"description\": \"\",\n \"sizes\": [{\n \"price\": 0.00,\n \"name\": \"normal\"\n }],\n \"pic\": \"\",\n \"main_item\": false,\n \"sub_item\": true,\n \"id\": \"1131059\",\n \"name\": \"Balsamico\"\n }, {\n \"description\": \"\",\n \"sizes\": [{\n \"price\": 0.00,\n \"name\": \"normal\"\n }],\n \"pic\": \"\",\n \"main_item\": false,\n \"sub_item\": true,\n \"id\": \"1131060\",\n \"name\": \"Caesar*1 \"\n }, {\n \"description\": \"\",\n \"sizes\": [{\n \"price\": 0.00,\n \"name\": \"normal\"\n }],\n \"pic\": \"\",\n \"main_item\": false,\n \"sub_item\": true,\n \"id\": \"1131057\",\n \"name\": \"ohne Dressing\"\n }],\n \"id\": \"76665\",\n \"structure\": \"1\"\n },\n \"description\": \"\",\n \"sizes\": [],\n \"pic\": \"\",\n \"main_item\": false,\n \"sub_item\": false,\n \"id\": \"76665\",\n \"name\": \"Dressing\"\n }],\n \"id\": \"1633809\",\n \"structure\": \"-1\"\n },\n \"description\": \"und Parmesan (inkl. 1 Dressing) \",\n \"sizes\": [{\n \"price\": 4.50,\n \"name\": \"normal\"\n }],\n \"pic\": \"\",\n \"main_item\": true,\n \"sub_item\": false,\n \"id\": \"1633809\",\n \"name\": \"Gemischter Salat mit H\\u00e4hnchenbrustfilet \"\n }\n\n var flb = new core.views.FlavorsLightbox();\n var $result = $('Have a Gemischter Salat!').click( function() {\n flb.show(demoItem);\n flb.on(\"flavorsChosen\", function(flavorsObj) {\n //me.cartCollection.add(flavorsObj.selectedItemModel); //just a demo!\n });\n });\n return $result;\n }\n});\n\n//TODO: Temporarily Solution as long as backbone does not work with the current data structure\ncore.utils.flavorUtils = function(jsonData) {\n this.jsonData = jsonData;\n this.getItem = function(searchId) {\n return this.getItemRecursive(this.jsonData, searchId);\n }" + }, + { + "tags": [], + "description": { + "full": "

    returns the searched element or null

    ", + "summary": "

    returns the searched element or null

    ", + "body": "" + }, + "ignore": false, + "code": "this.getItemRecursive = function(curItem, searchId) {\n if(!!curItem.items && curItem.items.length > 0) {\n for(var i = 0, ii = curItem.items.length; i < ii; ++i) {\n var returnValue = this.getItemRecursive(curItem.items[i], searchId);\n if(returnValue != null) {\n return returnValue;\n }\n }\n } else if(curItem.flavors != null && curItem.flavors.id != null && curItem.flavors.id == searchId) {\n return curItem;\n } else if(curItem.flavors != null && curItem.flavors.items != null && curItem.flavors.items.length > 0) {\n var elems = curItem.flavors.items;\n for(var i = 0, ii = elems.length; i < ii; ++i) {\n var returnValue = this.getItemRecursive(elems[i], searchId);\n if(returnValue != null) {\n return returnValue;\n }\n }\n } else if(curItem.length != null && curItem.length > 0) {\n for(var i = 0, ii = curItem.length; i < ii; ++i) {\n var returnValue = this.getItemRecursive(curItem[i], searchId);\n if(returnValue != null) {\n return returnValue;\n }\n }\n } else {\n if(curItem.id == searchId) {\n return curItem;\n } else {\n return null;\n }\n }\n };", + "ctx": { + "type": "method", + "receiver": "this", + "name": "getItemRecursive", + "string": "this.getItemRecursive()" + } + }, + { + "tags": [], + "description": { + "full": "

    returns an array of items

    ", + "summary": "

    returns an array of items

    ", + "body": "" + }, + "ignore": false, + "code": "this.getFlavorsForItem = function(id) {\n var item = this.getItem(id);\n if(item != null && item.flavors != null && item.flavors.items != null && item.flavors.items.length > 0) {\n return item.flavors.items;\n }\n return null;\n };\n};", + "ctx": { + "type": "method", + "receiver": "this", + "name": "getFlavorsForItem", + "string": "this.getFlavorsForItem()" + } + } +] \ No newline at end of file diff --git a/utilitybelt/apps/UB_docs/data/core.views.Form.json b/utilitybelt/apps/UB_docs/data/core.views.Form.json new file mode 100644 index 0000000..9e67e25 --- /dev/null +++ b/utilitybelt/apps/UB_docs/data/core.views.Form.json @@ -0,0 +1,278 @@ +[ + { + "tags": [], + "description": { + "full": "

    Basic Form widget

    ", + "summary": "

    Basic Form widget

    ", + "body": "" + }, + "ignore": false, + "code": "core.define('core.views.Form', {\r\n\r\n extend: 'core.View',\r\n\r\n className: \"Form\"," + }, + { + "tags": [], + "description": { + "full": "

    Define events mapping

    ", + "summary": "

    Define events mapping

    ", + "body": "" + }, + "ignore": false, + "code": "events: {\r\n \"click input.submit\": \"submit\",\r\n \"submit form\": \"submit\",\r\n \"keypress input[type=text],textarea\": \"keypress\"\r\n }," + }, + { + "tags": [ + { + "type": "returns", + "string": "core.views.Form\r" + }, + { + "type": "constructor", + "string": "" + } + ], + "description": { + "full": "

    Constructor

    ", + "summary": "

    Constructor

    ", + "body": "" + }, + "isPrivate": false, + "ignore": false, + "code": "initialize: function() {\r\n this.validations = this.options.validations;\r\n }," + }, + { + "tags": [], + "description": { + "full": "

    Focus on the first field and select its content

    ", + "summary": "

    Focus on the first field and select its content

    ", + "body": "" + }, + "ignore": false, + "code": "selectFirstField: function() {\r\n\r\n var fields = $('input:visible', this.el);\r\n if (fields && fields[0]) {\r\n var field = $(fields[0]);\r\n if (field && field.focus && field.val) {\r\n field.focus();\r\n if (field.val()!='' && field.val()!=field.attr('placeholder'))\r\n field.select();\r\n }\r\n }\r\n }," + }, + { + "tags": [ + { + "type": "param", + "types": [ + "ArrayOfObject" + ], + "name": "errors", + "description": "Array of errors" + } + ], + "description": { + "full": "

    Mark fields via flashing in this form invalid in bulk.

    ", + "summary": "

    Mark fields via flashing in this form invalid in bulk.

    ", + "body": "" + }, + "isPrivate": false, + "ignore": false, + "code": "markInvalid: function(errors) { // TODO: rewrite completely, animation is terrible \r\n\r\n for (var i=0,ii=errors.length; iSave placeholder value before flashing

    ", + "summary": "

    Save placeholder value before flashing

    ", + "body": "" + }, + "ignore": false, + "code": "if (input.val) {\r\n input.attr('save_val', input.attr('placeholder'));\r\n input.attr('placeholder', '');\r\n }\r\n \r\n //-- Saving background --\r\n input.attr('save_background', input.css('background-color')); \r\n \r\n //adding error message if defined and put in html pwojcieszuk\r\n if(input.parent().children('.error_message').size() > 0)\r\n \tinput.parent().children('.error_message').text(errors[i].message);\r\n \r\n if (input.animate) {\r\n input.animate({backgroundColor: '#fffcb7'}, 300, function(){ // todo: convert to animation function\r\n $(this).animate({backgroundColor: '#fff'}, 200, function(){\r\n $(this).animate({backgroundColor: '#fffcb7'}, 300, function(){\r\n $(this).animate({backgroundColor: '#fff'}, 200, function(){\r\n $(this).animate({backgroundColor: '#fffcb7'}, 300, function(){\r\n $(this).animate({backgroundColor: '#fff'}, 200, function(){\r\n $(this).attr('placeholder', $(this).attr('save_val'));\r\n \r\n //-- Fix to avoid wide background in some fields --\r\n \t\t\t\t\t $(this).css('background-color', $(this).attr('save_background'));\r\n });\r\n });\r\n });\r\n });\r\n });\r\n }); \r\n }\r\n }\r\n \r\n return false;\r\n }," + }, + { + "tags": [ + { + "type": "param", + "types": [ + "ArrayOfObjects" + ], + "name": "errors", + "description": "Array of errors" + } + ], + "description": { + "full": "

    Mark invalid via simple label color change

    ", + "summary": "

    Mark invalid via simple label color change

    ", + "body": "" + }, + "isPrivate": false, + "ignore": false, + "code": "markInvalidSimple: function(errors) {\r\n var form = this.$el;\r\n this.clearInvalidSimple();\r\n for (var i=0,ii=errors.length; iClear invalid simple

    ", + "summary": "

    Clear invalid simple

    ", + "body": "" + }, + "ignore": false, + "code": "clearInvalidSimple: function() {\r\n var form = this.$el;\r\n $('input', form).parent('div').removeClass('input-error');\r\n }," + }, + { + "tags": [], + "description": { + "full": "

    Show inline loading spinner

    ", + "summary": "

    Show inline loading spinner

    ", + "body": "" + }, + "ignore": false, + "code": "showSpinner: function() {\r\n $('.spinner', this.el).show();\r\n }," + }, + { + "tags": [], + "description": { + "full": "

    Hide inline loading spinner

    ", + "summary": "

    Hide inline loading spinner

    ", + "body": "" + }, + "ignore": false, + "code": "hideSpinner: function() {\r\n $('.spinner', this.el).hide();\r\n }," + }, + { + "tags": [ + { + "type": "param", + "types": [ + "ArrayOfObjects" + ], + "name": "values", + "description": "Array of values" + } + ], + "description": { + "full": "

    Set form values

    ", + "summary": "

    Set form values

    ", + "body": "" + }, + "isPrivate": false, + "ignore": false, + "code": "setValues: function(mdl) {\r\n\r\n var values = mdl.attributes || mdl;\r\n\r\n if (values) {\r\n for (var field in values) {\r\n var field_el = $('input[name=\"' + field + '\"]', this.el);\r\n if (field_el.length)\r\n field_el.val(values[field]);\r\n }\r\n }\r\n }," + }, + { + "tags": [], + "description": { + "full": "

    On key press handler

    ", + "summary": "

    On key press handler

    ", + "body": "" + }, + "ignore": false, + "code": "keypress: function(ev) {" + }, + { + "tags": [], + "description": { + "full": "

    f there was an error message and the input is being filled again, remove error message

    ", + "summary": "

    f there was an error message and the input is being filled again, remove error message

    ", + "body": "" + }, + "ignore": false, + "code": "var changedInput = $(\"input[name='\" + ev.target.name +\"']\");\r\n \tif(changedInput.parent().children('.error_message').size() > 0)\r\n \t\tchangedInput.parent().children('.error_message').text('');\r\n },", + "ctx": { + "type": "declaration", + "name": "changedInput", + "value": "$(\"input[name='\" + ev.target.name +\"']\")", + "string": "changedInput" + } + }, + { + "tags": [ + { + "type": "param", + "types": [ + "Object" + ], + "name": "data", + "description": "Form data \r" + }, + { + "type": "returns", + "string": "{ArrayOfObjects} errors Array of errors" + } + ], + "description": { + "full": "

    Validate form data against validations defined on Model [DEPRECATED] Use Model.validate

    ", + "summary": "

    Validate form data against validations defined on Model [DEPRECATED] Use Model.validate

    ", + "body": "" + }, + "isPrivate": false, + "ignore": false, + "code": "validate: function(data) {\r\n var errors = []; // basic validations, afterwards we'll use more solid library for this\r\n var validations = this.model.validations;\r\n for (var i in validations) {\r\n var validation = validations[i],\r\n field = validation.field,\r\n field_el = $('input:visible[name=\"' + field + '\"]', this.el),\r\n visible = (field_el.size() > 0),\r\n field_empty = ($.trim(data[field])+''=='' || field_el.attr('placeholder') == field_el.val());\r\n\r\n if (visible && field_empty && validation['type'] == 'required') {\r\n \terrorMessage = (validation['message_key'])? jsGetText(validation['message_key']): 'Field is required';\r\n \terrors.push({ field: field, type: 'required', message: errorMessage });\r\n }\r\n }\r\n return errors;\r\n }," + }, + { + "tags": [ + { + "type": "param", + "types": [ + "Object" + ], + "name": "data", + "description": "Form data \r" + }, + { + "type": "param", + "types": [ + "Object" + ], + "name": "options", + "description": "Ajax call options ('url', 'type' and 'success' for success callback)" + } + ], + "description": { + "full": "

    Send form data via jQuery's Ajax call
    Use inline spinner as a loading indicator

    ", + "summary": "

    Send form data via jQuery's Ajax call
    Use inline spinner as a loading indicator

    ", + "body": "" + }, + "isPrivate": false, + "ignore": false, + "code": "send: function(data, options) {\r\n var me = this;\r\n this.showSpinner();\r\n $.ajax({\r\n url: options.url,\r\n type: options.type || 'post',\r\n dataType: 'json',\r\n success: _.bind(options.success, this),\r\n error: function() { me.hideSpinner; console.log('error', arguments) },\r\n// complete: this.hideSpinner,\r\n data: data\r\n });\r\n }," + }, + { + "tags": [ + { + "type": "param", + "types": [ + "Object" + ], + "name": "data", + "description": "Form data \r" + }, + { + "type": "param", + "types": [ + "Object" + ], + "name": "options", + "description": "Ajax call options ('url', 'type' and 'success' for success callback)" + } + ], + "description": { + "full": "

    Submit form handler
    Serialize form data and validate it. If validation succeeds, send it, if failed -- use markInvalid to mark errors

    ", + "summary": "

    Submit form handler
    Serialize form data and validate it. If validation succeeds, send it, if failed -- use markInvalid to mark errors

    ", + "body": "" + }, + "isPrivate": false, + "ignore": false, + "code": "submit: function() {\r\n\r\n var options = this.options, validations = this.validations, me = this;\r\n\r\n var form = this.options.form || this.el;\r\n\r\n // TODO: visible selector should be removed, it was temporary patch\r\n var data = {}, data_arr = $('input', form).serializeArray();\r\n\r\n for (var i=0,ii=data_arr.length; iSerialize and returns Form data as a JSON

    ", + "summary": "

    Serialize and returns Form data as a JSON

    ", + "body": "" + }, + "ignore": false, + "code": "getFormValues: function(){\r\n var data = {}, data_arr = $('input:visible', this.form).serializeArray();\r\n\r\n for (var i=0,ii=data_arr.length; iValidate the form data and save the Model if validated successfully, mark errors otherwise

    ", + "summary": "

    Validate the form data and save the Model if validated successfully, mark errors otherwise

    ", + "body": "" + }, + "ignore": false, + "code": "formValidate: function() { \r\n var data = this.getFormValues();\r\n this.model.set(data, { silent: true });\r\n \r\n if (this.model.isValid()) {\r\n this.model.save( {}, { success: this.options.onSuccess } ); \r\n } else {\r\n var errors = this.model.getValidationErrors();\r\n this.markInvalidSimple(errors);\r\n }\r\n },\r\n\r\n demo: function() {\r\n return $('')\r\n } \r\n\r\n});" + } +] \ No newline at end of file diff --git a/utilitybelt/apps/UB_docs/data/core.views.Lightbox.json b/utilitybelt/apps/UB_docs/data/core.views.Lightbox.json new file mode 100644 index 0000000..db8cdc9 --- /dev/null +++ b/utilitybelt/apps/UB_docs/data/core.views.Lightbox.json @@ -0,0 +1,278 @@ +[ + { + "tags": [ + { + "type": "title", + "string": "Basic Lightbox\r" + }, + { + "type": "param", + "types": [ + "String" + ], + "name": "title", + "description": "Title\r" + }, + { + "type": "param", + "types": [ + "String" + ], + "name": "content", + "description": "Content\r" + }, + { + "type": "param", + "types": [ + "Number" + ], + "name": "x", + "description": "X coordinate on the screen\r" + }, + { + "type": "param", + "types": [ + "Number" + ], + "name": "y", + "description": "Y coordinate on the screen\r" + }, + { + "type": "param", + "types": [ + "String" + ], + "name": "tempate", + "description": "Path to template" + } + ], + "description": { + "full": "

    Basic Lightbox with the title, close icon, and content

    \n\n

    Example

    \n\n
    var lb = new core.views.Lightbox({ title: 'Some awesome title', content: 'Awesomest content' });\nlb.show();\n
    ", + "summary": "

    Basic Lightbox with the title, close icon, and content

    \n\n

    Example

    \n\n
    var lb = new core.views.Lightbox({ title: 'Some awesome title', content: 'Awesomest content' });\nlb.show();\n
    ", + "body": "" + }, + "isPrivate": false, + "ignore": false, + "code": "core.define('core.views.Lightbox', {\r\n\r\n extend: 'core.View',\r\n\r\n className: \"Lightbox\",\r\n\r\n template: 'Lightbox/base.html',\r\n\r\n events: {\r\n \"click .top-box .close-icon\": \"hide\"\r\n }," + }, + { + "tags": [], + "description": { + "full": "

    Init blanket layer which cover the page to prevent interaction when Lightbox is modal.
    Click on blanket close the Lightbox.

    ", + "summary": "

    Init blanket layer which cover the page to prevent interaction when Lightbox is modal.
    Click on blanket close the Lightbox.

    ", + "body": "" + }, + "ignore": false, + "code": "initBlanket: function() {\r\n this.blanket = $('#lightbox_blanket');\r\n this.blanket.bind('click', _.bind(this.hide, this));\r\n this.blanket.height($(document).height());\r\n this.blanket.width($(document).width());\r\n // IE7 complains about blanket positioning, hence the need to\r\n // force it explicitly at the top left corner of the window\r\n $(this.blanket).css({'top': '0', 'left': '0', 'position': 'absolute' });\r\n }," + }, + { + "tags": [], + "description": { + "full": "

    Hide Lightbox, destroy its DOM structure, hide blanket

    ", + "summary": "

    Hide Lightbox, destroy its DOM structure, hide blanket

    ", + "body": "" + }, + "ignore": false, + "code": "hide: function() {\r\n this.$el.trigger('hide');\r\n this.remove(); // remove from DOM, to prevent flooding\r\n if (this.isModal() && this.blanket) {\r\n this.blanket.hide();\r\n }\r\n }," + }, + { + "tags": [], + "description": { + "full": "

    Init Lightbox position and layout properties

    ", + "summary": "

    Init Lightbox position and layout properties

    ", + "body": "" + }, + "ignore": false, + "code": "initPosition: function() {\r\n this.$el.css({ position:' fixed', 'margin': '0px' });\r\n\r\n this.body = this.getBody();\r\n\r\n if (this.options.height) {\r\n this.body.height(this.options.height);\r\n this.body.css({ 'overflow-x': 'hidden', 'overflow-y': 'auto' });\r\n }\r\n \r\n if (this.options.x && this.options.y) {\r\n this.$el.css({ top: this.options.x + 'px', left: this.options.y + 'px' })\r\n }\r\n else {\r\n this.center();\r\n }\r\n\r\n }," + }, + { + "tags": [ + { + "type": "constructor", + "string": "" + } + ], + "description": { + "full": "

    Initialize the widget

    ", + "summary": "

    Initialize the widget

    ", + "body": "" + }, + "isPrivate": false, + "ignore": false, + "code": "initialize: function() {\r\n\r\n var template = this.options.template || this.template;\r\n\r\n var me = this;\r\n core.utils.getTemplate(template, function(tpl) {\r\n me.render(tpl);\r\n });\r\n\r\n }," + }, + { + "tags": [ + { + "type": "param", + "types": [ + "String" + ], + "name": "tpl", + "description": "Widget template" + } + ], + "description": { + "full": "

    Render Lightbox

    ", + "summary": "

    Render Lightbox

    ", + "body": "" + }, + "isPrivate": false, + "ignore": false, + "code": "render: function(tpl) {\r\n\r\n this.el = $(tpl);\r\n this.$el = this.el.appendTo($('body')).hide();\r\n this.delegateEvents();" + }, + { + "tags": [], + "description": { + "full": "

    Assign user-defined events TODO: put in our core View wrapper

    ", + "summary": "

    Assign user-defined events TODO: put in our core View wrapper

    ", + "body": "" + }, + "ignore": false, + "code": "if (this.options.events) {\r\n this.delegateEvents(_.extend(this.events, this.options.events));\r\n }\r\n\r\n this.initPosition();\r\n\r\n var title = this.title || this.options.title || this.$el.attr('header');\r\n if (title" + }, + { + "tags": [], + "description": { + "full": "

    & $('.title-container', this.$el).length < 1

    ", + "summary": "

    & $('.title-container', this.$el).length < 1

    ", + "body": "" + }, + "ignore": false, + "code": "{\r\n this.setTitle(title);\r\n }\r\n\r\n var content = this.options.content;\r\n if (content) {\r\n this.setContent(content);\r\n }\r\n\r\n core.views.Lightbox.__super__.render.call(this);\r\n\r\n }," + }, + { + "tags": [ + { + "type": "param", + "types": [ + "String" + ], + "name": "title", + "description": "Lightbox Title" + } + ], + "description": { + "full": "

    Set title for the Lightbox

    ", + "summary": "

    Set title for the Lightbox

    ", + "body": "" + }, + "isPrivate": false, + "ignore": false, + "code": "setTitle: function(title) {\r\n $('.title-container', this.$el).html(title);\r\n }," + }, + { + "tags": [ + { + "type": "param", + "types": [ + "String" + ], + "name": "content", + "description": "Lightbox Content" + } + ], + "description": { + "full": "

    Set content for the Lightbox

    ", + "summary": "

    Set content for the Lightbox

    ", + "body": "" + }, + "isPrivate": false, + "ignore": false, + "code": "setContent: function(content) {\r\n this.getBody().html(content);\r\n }," + }, + { + "tags": [ + { + "type": "returns", + "string": "Boolean" + } + ], + "description": { + "full": "

    Check if Lightbox is modal

    ", + "summary": "

    Check if Lightbox is modal

    ", + "body": "" + }, + "isPrivate": false, + "ignore": false, + "code": "isModal: function() {\r\n return this.options.modal == undefined || this.options.modal === true;\r\n }," + }, + { + "tags": [ + { + "type": "method", + "string": "showError" + } + ], + "description": { + "full": "

    Show inline error via .message element

    ", + "summary": "

    Show inline error via .message element

    ", + "body": "" + }, + "isPrivate": false, + "ignore": false, + "code": "showError: function(text) {\r\n $('.message', this.$el).html(text);\r\n }," + }, + { + "tags": [], + "description": { + "full": "

    Clear error from the .message element

    ", + "summary": "

    Clear error from the .message element

    ", + "body": "" + }, + "ignore": false, + "code": "clearError: function() {\r\n $('.message', this.$el).html('');\r\n }," + }, + { + "tags": [], + "description": { + "full": "

    Show Lightbox

    ", + "summary": "

    Show Lightbox

    ", + "body": "" + }, + "ignore": false, + "code": "show: function() {\r\n var me = this;\r\n me.on('render', function() {\r\n me.clearError();\r\n me.$el.show();\r\n if (me.isModal()) {\r\n me.initBlanket();\r\n me.blanket.show();\r\n }\r\n });\r\n }," + }, + { + "tags": [], + "description": { + "full": "

    Center Lightbox

    ", + "summary": "

    Center Lightbox

    ", + "body": "" + }, + "ignore": false, + "code": "center: function () {\r\n this.$el.css(\"position\", \"fixed\");\r\n var visualFix = parseInt($(window).height()/15);\r\n this.$el.css(\"top\", '130px');\r\n this.$el.css(\"left\", (($(window).width() - this.$el.outerWidth()) / 2) + $(window).scrollLeft() + \"px\");\r\n return this;\r\n }," + }, + { + "tags": [ + { + "type": "returns", + "string": "jQuery" + } + ], + "description": { + "full": "

    Returns body element

    ", + "summary": "

    Returns body element

    ", + "body": "" + }, + "isPrivate": false, + "ignore": false, + "code": "getBody: function() {\r\n return $('.body-box', this.$el);\r\n }," + }, + { + "tags": [ + { + "type": "returns", + "string": "jQuery" + } + ], + "description": { + "full": "

    Adds link to title container and return its element

    ", + "summary": "

    Adds link to title container and return its element

    ", + "body": "" + }, + "isPrivate": false, + "ignore": false, + "code": "addTitleLink: function(html) {\r\n var $title_link = $('.top-box .title-link', this.$el);\r\n return $title_link.append(html);\r\n },\r\n\r\n demo: function() {\r\n var $result = $('Click Me')\r\n .click( function() {\r\n var lb = new core.views.Lightbox({ title: 'Some awesome title', content: 'Awesomest content' });\r\n lb.show();\r\n });\r\n return $result;\r\n }\r\n\r\n});" + } +] \ No newline at end of file diff --git a/utilitybelt/apps/UB_docs/data/core.views.LoginForm.json b/utilitybelt/apps/UB_docs/data/core.views.LoginForm.json new file mode 100644 index 0000000..217f081 --- /dev/null +++ b/utilitybelt/apps/UB_docs/data/core.views.LoginForm.json @@ -0,0 +1,122 @@ +[ + { + "tags": [ + { + "type": "param", + "types": [ + "Object" + ], + "name": "model", + "description": "An instance of a user model" + } + ], + "description": { + "full": "

    Form for User login

    \n\n

    Example

    \n\n
    var form = new core.views.LoginForm({ model: theUser });\nform.renderTo($('#my_container'));\n
    ", + "summary": "

    Form for User login

    ", + "body": "

    Example

    \n\n
    var form = new core.views.LoginForm({ model: theUser });\nform.renderTo($('#my_container'));\n
    " + }, + "isPrivate": false, + "ignore": false, + "code": "core.define('core.views.LoginForm', {\n extend: 'core.views.Form',\n className: 'LoginForm',\n template: '',\n \n events: {\n 'submit form': 'authorize',\n 'click .submit': 'authorize'\n }," + }, + { + "tags": [ + { + "type": "param", + "types": [ + "Object" + ], + "name": "options", + "description": "A hash containing the view options" + } + ], + "description": { + "full": "

    Constructor.
    Instantiates an empty User model if none provided.
    Renders the form with the provided template (defaults to Login/LoginForm.html).
    Sets up authorize callbacks.

    ", + "summary": "

    Constructor.
    Instantiates an empty User model if none provided.
    Renders the form with the provided template (defaults to Login/LoginForm.html).
    Sets up authorize callbacks.

    ", + "body": "" + }, + "isPrivate": false, + "ignore": false, + "code": "initialize: function(options) {\n var me = this;\n this.model = this.options.model || new core.models.User();\n var tpl = this.options.template || 'Login/LoginForm.html';\n\n this.model.on('authorize:grant', function() { me.authorizeSuccess.apply(me, arguments); });\n this.model.on('authorize:deny', function() { me.authorizeError.apply(me, arguments); });\n\n core.utils.getTemplate(tpl, function(tpl) {\n me.render(tpl);\n });\n }," + }, + { + "tags": [ + { + "type": "param", + "types": [ + "String" + ], + "name": "tpl", + "description": "The html template used to render the form" + } + ], + "description": { + "full": "

    Renders the widget using the given template.

    ", + "summary": "

    Renders the widget using the given template.

    ", + "body": "" + }, + "isPrivate": false, + "ignore": false, + "code": "render: function(tpl) {\n this.template = _.template(tpl);\n this.$el.append(this.template);\n this.form = this.$el.find('form');\n this.clearInvalidSimple();\n core.views.UserAddressForm.__super__.render.call(this);\n }," + }, + { + "tags": [], + "description": { + "full": "

    Validates login inputs and forward authorization query to the User model.

    ", + "summary": "

    Validates login inputs and forward authorization query to the User model.

    ", + "body": "" + }, + "ignore": false, + "code": "authorize: function() {\n var data = this.getFormValues();\n this.model.set(data, { silent: true });\n if (!this.model.isValid()) {\n var errors = this.model.getValidationErrors();\n this.markInvalid(errors);\n } else {\n this.model.authorize(data.pwd);\n }\n }," + }, + { + "tags": [ + { + "type": "param", + "types": [ + "core.models.User" + ], + "name": "user", + "description": "The freshly authorized user" + } + ], + "description": { + "full": "

    Authorization success handler.

    ", + "summary": "

    Authorization success handler.

    ", + "body": "" + }, + "isPrivate": false, + "ignore": false, + "code": "authorizeSuccess: function(user) {\n var bits = ['Hi,', user.get('name'), user.get('last_name'), '!'];\n this.$el.text(bits.join(' '));\n }," + }, + { + "tags": [ + { + "type": "param", + "types": [ + "core.models.User" + ], + "name": "user", + "description": "The unauthorized user" + } + ], + "description": { + "full": "

    Authorization error handler.

    ", + "summary": "

    Authorization error handler.

    ", + "body": "" + }, + "isPrivate": false, + "ignore": false, + "code": "authorizeError: function(user) {\n this.markInvalidSimple([{field: 'email'}, {field: 'pwd'}])\n }," + }, + { + "tags": [], + "description": { + "full": "

    Demo code

    ", + "summary": "

    Demo code

    ", + "body": "" + }, + "ignore": false, + "code": "demo: function() {\n var form = new core.views.LoginForm();\n return form;\n }\n});" + } +] \ No newline at end of file diff --git a/utilitybelt/apps/UB_docs/data/core.views.LoginLightbox.json b/utilitybelt/apps/UB_docs/data/core.views.LoginLightbox.json new file mode 100644 index 0000000..a87feae --- /dev/null +++ b/utilitybelt/apps/UB_docs/data/core.views.LoginLightbox.json @@ -0,0 +1,56 @@ +[ + { + "tags": [ + { + "type": "extends", + "string": "core.views.Lightbox" + } + ], + "description": { + "full": "

    Lightbox with the Login Form inside

    \n\n

    Examples

    \n\n
    var lb = new core.views.LoginLightbox({ title: jsGetText('login') });\nlb.show();\n
    ", + "summary": "

    Lightbox with the Login Form inside

    ", + "body": "

    Examples

    \n\n
    var lb = new core.views.LoginLightbox({ title: jsGetText('login') });\nlb.show();\n
    " + }, + "isPrivate": false, + "ignore": false, + "code": "core.define('core.views.LoginLightbox', {\n\n extend: 'core.views.Lightbox'," + }, + { + "tags": [ + { + "type": "method", + "string": "initialize" + }, + { + "type": "constructor", + "string": "" + }, + { + "type": "param", + "types": [ + "Object" + ], + "name": "options", + "description": "Options passed to the login form view" + } + ], + "description": { + "full": "

    Constructor.

    ", + "summary": "

    Constructor.

    ", + "body": "" + }, + "isPrivate": false, + "ignore": false, + "code": "initialize: function(options) {\n this.on('render', function() {\n var lb = this;\n this.addItem(core.views.LoginForm, this.options);\n });\n core.views.LoginLightbox.__super__.initialize.call(this);\n }," + }, + { + "tags": [], + "description": { + "full": "

    Demo code.

    ", + "summary": "

    Demo code.

    ", + "body": "" + }, + "ignore": false, + "code": "demo: function() {\n var $result = $('Click Me')\n .click(function() {\n var lb = new core.views.LoginLightbox({ title: jsGetText('login_title') });\n lb.show();\n });\n return $result;\n }\n});" + } +] \ No newline at end of file diff --git a/utilitybelt/apps/UB_docs/data/core.views.MultipleAddressLightbox.json b/utilitybelt/apps/UB_docs/data/core.views.MultipleAddressLightbox.json new file mode 100644 index 0000000..97973ce --- /dev/null +++ b/utilitybelt/apps/UB_docs/data/core.views.MultipleAddressLightbox.json @@ -0,0 +1,112 @@ +[ + { + "tags": [ + { + "type": "extends", + "string": "core.views.Lightbox" + } + ], + "description": { + "full": "

    Lightbox with the User Addresses List inside

    \n\n

    Examples

    \n\n
    var lb = new core.views.MultipleAddressLightbox();\nlb.show();\n
    ", + "summary": "

    Lightbox with the User Addresses List inside

    \n\n

    Examples

    \n\n
    var lb = new core.views.MultipleAddressLightbox();\nlb.show();\n
    ", + "body": "" + }, + "isPrivate": false, + "ignore": false, + "code": "core.define('core.views.MultipleAddressLightbox', {\r\n\r\n extend: 'core.views.Lightbox',\r\n\r\n template: [\"MultipleAddress/MultipleAddress.html\"],\r\n\r\n events: {\r\n \"click .top-box .close-icon\": \"hide\"\r\n // ,\"click li\": \"addressSelectHandler\"\r\n },\r\n\r\n __shown: false,\r\n\r\n className: 'MultipleAddressLightbox'," + }, + { + "tags": [ + { + "type": "constructor\r", + "string": "" + }, + { + "type": "param", + "types": [ + "Object" + ], + "name": "options", + "description": "Options" + } + ], + "description": { + "full": "

    Constructor

    ", + "summary": "

    Constructor

    ", + "body": "" + }, + "isPrivate": false, + "ignore": false, + "code": "initialize: function(options) {\r\n this.locations = this.options.locations;\r\n var me = this;\r\n this.title = jsGetText(\"multiple_addresses\");\r\n\r\n core.utils.getTemplate(me.template, function(tpl) {\r\n me.template = _.template(tpl)();\r\n });\r\n\r\n this.on('render', function() {\r\n //adding the map and the list\r\n me.initPosition();\r\n me.map = me.addItem(core.views.Map, me.locations, \".maps\");\r\n me.map.on(\"markerClick\", function(elem) {\r\n me.addressMapSelectHandler.call(me, elem)\r\n });\r\n var listOptions = {\r\n addresses: me.locations,\r\n getBackgroundCSS: me.getBackgroundCSS\r\n };\r\n me.map.render();\r\n me.list = me.addItem(core.views.MultipleAddressList, me.options, \".streets\");\r\n me.list.on(\"locationSelected\", function(elem) {\r\n me.addressSelectHandler.call(me, elem);\r\n });\r\n me.list.render();\r\n });\r\n },\r\n addItem: function(type, config, place) {\r\n if(place != null) {\r\n var positionElement = $('.body-box', this.$el).find(place);\r\n var new_el = new type(config);\r\n $(positionElement).html(new_el.$el);\r\n return new_el;\r\n } else if(this.getBody) {\r\n core.views.MultipleAddressLightbox.__super__.addItem.call(this, type, config);\r\n }\r\n }," + }, + { + "tags": [ + { + "type": "param", + "types": [ + "Object" + ], + "name": "list", + "description": "" + } + ], + "description": { + "full": "

    setAddressList

    ", + "summary": "

    setAddressList

    ", + "body": "" + }, + "isPrivate": false, + "ignore": false, + "code": "setAddressList: function(list) {\r\n this.addressList = list;\r\n }," + }, + { + "tags": [], + "description": { + "full": "", + "summary": "", + "body": "" + }, + "ignore": false, + "code": "getAddressList: function() {\r\n return this.addressList;\r\n }," + }, + { + "tags": [ + { + "type": "param", + "types": [ + "Object" + ], + "name": "elem", + "description": "" + } + ], + "description": { + "full": "

    addressSelectHandler

    ", + "summary": "

    addressSelectHandler

    ", + "body": "" + }, + "isPrivate": false, + "ignore": false, + "code": "addressSelectHandler: function(elem) {\r\n //TODO do something here\r\n this.trigger(\"locationSelected\", elem);\r\n }," + }, + { + "tags": [ + { + "type": "param", + "types": [ + "Object" + ], + "name": "elem", + "description": "" + } + ], + "description": { + "full": "

    addressMapSelectHandler

    ", + "summary": "

    addressMapSelectHandler

    ", + "body": "" + }, + "isPrivate": false, + "ignore": false, + "code": "addressMapSelectHandler: function(elem) {\r\n //TODO do something here\r\n this.trigger(\"locationSelected\", elem);\r\n },\r\n hide: function() {\r\n //TODO as soon as we have a container object which keeps children we can add them here\r\n this.map.hide();\r\n this.list.remove();\r\n this.$el.remove();\r\n this.__shown = false;\r\n },\r\n demo: function() {\r\n\r\n var locations = {\r\n \"pagination\": {\r\n \"total_items\": 2,\r\n \"limit\": 10,\r\n \"total_pages\": 1,\r\n \"page\": 1,\r\n \"offset\": 0\r\n },\r\n \"data\": [{\r\n \"uri_search\": \"http://mockapi.lieferheld.de/restaurants/?city=berlin&lat=45.23234&long=37.77234\",\r\n \"address\": {\r\n \"city_slug\": \"berlin\",\r\n \"city\": \"Berlin\",\r\n \"street_number\": \"60\",\r\n \"latitude\": 45.232340000000001,\r\n \"country\": \"DE\",\r\n \"street_name\": \"Mohrenstrasse\",\r\n \"zipcode\": \"10117\",\r\n \"longitude\": 37.77234\r\n }\r\n }, {\r\n \"uri_search\": \"http://mockapi.lieferheld.de/restaurants/?city=berlin&lat=46.2004&long=37.88774\",\r\n \"address\": {\r\n \"city_slug\": \"berlin\",\r\n \"city\": \"Berlin\",\r\n \"street_number\": \"59\",\r\n \"latitude\": 46.40000000000002,\r\n \"country\": \"DE\",\r\n \"street_name\": \"Mohrenstrasse\",\r\n \"zipcode\": \"10117\",\r\n \"longitude\": 37.887740000000001\r\n }\r\n }, {\r\n \"uri_search\": \"http://mockapi.lieferheld.de/restaurants/?city=berlin&lat=46.2004&long=37.88774\",\r\n \"address\": {\r\n \"city_slug\": \"berlin\",\r\n \"city\": \"Berlin\",\r\n \"street_number\": \"59\",\r\n \"latitude\": 46.200400000000002,\r\n \"country\": \"DE\",\r\n \"street_name\": \"Mohrenstrasse\",\r\n \"zipcode\": \"10117\",\r\n \"longitude\": 37.987740000000001\r\n }\r\n }, {\r\n \"uri_search\": \"http://mockapi.lieferheld.de/restaurants/?city=berlin&lat=46.2004&long=37.88774\",\r\n \"address\": {\r\n \"city_slug\": \"berlin\",\r\n \"city\": \"Berlin\",\r\n \"street_number\": \"59\",\r\n \"latitude\": 46.400400000000002,\r\n \"country\": \"DE\",\r\n \"street_name\": \"Mohrenstrasse\",\r\n \"zipcode\": \"10117\",\r\n \"longitude\": 37.987740000000001\r\n }\r\n }]\r\n };\r\n\r\n var $result = $('Click Me')\r\n .click( function() {\r\n var lb = new core.views.MultipleAddressLightbox({\r\n locations: locations.data\r\n });\r\n lb.show();\r\n });\r\n return $result;\r\n }\r\n});" + } +] \ No newline at end of file diff --git a/utilitybelt/apps/UB_docs/data/core.views.ReadOnlyCartBox.json b/utilitybelt/apps/UB_docs/data/core.views.ReadOnlyCartBox.json new file mode 100644 index 0000000..94f2c5a --- /dev/null +++ b/utilitybelt/apps/UB_docs/data/core.views.ReadOnlyCartBox.json @@ -0,0 +1,12 @@ +[ + { + "tags": [], + "description": { + "full": "

    Shopping Cart container (read only mode), for inline display.

    ", + "summary": "

    Shopping Cart container (read only mode), for inline display.

    ", + "body": "" + }, + "ignore": false, + "code": "core.define('core.views.ReadOnlyCartBox', {\n extend: 'core.views.CartBox',\n\n demo: function() {\n var items = [\n {\n \"description\": \"inkl. 0,15\\u20ac Pfand\",\n \"sizes\": [{\"price\": 5, \"name\": \"L\"}],\n \"pic\": \"\",\n \"main_item\": true,\n \"sub_item\": false,\n \"id\": \"mp1\",\n \"name\": \"Fanta*1,3,5,7 0,5L \"\n }, {\n \"description\": \"inkl. 0,15\\u20ac Pfand\",\n \"sizes\": [{\"price\": 20, \"name\": \"XL\"}],\n \"pic\": \"\",\n \"main_item\": true,\n \"sub_item\": false,\n \"id\": \"mp2\",\n \"name\": \"Pain saucisse\"\n }\n ];\n var cart = new core.collections.Cart();\n for (var i=0, bound=items.length; iForm for editing User Address

    \n\n

    Example

    \n\n
    var form = new core.views.UserAddressForm({ id: 'a2', user_id: 'a1' });\nform.renderTo($('my_container'));\n
    ", + "summary": "

    Form for editing User Address

    \n\n

    Example

    \n\n
    var form = new core.views.UserAddressForm({ id: 'a2', user_id: 'a1' });\nform.renderTo($('my_container'));\n
    ", + "body": "" + }, + "isPrivate": false, + "ignore": false, + "code": "core.define('core.views.UserAddressForm', {\r\n extend: 'core.views.Form',\r\n className: 'UserAddressForm',\r\n template: '',\r\n user_id: null,\r\n model: null,\r\n \r\n events: {\r\n \"submit form\": \"formValidate\",\r\n \"click .submit\": \"formValidate\",\r\n \"click a.back\": \"onBackLinkClick\"\r\n }," + }, + { + "tags": [], + "description": { + "full": "

    Constructor

    ", + "summary": "

    Constructor

    ", + "body": "" + }, + "ignore": false, + "code": "initialize: function(){\r\n\r\n var me = this;\r\n this.user_id = this.options.user_id;\r\n this.id = this.options.id;\r\n\r\n this.record = this.options.record;\r\n\r\n this.model = new core.models.Address({ user_id: this.user_id, id: this.id });\r\n\r\n core.utils.getTemplate('UserAddress/UserAddressForm.html', function(tpl) {\r\n me.render(tpl);\r\n });\r\n \r\n }," + }, + { + "tags": [], + "description": { + "full": "

    Render the widget using the template

    ", + "summary": "

    Render the widget using the template

    ", + "body": "" + }, + "ignore": false, + "code": "render: function(tpl){\r\n var me = this;\r\n me.template = _.template(tpl);\r\n me.$el.append(me.template);\r\n me.form = me.$el.find('form');\r\n me.clearInvalidSimple();\r\n me.loadAddressData();\r\n core.views.UserAddressForm.__super__.render.call(this);\r\n }," + }, + { + "tags": [], + "description": { + "full": "

    Populate with the address data

    ", + "summary": "

    Populate with the address data

    ", + "body": "" + }, + "ignore": false, + "code": "loadAddressData: function(){\r\n var me = this;\r\n if (this.record) {\r\n me.model = this.record;\r\n me.setValues(this.record);\r\n }\r\n else if (this.options.id) {\r\n this.model.fetch({ success: function(mdl) {\r\n me.setValues(mdl)\r\n } });\r\n }\r\n }," + }, + { + "tags": [], + "description": { + "full": "

    Runs when user clicks on a \"Back\" link

    ", + "summary": "

    Runs when user clicks on a \"Back\" link

    ", + "body": "" + }, + "ignore": false, + "code": "onBackLinkClick: function() {\r\n if (this.options && this.options.onBackLinkClick)\r\n this.options.onBackLinkClick();\r\n }," + }, + { + "tags": [], + "description": { + "full": "

    Demo code

    ", + "summary": "

    Demo code

    ", + "body": "" + }, + "ignore": false, + "code": "demo: function() {\r\n var form = new core.views.UserAddressForm({ id: 1 });\r\n return form;\r\n }\r\n\r\n});" + } +] \ No newline at end of file diff --git a/utilitybelt/apps/UB_docs/data/core.views.UserAddressLightbox.json b/utilitybelt/apps/UB_docs/data/core.views.UserAddressLightbox.json new file mode 100644 index 0000000..fbfee0d --- /dev/null +++ b/utilitybelt/apps/UB_docs/data/core.views.UserAddressLightbox.json @@ -0,0 +1,46 @@ +[ + { + "tags": [ + { + "type": "extends", + "string": "core.views.Lightbox" + } + ], + "description": { + "full": "

    Lightbox with the User Address Form inside

    \n\n

    Examples

    \n\n
    var lb = new core.views.UserAddressLightbox({ id: 1, title: jsGetText('save_address') }); // fetch the record from the back-end\nlb.show();\n\nvar lb2 = new core.views.UserAddressLightbox({ record: { ... } }); // use the predefined record\n\nvar lb3 = new core.views.UserAddressLightbox({ title: jsGetText('new_address') }); // open empty form\nlb3.on('render', function(lightbox) {  // populate with data\n    lightbox.getForm().populate( { ... } ) \n});\n
    ", + "summary": "

    Lightbox with the User Address Form inside

    \n\n

    Examples

    \n\n
    var lb = new core.views.UserAddressLightbox({ id: 1, title: jsGetText('save_address') }); // fetch the record from the back-end\nlb.show();\n\nvar lb2 = new core.views.UserAddressLightbox({ record: { ... } }); // use the predefined record\n\nvar lb3 = new core.views.UserAddressLightbox({ title: jsGetText('new_address') }); // open empty form\nlb3.on('render', function(lightbox) {  // populate with data\n    lightbox.getForm().populate( { ... } ) \n});\n
    ", + "body": "" + }, + "isPrivate": false, + "ignore": false, + "code": "core.define('core.views.UserAddressLightbox', {\r\n\r\n extend: 'core.views.Lightbox'," + }, + { + "tags": [ + { + "type": "method", + "string": "initialize\r" + }, + { + "type": "constructor\r", + "string": "" + }, + { + "type": "param", + "types": [ + "Object" + ], + "name": "options", + "description": "Options" + } + ], + "description": { + "full": "

    Initialize

    ", + "summary": "

    Initialize

    ", + "body": "" + }, + "isPrivate": false, + "ignore": false, + "code": "initialize: function(options){\r\n \r\n this.on('render', function() { \r\n var lb = this;\r\n this.options.onSuccess = function() { \r\n lb.hide(); \r\n \tnew core.views.UserAddressListLightbox().show();\r\n };\r\n this.options.onBackLinkClick = function() { \r\n lb.hide(); \r\n \tnew core.views.UserAddressListLightbox().show();\r\n };\r\n this.addItem(core.views.UserAddressForm, this.options);\r\n });\r\n\r\n core.views.UserAddressLightbox.__super__.initialize.call(this);\r\n\r\n },\r\n\r\n demo: function() {\r\n var $result = $('Click Me')\r\n .click( function() {\r\n var lb = new core.views.UserAddressLightbox({ id: 1, title: jsGetText('save_address') });\r\n lb.show();\r\n });\r\n return $result;\r\n }\r\n\r\n \r\n});" + } +] \ No newline at end of file diff --git a/utilitybelt/apps/UB_docs/data/core.views.UserAddressList.json b/utilitybelt/apps/UB_docs/data/core.views.UserAddressList.json new file mode 100644 index 0000000..a5fd1c5 --- /dev/null +++ b/utilitybelt/apps/UB_docs/data/core.views.UserAddressList.json @@ -0,0 +1,106 @@ +[ + { + "tags": [ + { + "type": "param", + "types": [ + "Collection" + ], + "name": "collection", + "description": "Collection of core.models.Address \r" + }, + { + "type": "param", + "types": [ + "Function" + ], + "name": "openEditWidget", + "description": "Handler to open the widget for model editing" + } + ], + "description": { + "full": "

    View with the list of User Addresses

    \n\n

    Example

    \n\n
    var list = new core.views.UserAddressList();\nlist.renderTo($('#some_element'));\n
    ", + "summary": "

    View with the list of User Addresses

    \n\n

    Example

    \n\n
    var list = new core.views.UserAddressList();\nlist.renderTo($('#some_element'));\n
    ", + "body": "" + }, + "isPrivate": false, + "ignore": false, + "code": "core.define('core.views.UserAddressList', {\r\n\r\n extend: 'core.View',\r\n className: 'UserAddressList',\r\n template: '',\r\n// user_id: null,\r\n \r\n events: {\r\n \"click .edit-icon\": \"editAddress\",\r\n \"click .delete-icon\": \"deleteAddressConfirm\"\r\n }," + }, + { + "tags": [], + "description": { + "full": "

    Constructor, set collection and template

    ", + "summary": "

    Constructor, set collection and template

    ", + "body": "" + }, + "ignore": false, + "code": "initialize: function(){\r\n this.collection = this.options.collection || new core.collections.Address();\r\n this.collection.bindTo(this);" + }, + { + "tags": [], + "description": { + "full": "

    this.defaults = this.options.defaults || this.collection.defaults;

    \n\n
        this.user_id = this.options.user_id;\n    this.collection.user_id = this.user_id;\n
    ", + "summary": "

    this.defaults = this.options.defaults || this.collection.defaults;

    \n\n
        this.user_id = this.options.user_id;\n    this.collection.user_id = this.user_id;\n
    ", + "body": "" + }, + "ignore": false, + "code": "var me = this;\r\n core.utils.getTemplate(['UserAddress/UserAddresses.html'], function(tpl) {\r\n me.template = tpl;\r\n me.collection.fetch(); // should change url first...\r\n }); \r\n \r\n },", + "ctx": { + "type": "declaration", + "name": "me", + "value": "this", + "string": "me" + } + }, + { + "tags": [], + "description": { + "full": "

    Render widget using the collection and the template

    ", + "summary": "

    Render widget using the collection and the template

    ", + "body": "" + }, + "ignore": false, + "code": "render: function() {\r\n\r\n var collection = this.collection;\r\n\r\n if (collection.length == 0) {\r\n this.onUserListEmpty();\r\n }\r\n else {\r\n\r\n var col = collection.getRecords();\r\n\r\n var tpl = _.template(this.template);\r\n var data = { addresses: col };\r\n data.get_full_address = function(address) { // TODO: create set of global renderers and process them in generic way\r\n return address.city + ' ' + address.street_name + ' ' + address.street_number;\r\n }\r\n\r\n this.$el.empty().append(tpl(data));\r\n\r\n }\r\n\r\n core.views.UserAddressList.__super__.render.call(this);\r\n\r\n }," + }, + { + "tags": [], + "description": { + "full": "

    Run when addresses collection is empty

    ", + "summary": "

    Run when addresses collection is empty

    ", + "body": "" + }, + "ignore": false, + "code": "onUserListEmpty: function() {\r\n this.$el.empty().append(jsGetText(\"no_addresses\"));\r\n }," + }, + { + "tags": [], + "description": { + "full": "

    Run when click on \"edit user\" icon

    ", + "summary": "

    Run when click on \"edit user\" icon

    ", + "body": "" + }, + "ignore": false, + "code": "editAddress: function(e) { \r\n var id = e.target.id;\r\n var rec = this.collection.get(id);\r\n if (rec && rec.id) {\r\n this.options.openEditWidget({ record: rec, title: jsGetText('edit_address') });\r\n }\r\n }," + }, + { + "tags": [], + "description": { + "full": "

    Remove the address from collection and re-render the widget

    ", + "summary": "

    Remove the address from collection and re-render the widget

    ", + "body": "" + }, + "ignore": false, + "code": "deleteAddress: function(id) {\r\n var rec = this.collection.get(id);\r\n var me = this;\r\n rec.destroy({ success: function() {\r\n me.collection.remove();\r\n me.render();\r\n } });\r\n }," + }, + { + "tags": [], + "description": { + "full": "

    Run when click on \"delete user\" icon, show small inline \"confirmation\" dialog

    ", + "summary": "

    Run when click on \"delete user\" icon, show small inline \"confirmation\" dialog

    ", + "body": "" + }, + "ignore": false, + "code": "deleteAddressConfirm: function(e){\r\n var id = e.target.id, me = this;\r\n core.utils.getTemplate(['UserAddress/DeleteConfirm.html'], function(tpl) {\r\n var html = _.template(tpl);\r\n var ct = $(e.target).parents('li');\r\n var confirm_message = ct.hide().after(html).next();\r\n confirm_message.height(ct.height());\r\n $('.yes', confirm_message).click( function() { \r\n ct.show();\r\n confirm_message.hide();\r\n me.deleteAddress(id); \r\n } );\r\n $('.no', confirm_message).click( function() { \r\n ct.show();\r\n confirm_message.hide() \r\n } );\r\n }); \r\n },\r\n\r\n demo: function() {\r\n var list = new core.views.UserAddressList();\r\n return list; \r\n }\r\n\r\n});" + } +] \ No newline at end of file diff --git a/utilitybelt/apps/UB_docs/data/core.views.UserAddressListLightbox.json b/utilitybelt/apps/UB_docs/data/core.views.UserAddressListLightbox.json new file mode 100644 index 0000000..c1faf01 --- /dev/null +++ b/utilitybelt/apps/UB_docs/data/core.views.UserAddressListLightbox.json @@ -0,0 +1,46 @@ +[ + { + "tags": [ + { + "type": "extends", + "string": "core.views.Lightbox" + } + ], + "description": { + "full": "

    Lightbox with the User Addresses List inside

    \n\n

    Examples

    \n\n
    var lb = new core.views.UserAddressListLightbox();\nlb.show();\n
    ", + "summary": "

    Lightbox with the User Addresses List inside

    \n\n

    Examples

    \n\n
    var lb = new core.views.UserAddressListLightbox();\nlb.show();\n
    ", + "body": "" + }, + "isPrivate": false, + "ignore": false, + "code": "core.define('core.views.UserAddressListLightbox', {\r\n\r\n extend: 'core.views.Lightbox',\r\n\r\n className: 'UserAddressListLightbox'," + }, + { + "tags": [ + { + "type": "method", + "string": "initialize\r" + }, + { + "type": "constructor\r", + "string": "" + }, + { + "type": "param", + "types": [ + "Object" + ], + "name": "options", + "description": "Options" + } + ], + "description": { + "full": "

    Initialize

    ", + "summary": "

    Initialize

    ", + "body": "" + }, + "isPrivate": false, + "ignore": false, + "code": "initialize: function(options){\r\n\r\n this.title = jsGetText('address_list');\r\n \r\n this.on('render', function() { \r\n var lb = this;\r\n lb.addTitleLink(jsGetText('new_address')).addClass('new-address').click(function() {\r\n var form = new core.views.UserAddressLightbox({ title: jsGetText('new_address') });\r\n form.show();\r\n lb.hide();\r\n });\r\n this.options.openEditWidget = function(options) {\r\n var form = new core.views.UserAddressLightbox(options);\r\n form.show();\r\n lb.hide();\r\n };\r\n\r\n var list = this.addItem(core.views.UserAddressList, this.options);\r\n this.setAddressList(list);\r\n\r\n });\r\n\r\n core.views.UserAddressListLightbox.__super__.initialize.call(this);\r\n\r\n },\r\n\r\n setAddressList: function(list) {\r\n this.addressList = list;\r\n },\r\n\r\n getAddressList: function() {\r\n return this.addressList;\r\n },\r\n\r\n demo: function() {\r\n var $result = $('Click Me')\r\n .click( function() {\r\n var lb = new core.views.UserAddressListLightbox({ user_id: 1 });\r\n lb.show();\r\n });\r\n return $result;\r\n }\r\n\r\n});" + } +] \ No newline at end of file diff --git a/utilitybelt/apps/UB_docs/images/common/arrow_filter.png b/utilitybelt/apps/UB_docs/images/common/arrow_filter.png new file mode 100644 index 0000000..998e6b9 Binary files /dev/null and b/utilitybelt/apps/UB_docs/images/common/arrow_filter.png differ diff --git a/utilitybelt/apps/UB_docs/images/common/arrows_menu.png b/utilitybelt/apps/UB_docs/images/common/arrows_menu.png new file mode 100644 index 0000000..29fe949 Binary files /dev/null and b/utilitybelt/apps/UB_docs/images/common/arrows_menu.png differ diff --git a/utilitybelt/apps/UB_docs/images/common/back_other_pages.jpg b/utilitybelt/apps/UB_docs/images/common/back_other_pages.jpg new file mode 100644 index 0000000..be7170d Binary files /dev/null and b/utilitybelt/apps/UB_docs/images/common/back_other_pages.jpg differ diff --git a/utilitybelt/apps/UB_docs/images/common/back_pizza.jpg b/utilitybelt/apps/UB_docs/images/common/back_pizza.jpg new file mode 100644 index 0000000..2031538 Binary files /dev/null and b/utilitybelt/apps/UB_docs/images/common/back_pizza.jpg differ diff --git a/utilitybelt/apps/UB_docs/images/common/bar_brown.jpg b/utilitybelt/apps/UB_docs/images/common/bar_brown.jpg new file mode 100644 index 0000000..fed3033 Binary files /dev/null and b/utilitybelt/apps/UB_docs/images/common/bar_brown.jpg differ diff --git a/utilitybelt/apps/UB_docs/images/common/baseline-20.png b/utilitybelt/apps/UB_docs/images/common/baseline-20.png new file mode 100644 index 0000000..726e2d6 Binary files /dev/null and b/utilitybelt/apps/UB_docs/images/common/baseline-20.png differ diff --git a/utilitybelt/apps/UB_docs/images/common/big_logo.png b/utilitybelt/apps/UB_docs/images/common/big_logo.png new file mode 100644 index 0000000..b52b8a4 Binary files /dev/null and b/utilitybelt/apps/UB_docs/images/common/big_logo.png differ diff --git a/utilitybelt/apps/UB_docs/images/common/bloomsburys/STYLE-bl.png b/utilitybelt/apps/UB_docs/images/common/bloomsburys/STYLE-bl.png new file mode 100644 index 0000000..47c61f1 Binary files /dev/null and b/utilitybelt/apps/UB_docs/images/common/bloomsburys/STYLE-bl.png differ diff --git a/utilitybelt/apps/UB_docs/images/common/bloomsburys/STYLE-br.png b/utilitybelt/apps/UB_docs/images/common/bloomsburys/STYLE-br.png new file mode 100644 index 0000000..7111e7c Binary files /dev/null and b/utilitybelt/apps/UB_docs/images/common/bloomsburys/STYLE-br.png differ diff --git a/utilitybelt/apps/UB_docs/images/common/bloomsburys/STYLE-tl.png b/utilitybelt/apps/UB_docs/images/common/bloomsburys/STYLE-tl.png new file mode 100644 index 0000000..27e1b88 Binary files /dev/null and b/utilitybelt/apps/UB_docs/images/common/bloomsburys/STYLE-tl.png differ diff --git a/utilitybelt/apps/UB_docs/images/common/bloomsburys/STYLE-tr.png b/utilitybelt/apps/UB_docs/images/common/bloomsburys/STYLE-tr.png new file mode 100644 index 0000000..20c5df3 Binary files /dev/null and b/utilitybelt/apps/UB_docs/images/common/bloomsburys/STYLE-tr.png differ diff --git a/utilitybelt/apps/UB_docs/images/common/bloomsburys/big_logo.jpg b/utilitybelt/apps/UB_docs/images/common/bloomsburys/big_logo.jpg new file mode 100644 index 0000000..5575cad Binary files /dev/null and b/utilitybelt/apps/UB_docs/images/common/bloomsburys/big_logo.jpg differ diff --git a/utilitybelt/apps/UB_docs/images/common/bloomsburys/button_green_center.png b/utilitybelt/apps/UB_docs/images/common/bloomsburys/button_green_center.png new file mode 100644 index 0000000..d3c26f3 Binary files /dev/null and b/utilitybelt/apps/UB_docs/images/common/bloomsburys/button_green_center.png differ diff --git a/utilitybelt/apps/UB_docs/images/common/bloomsburys/button_green_left_right.png b/utilitybelt/apps/UB_docs/images/common/bloomsburys/button_green_left_right.png new file mode 100644 index 0000000..a2e5567 Binary files /dev/null and b/utilitybelt/apps/UB_docs/images/common/bloomsburys/button_green_left_right.png differ diff --git a/utilitybelt/apps/UB_docs/images/common/bloomsburys/button_vorbestellen_2.png b/utilitybelt/apps/UB_docs/images/common/bloomsburys/button_vorbestellen_2.png new file mode 100644 index 0000000..3ddcf0a Binary files /dev/null and b/utilitybelt/apps/UB_docs/images/common/bloomsburys/button_vorbestellen_2.png differ diff --git a/utilitybelt/apps/UB_docs/images/common/bloomsburys/button_zummenue.png b/utilitybelt/apps/UB_docs/images/common/bloomsburys/button_zummenue.png new file mode 100644 index 0000000..daa60ef Binary files /dev/null and b/utilitybelt/apps/UB_docs/images/common/bloomsburys/button_zummenue.png differ diff --git a/utilitybelt/apps/UB_docs/images/common/bloomsburys/cart_delete.png b/utilitybelt/apps/UB_docs/images/common/bloomsburys/cart_delete.png new file mode 100644 index 0000000..c96b6ec Binary files /dev/null and b/utilitybelt/apps/UB_docs/images/common/bloomsburys/cart_delete.png differ diff --git a/utilitybelt/apps/UB_docs/images/common/bloomsburys/cucine.png b/utilitybelt/apps/UB_docs/images/common/bloomsburys/cucine.png new file mode 100644 index 0000000..ba278e2 Binary files /dev/null and b/utilitybelt/apps/UB_docs/images/common/bloomsburys/cucine.png differ diff --git a/utilitybelt/apps/UB_docs/images/common/bloomsburys/hat.png b/utilitybelt/apps/UB_docs/images/common/bloomsburys/hat.png new file mode 100644 index 0000000..7704cc1 Binary files /dev/null and b/utilitybelt/apps/UB_docs/images/common/bloomsburys/hat.png differ diff --git a/utilitybelt/apps/UB_docs/images/common/bloomsburys/logo.gif b/utilitybelt/apps/UB_docs/images/common/bloomsburys/logo.gif new file mode 100644 index 0000000..fd8dea1 Binary files /dev/null and b/utilitybelt/apps/UB_docs/images/common/bloomsburys/logo.gif differ diff --git a/utilitybelt/apps/UB_docs/images/common/bloomsburys/logo_blooms.png b/utilitybelt/apps/UB_docs/images/common/bloomsburys/logo_blooms.png new file mode 100644 index 0000000..4b0618b Binary files /dev/null and b/utilitybelt/apps/UB_docs/images/common/bloomsburys/logo_blooms.png differ diff --git a/utilitybelt/apps/UB_docs/images/common/bloomsburys/logo_widget.png b/utilitybelt/apps/UB_docs/images/common/bloomsburys/logo_widget.png new file mode 100644 index 0000000..2429b4a Binary files /dev/null and b/utilitybelt/apps/UB_docs/images/common/bloomsburys/logo_widget.png differ diff --git a/utilitybelt/apps/UB_docs/images/common/bloomsburys/menu_arrow_2.png b/utilitybelt/apps/UB_docs/images/common/bloomsburys/menu_arrow_2.png new file mode 100644 index 0000000..b1599de Binary files /dev/null and b/utilitybelt/apps/UB_docs/images/common/bloomsburys/menu_arrow_2.png differ diff --git a/utilitybelt/apps/UB_docs/images/common/bloomsburys/pattern.png b/utilitybelt/apps/UB_docs/images/common/bloomsburys/pattern.png new file mode 100644 index 0000000..6e61827 Binary files /dev/null and b/utilitybelt/apps/UB_docs/images/common/bloomsburys/pattern.png differ diff --git a/utilitybelt/apps/UB_docs/images/common/bloomsburys/tab_active.png b/utilitybelt/apps/UB_docs/images/common/bloomsburys/tab_active.png new file mode 100644 index 0000000..7cdeab2 Binary files /dev/null and b/utilitybelt/apps/UB_docs/images/common/bloomsburys/tab_active.png differ diff --git a/utilitybelt/apps/UB_docs/images/common/bloomsburys/top_tooltip_description.png b/utilitybelt/apps/UB_docs/images/common/bloomsburys/top_tooltip_description.png new file mode 100644 index 0000000..70f9fe6 Binary files /dev/null and b/utilitybelt/apps/UB_docs/images/common/bloomsburys/top_tooltip_description.png differ diff --git a/utilitybelt/apps/UB_docs/images/common/brown_pattern.png b/utilitybelt/apps/UB_docs/images/common/brown_pattern.png new file mode 100644 index 0000000..5fe3be2 Binary files /dev/null and b/utilitybelt/apps/UB_docs/images/common/brown_pattern.png differ diff --git a/utilitybelt/apps/UB_docs/images/common/coup_yes.png b/utilitybelt/apps/UB_docs/images/common/coup_yes.png new file mode 100644 index 0000000..c21dda9 Binary files /dev/null and b/utilitybelt/apps/UB_docs/images/common/coup_yes.png differ diff --git a/utilitybelt/apps/UB_docs/images/common/ekomi_rating.png b/utilitybelt/apps/UB_docs/images/common/ekomi_rating.png new file mode 100644 index 0000000..98fba96 Binary files /dev/null and b/utilitybelt/apps/UB_docs/images/common/ekomi_rating.png differ diff --git a/utilitybelt/apps/UB_docs/images/common/express_icon.png b/utilitybelt/apps/UB_docs/images/common/express_icon.png new file mode 100644 index 0000000..ea5c931 Binary files /dev/null and b/utilitybelt/apps/UB_docs/images/common/express_icon.png differ diff --git a/utilitybelt/apps/UB_docs/images/common/facebook-plugin.png b/utilitybelt/apps/UB_docs/images/common/facebook-plugin.png new file mode 100644 index 0000000..4052e1b Binary files /dev/null and b/utilitybelt/apps/UB_docs/images/common/facebook-plugin.png differ diff --git a/utilitybelt/apps/UB_docs/images/common/facebook_plugin_big.png b/utilitybelt/apps/UB_docs/images/common/facebook_plugin_big.png new file mode 100644 index 0000000..d1b3bac Binary files /dev/null and b/utilitybelt/apps/UB_docs/images/common/facebook_plugin_big.png differ diff --git a/utilitybelt/apps/UB_docs/images/common/gastrologic/button_zummenue.png b/utilitybelt/apps/UB_docs/images/common/gastrologic/button_zummenue.png new file mode 100644 index 0000000..cdf9a04 Binary files /dev/null and b/utilitybelt/apps/UB_docs/images/common/gastrologic/button_zummenue.png differ diff --git a/utilitybelt/apps/UB_docs/images/common/gastrologic/pattern.gif b/utilitybelt/apps/UB_docs/images/common/gastrologic/pattern.gif new file mode 100644 index 0000000..cec74ac Binary files /dev/null and b/utilitybelt/apps/UB_docs/images/common/gastrologic/pattern.gif differ diff --git a/utilitybelt/apps/UB_docs/images/common/gastrologic/taxi_icon.jpg b/utilitybelt/apps/UB_docs/images/common/gastrologic/taxi_icon.jpg new file mode 100644 index 0000000..713c57f Binary files /dev/null and b/utilitybelt/apps/UB_docs/images/common/gastrologic/taxi_icon.jpg differ diff --git a/utilitybelt/apps/UB_docs/images/common/gastrologic_pattern.gif b/utilitybelt/apps/UB_docs/images/common/gastrologic_pattern.gif new file mode 100644 index 0000000..cec74ac Binary files /dev/null and b/utilitybelt/apps/UB_docs/images/common/gastrologic_pattern.gif differ diff --git a/utilitybelt/apps/UB_docs/images/common/goldPattern.png b/utilitybelt/apps/UB_docs/images/common/goldPattern.png new file mode 100644 index 0000000..5700953 Binary files /dev/null and b/utilitybelt/apps/UB_docs/images/common/goldPattern.png differ diff --git a/utilitybelt/apps/UB_docs/images/common/gplus-plugin.png b/utilitybelt/apps/UB_docs/images/common/gplus-plugin.png new file mode 100644 index 0000000..227166e Binary files /dev/null and b/utilitybelt/apps/UB_docs/images/common/gplus-plugin.png differ diff --git a/utilitybelt/apps/UB_docs/images/common/icon_cuisine.png b/utilitybelt/apps/UB_docs/images/common/icon_cuisine.png new file mode 100644 index 0000000..5bac930 Binary files /dev/null and b/utilitybelt/apps/UB_docs/images/common/icon_cuisine.png differ diff --git a/utilitybelt/apps/UB_docs/images/common/icon_time_green.png b/utilitybelt/apps/UB_docs/images/common/icon_time_green.png new file mode 100644 index 0000000..7e62490 Binary files /dev/null and b/utilitybelt/apps/UB_docs/images/common/icon_time_green.png differ diff --git a/utilitybelt/apps/UB_docs/images/common/icons.png b/utilitybelt/apps/UB_docs/images/common/icons.png new file mode 100644 index 0000000..4350324 Binary files /dev/null and b/utilitybelt/apps/UB_docs/images/common/icons.png differ diff --git a/utilitybelt/apps/UB_docs/images/common/logo.png b/utilitybelt/apps/UB_docs/images/common/logo.png new file mode 100644 index 0000000..1d8f4d5 Binary files /dev/null and b/utilitybelt/apps/UB_docs/images/common/logo.png differ diff --git a/utilitybelt/apps/UB_docs/images/common/logo125.png b/utilitybelt/apps/UB_docs/images/common/logo125.png new file mode 100644 index 0000000..c6d6534 Binary files /dev/null and b/utilitybelt/apps/UB_docs/images/common/logo125.png differ diff --git a/utilitybelt/apps/UB_docs/images/common/logo57x57.png b/utilitybelt/apps/UB_docs/images/common/logo57x57.png new file mode 100644 index 0000000..06a0e33 Binary files /dev/null and b/utilitybelt/apps/UB_docs/images/common/logo57x57.png differ diff --git a/utilitybelt/apps/UB_docs/images/common/logo70.png b/utilitybelt/apps/UB_docs/images/common/logo70.png new file mode 100644 index 0000000..30d3ca4 Binary files /dev/null and b/utilitybelt/apps/UB_docs/images/common/logo70.png differ diff --git a/utilitybelt/apps/UB_docs/images/common/logo_small.png b/utilitybelt/apps/UB_docs/images/common/logo_small.png new file mode 100644 index 0000000..db721d6 Binary files /dev/null and b/utilitybelt/apps/UB_docs/images/common/logo_small.png differ diff --git a/utilitybelt/apps/UB_docs/images/common/pattern-yellow.png b/utilitybelt/apps/UB_docs/images/common/pattern-yellow.png new file mode 100644 index 0000000..ad181cd Binary files /dev/null and b/utilitybelt/apps/UB_docs/images/common/pattern-yellow.png differ diff --git a/utilitybelt/apps/UB_docs/images/common/pattern.png b/utilitybelt/apps/UB_docs/images/common/pattern.png new file mode 100644 index 0000000..4adcfa3 Binary files /dev/null and b/utilitybelt/apps/UB_docs/images/common/pattern.png differ diff --git a/utilitybelt/apps/UB_docs/images/common/pattern_blooms.png b/utilitybelt/apps/UB_docs/images/common/pattern_blooms.png new file mode 100644 index 0000000..6e61827 Binary files /dev/null and b/utilitybelt/apps/UB_docs/images/common/pattern_blooms.png differ diff --git a/utilitybelt/apps/UB_docs/images/common/payment_icons.png b/utilitybelt/apps/UB_docs/images/common/payment_icons.png new file mode 100644 index 0000000..7ab26f3 Binary files /dev/null and b/utilitybelt/apps/UB_docs/images/common/payment_icons.png differ diff --git a/utilitybelt/apps/UB_docs/images/common/restaurantlist_logoborder_hover.png b/utilitybelt/apps/UB_docs/images/common/restaurantlist_logoborder_hover.png new file mode 100644 index 0000000..21891df Binary files /dev/null and b/utilitybelt/apps/UB_docs/images/common/restaurantlist_logoborder_hover.png differ diff --git a/utilitybelt/apps/UB_docs/images/common/restaurantlist_logoborder_sprite.png b/utilitybelt/apps/UB_docs/images/common/restaurantlist_logoborder_sprite.png new file mode 100644 index 0000000..e69f3c1 Binary files /dev/null and b/utilitybelt/apps/UB_docs/images/common/restaurantlist_logoborder_sprite.png differ diff --git a/utilitybelt/apps/UB_docs/images/common/search_lupe_klein.png b/utilitybelt/apps/UB_docs/images/common/search_lupe_klein.png new file mode 100644 index 0000000..e096d96 Binary files /dev/null and b/utilitybelt/apps/UB_docs/images/common/search_lupe_klein.png differ diff --git a/utilitybelt/apps/UB_docs/images/common/sprite_nav.png b/utilitybelt/apps/UB_docs/images/common/sprite_nav.png new file mode 100644 index 0000000..b53a7f3 Binary files /dev/null and b/utilitybelt/apps/UB_docs/images/common/sprite_nav.png differ diff --git a/utilitybelt/apps/UB_docs/images/common/stars_empty-ekomi-big.png b/utilitybelt/apps/UB_docs/images/common/stars_empty-ekomi-big.png new file mode 100644 index 0000000..29b7c0e Binary files /dev/null and b/utilitybelt/apps/UB_docs/images/common/stars_empty-ekomi-big.png differ diff --git a/utilitybelt/apps/UB_docs/images/common/temp_restaurantlogo.jpeg b/utilitybelt/apps/UB_docs/images/common/temp_restaurantlogo.jpeg new file mode 100644 index 0000000..94cb68e Binary files /dev/null and b/utilitybelt/apps/UB_docs/images/common/temp_restaurantlogo.jpeg differ diff --git a/utilitybelt/apps/UB_docs/images/common/testimonials_closing_quotation.png b/utilitybelt/apps/UB_docs/images/common/testimonials_closing_quotation.png new file mode 100644 index 0000000..c8f176b Binary files /dev/null and b/utilitybelt/apps/UB_docs/images/common/testimonials_closing_quotation.png differ diff --git a/utilitybelt/apps/UB_docs/images/common/testimonials_opening_quotation.png b/utilitybelt/apps/UB_docs/images/common/testimonials_opening_quotation.png new file mode 100644 index 0000000..f607777 Binary files /dev/null and b/utilitybelt/apps/UB_docs/images/common/testimonials_opening_quotation.png differ diff --git a/utilitybelt/apps/UB_docs/images/jqtransform/button_checkbox.png b/utilitybelt/apps/UB_docs/images/jqtransform/button_checkbox.png new file mode 100644 index 0000000..380fd52 Binary files /dev/null and b/utilitybelt/apps/UB_docs/images/jqtransform/button_checkbox.png differ diff --git a/utilitybelt/apps/UB_docs/images/jqtransform/button_radio.png b/utilitybelt/apps/UB_docs/images/jqtransform/button_radio.png new file mode 100644 index 0000000..8b6d3b1 Binary files /dev/null and b/utilitybelt/apps/UB_docs/images/jqtransform/button_radio.png differ diff --git a/utilitybelt/apps/UB_docs/images/jqtransform/cart_delete.png b/utilitybelt/apps/UB_docs/images/jqtransform/cart_delete.png new file mode 100644 index 0000000..597b0fc Binary files /dev/null and b/utilitybelt/apps/UB_docs/images/jqtransform/cart_delete.png differ diff --git a/utilitybelt/apps/UB_docs/images/jqtransform/cart_plus_minus_bg.png b/utilitybelt/apps/UB_docs/images/jqtransform/cart_plus_minus_bg.png new file mode 100644 index 0000000..511805a Binary files /dev/null and b/utilitybelt/apps/UB_docs/images/jqtransform/cart_plus_minus_bg.png differ diff --git a/utilitybelt/apps/UB_docs/images/jqtransform/select_left.png b/utilitybelt/apps/UB_docs/images/jqtransform/select_left.png new file mode 100644 index 0000000..6448beb Binary files /dev/null and b/utilitybelt/apps/UB_docs/images/jqtransform/select_left.png differ diff --git a/utilitybelt/apps/UB_docs/images/jqtransform/select_right.png b/utilitybelt/apps/UB_docs/images/jqtransform/select_right.png new file mode 100644 index 0000000..64731aa Binary files /dev/null and b/utilitybelt/apps/UB_docs/images/jqtransform/select_right.png differ diff --git a/utilitybelt/apps/UB_docs/images/widgets/hero_animation/background_slide.png b/utilitybelt/apps/UB_docs/images/widgets/hero_animation/background_slide.png new file mode 100644 index 0000000..c6b467c Binary files /dev/null and b/utilitybelt/apps/UB_docs/images/widgets/hero_animation/background_slide.png differ diff --git a/utilitybelt/apps/UB_docs/images/widgets/hero_animation/hero.png b/utilitybelt/apps/UB_docs/images/widgets/hero_animation/hero.png new file mode 100644 index 0000000..91afa88 Binary files /dev/null and b/utilitybelt/apps/UB_docs/images/widgets/hero_animation/hero.png differ diff --git a/utilitybelt/apps/UB_docs/images/widgets/hero_animation/speech_1.png b/utilitybelt/apps/UB_docs/images/widgets/hero_animation/speech_1.png new file mode 100644 index 0000000..05895e8 Binary files /dev/null and b/utilitybelt/apps/UB_docs/images/widgets/hero_animation/speech_1.png differ diff --git a/utilitybelt/apps/UB_docs/images/widgets/hero_animation/speech_2_1.png b/utilitybelt/apps/UB_docs/images/widgets/hero_animation/speech_2_1.png new file mode 100644 index 0000000..21ba601 Binary files /dev/null and b/utilitybelt/apps/UB_docs/images/widgets/hero_animation/speech_2_1.png differ diff --git a/utilitybelt/apps/UB_docs/images/widgets/hero_animation/speech_2_2.png b/utilitybelt/apps/UB_docs/images/widgets/hero_animation/speech_2_2.png new file mode 100644 index 0000000..21aecd8 Binary files /dev/null and b/utilitybelt/apps/UB_docs/images/widgets/hero_animation/speech_2_2.png differ diff --git a/utilitybelt/apps/UB_docs/images/widgets/hero_animation/speech_3_1.png b/utilitybelt/apps/UB_docs/images/widgets/hero_animation/speech_3_1.png new file mode 100644 index 0000000..ad701ec Binary files /dev/null and b/utilitybelt/apps/UB_docs/images/widgets/hero_animation/speech_3_1.png differ diff --git a/utilitybelt/apps/UB_docs/images/widgets/hero_animation/speech_3_2.png b/utilitybelt/apps/UB_docs/images/widgets/hero_animation/speech_3_2.png new file mode 100644 index 0000000..764e55c Binary files /dev/null and b/utilitybelt/apps/UB_docs/images/widgets/hero_animation/speech_3_2.png differ diff --git a/utilitybelt/apps/UB_docs/images/widgets/hero_order/1_order_brown.png b/utilitybelt/apps/UB_docs/images/widgets/hero_order/1_order_brown.png new file mode 100644 index 0000000..5cbe9d7 Binary files /dev/null and b/utilitybelt/apps/UB_docs/images/widgets/hero_order/1_order_brown.png differ diff --git a/utilitybelt/apps/UB_docs/images/widgets/hero_order/2_order_grey.png b/utilitybelt/apps/UB_docs/images/widgets/hero_order/2_order_grey.png new file mode 100644 index 0000000..a3cf758 Binary files /dev/null and b/utilitybelt/apps/UB_docs/images/widgets/hero_order/2_order_grey.png differ diff --git a/utilitybelt/apps/UB_docs/images/widgets/hero_order/3_order_grey.png b/utilitybelt/apps/UB_docs/images/widgets/hero_order/3_order_grey.png new file mode 100644 index 0000000..7afbff5 Binary files /dev/null and b/utilitybelt/apps/UB_docs/images/widgets/hero_order/3_order_grey.png differ diff --git a/utilitybelt/apps/UB_docs/images/widgets/hero_order/baloon_order.png b/utilitybelt/apps/UB_docs/images/widgets/hero_order/baloon_order.png new file mode 100644 index 0000000..6bcc20c Binary files /dev/null and b/utilitybelt/apps/UB_docs/images/widgets/hero_order/baloon_order.png differ diff --git a/utilitybelt/apps/UB_docs/images/widgets/hero_order/hero_no_order.png b/utilitybelt/apps/UB_docs/images/widgets/hero_order/hero_no_order.png new file mode 100644 index 0000000..3bb4cfe Binary files /dev/null and b/utilitybelt/apps/UB_docs/images/widgets/hero_order/hero_no_order.png differ diff --git a/utilitybelt/apps/UB_docs/images/widgets/hero_order/hero_wait_order.png b/utilitybelt/apps/UB_docs/images/widgets/hero_order/hero_wait_order.png new file mode 100644 index 0000000..bb00bb9 Binary files /dev/null and b/utilitybelt/apps/UB_docs/images/widgets/hero_order/hero_wait_order.png differ diff --git a/utilitybelt/apps/UB_docs/images/widgets/hero_order/hero_yes_order.png b/utilitybelt/apps/UB_docs/images/widgets/hero_order/hero_yes_order.png new file mode 100644 index 0000000..80509b9 Binary files /dev/null and b/utilitybelt/apps/UB_docs/images/widgets/hero_order/hero_yes_order.png differ diff --git a/utilitybelt/apps/UB_docs/images/widgets/hero_order/ok_icon_order.png b/utilitybelt/apps/UB_docs/images/widgets/hero_order/ok_icon_order.png new file mode 100644 index 0000000..af0bba0 Binary files /dev/null and b/utilitybelt/apps/UB_docs/images/widgets/hero_order/ok_icon_order.png differ diff --git a/utilitybelt/apps/UB_docs/images/widgets/input_lc_plz.png b/utilitybelt/apps/UB_docs/images/widgets/input_lc_plz.png new file mode 100644 index 0000000..e98b2f6 Binary files /dev/null and b/utilitybelt/apps/UB_docs/images/widgets/input_lc_plz.png differ diff --git a/utilitybelt/apps/UB_docs/images/widgets/lightbox_border.png b/utilitybelt/apps/UB_docs/images/widgets/lightbox_border.png new file mode 100644 index 0000000..0d0a764 Binary files /dev/null and b/utilitybelt/apps/UB_docs/images/widgets/lightbox_border.png differ diff --git a/utilitybelt/apps/UB_docs/images/widgets/spinner.gif b/utilitybelt/apps/UB_docs/images/widgets/spinner.gif new file mode 100644 index 0000000..e158443 Binary files /dev/null and b/utilitybelt/apps/UB_docs/images/widgets/spinner.gif differ diff --git a/utilitybelt/apps/UB_docs/images/widgets/tooltips/body_address.png b/utilitybelt/apps/UB_docs/images/widgets/tooltips/body_address.png new file mode 100644 index 0000000..ce2276d Binary files /dev/null and b/utilitybelt/apps/UB_docs/images/widgets/tooltips/body_address.png differ diff --git a/utilitybelt/apps/UB_docs/images/widgets/tooltips/ekomi-tooltip-body.png b/utilitybelt/apps/UB_docs/images/widgets/tooltips/ekomi-tooltip-body.png new file mode 100644 index 0000000..fee74fe Binary files /dev/null and b/utilitybelt/apps/UB_docs/images/widgets/tooltips/ekomi-tooltip-body.png differ diff --git a/utilitybelt/apps/UB_docs/images/widgets/tooltips/lieferheld_express.jpg b/utilitybelt/apps/UB_docs/images/widgets/tooltips/lieferheld_express.jpg new file mode 100644 index 0000000..a1ce89c Binary files /dev/null and b/utilitybelt/apps/UB_docs/images/widgets/tooltips/lieferheld_express.jpg differ diff --git a/utilitybelt/apps/UB_docs/images/widgets/tooltips/middle-box-express.png b/utilitybelt/apps/UB_docs/images/widgets/tooltips/middle-box-express.png new file mode 100644 index 0000000..9deeda1 Binary files /dev/null and b/utilitybelt/apps/UB_docs/images/widgets/tooltips/middle-box-express.png differ diff --git a/utilitybelt/apps/UB_docs/images/widgets/tooltips/stars_full-ekomi-big.png b/utilitybelt/apps/UB_docs/images/widgets/tooltips/stars_full-ekomi-big.png new file mode 100644 index 0000000..5ac84d8 Binary files /dev/null and b/utilitybelt/apps/UB_docs/images/widgets/tooltips/stars_full-ekomi-big.png differ diff --git a/utilitybelt/apps/UB_docs/images/widgets/tooltips/stars_full-ekomi.png b/utilitybelt/apps/UB_docs/images/widgets/tooltips/stars_full-ekomi.png new file mode 100644 index 0000000..f935867 Binary files /dev/null and b/utilitybelt/apps/UB_docs/images/widgets/tooltips/stars_full-ekomi.png differ diff --git a/utilitybelt/apps/UB_docs/images/widgets/tooltips/tooltips.png b/utilitybelt/apps/UB_docs/images/widgets/tooltips/tooltips.png new file mode 100644 index 0000000..888fe1b Binary files /dev/null and b/utilitybelt/apps/UB_docs/images/widgets/tooltips/tooltips.png differ diff --git a/utilitybelt/apps/UB_docs/images/widgets/wrong_icon_order.png b/utilitybelt/apps/UB_docs/images/widgets/wrong_icon_order.png new file mode 100644 index 0000000..d7a2297 Binary files /dev/null and b/utilitybelt/apps/UB_docs/images/widgets/wrong_icon_order.png differ diff --git a/utilitybelt/apps/UB_docs/index.html b/utilitybelt/apps/UB_docs/index.html new file mode 100644 index 0000000..0010b77 --- /dev/null +++ b/utilitybelt/apps/UB_docs/index.html @@ -0,0 +1,106 @@ + + + + + Js documentation + + + + + + +
    + +
    + +
    +
    +
    +

    Widgets

    +
    + +
    +
    +
    +

    + +
    +
    +
    + Here the API +
    + +
    +
    +
    + + \ No newline at end of file diff --git a/utilitybelt/apps/UB_docs/namespaces.js b/utilitybelt/apps/UB_docs/namespaces.js new file mode 100644 index 0000000..13818c1 --- /dev/null +++ b/utilitybelt/apps/UB_docs/namespaces.js @@ -0,0 +1,4 @@ +/** Example application namespaces */ + +var docs = {}; +docs.messages = {}; diff --git a/utilitybelt/apps/UB_docs/pages/Page.js b/utilitybelt/apps/UB_docs/pages/Page.js new file mode 100644 index 0000000..a329c8c --- /dev/null +++ b/utilitybelt/apps/UB_docs/pages/Page.js @@ -0,0 +1,184 @@ + +/** Basic application page class, contains features which should be on every page */ + +core.define('docs.Page', { + + extend: 'core.Page', + + initialize: function() { + + var me = this, url = me.options.url, tab = me.options.tab; + me.active_tab = tab; + + $(document).ready(function() { + + me.initLS(); + + // var doc_obj = core.Class.instantiate(url); + me.setDocs(url); + + $.ajax({ + url: 'data/Classes.json?d=' + new Date().getTime(), + dataType: 'json', + success: function(data) { + me.buildMenu(data, url); + // $( "#accordion" ).accordion(); + // var processed = me.processData(data, url); + // var processed_classes = processed.classes; + // var current_class = processed.current; + // me.setTitle(current_class); + // $('body *').jqTransform( { imgPath:'images/jqtransform/' } ); + } + }); + + $(".tabs").tabs({ select: function(event, ui) { /* + var tab_id = ui.panel.id, route = url + '/' + tab_id; + core.app_router.navigate(route); */ + } })/*.tabs('select')*/; + + /* Init search */ + // var classes = [ 'core.views.Lightbox', 'core.views.UserAddressListLightbox', 'core.views.UserAddressLightbox' ]; + // $( ".bar_search input" ).autocomplete({ + // source: classes + // }); + }); + + + // this.setTitle('title (' + doc_obj.self + ')'); + // this.setDocs('data/' + url + '.json', url); + + // me.setDocs('data/Lightbox.json', 'core.views.Lightbox'); + + // $('#open-widget').unbind().click( function() { + // var lb = new core.views.Lightbox({ title: 'Some awesome title', content: 'Awesomest content' }); + // lb.show(); + // }); + }, + + processData: function(data, url) { + for (var i=0,ii=data.length; i' + data[i].category + '
      ').appendTo(accordion).find('ul'); + if (data[i].items) { + var its = data[i].items; + for (var j=0,jj=its.length; j(' + its[j].className + ')'); + css_class = 'active'; + } + var $ln = $('
    • ' + ( its[j].title || its[j].className ) +'
    • '); + $ln.attr('className', its[j].className); + $ln.appendTo(ct).click(function() { + var className = $(this).attr('className'); + core.app_router.navigate(className/* + '/' + me.active_tab*/, { trigger: true }); + }); + } + } + } + }, + + initLS: function() { + + /* Prepopulate LS with some mock-up data */ + + if (localStorage.getItem('ub-docs-data11') !== 'true') { + localStorage.clear(); + var data = [ + { id: 1, "user_id": 1, "email": "my@email.com", "city_slug": "berlin", "city": "Berlin", "door": "", "etage": "", "street_number": "60", "lastname": "The last name", "street_name": "Mohrenstrasse", "zipcode": "10117", "comments": "please, call the ....", "phone": "01717171717", "longitude": 37.77234, "country": "DE", "latitude": 45.232340000000001, "name": "The name"}, + { id: 2, user_id: 1, email: 'some@dot.com' ,city_slug: "berlin", door: "", etage: "", lastname: "The last name", street_name: "Greifswalder Straße", phone: "01717171717", comments: "please, don not call the ....", city: "Berlin", name: "The name", street_number: "164", country: "DE", zipcode: "10409", longitude: 37.77231 , latitude: 45.23236 } + ]; + for (var i=0,ii=data.length; iExtend: {{ extend }}

      ' + + '

      API

      ' + + '{% _.each(methods, function(method) { %}' + + '

      {{ method.method.trim() }}(' + + '{% _.each(method.params, function(param) { %}' + + '{{ param.types[0] }} {{ param.name }}' + + '{% }); %}' + + ') {% if (method.returns) { %}: {{ method.returns }}{% } %}

      ' + + '

      {{ method.description }}

      ' + + '{% }); %}'; + if (!methods.length) { + tpl += '

      See {{ extend }}

      '; + } + + $('#documentation').html(_.template(tpl)({ methods: methods, klass: klass, extend: extend })); + } + }); + } +}); diff --git a/utilitybelt/apps/example/config.js b/utilitybelt/apps/example/config.js new file mode 100644 index 0000000..4d2eda2 --- /dev/null +++ b/utilitybelt/apps/example/config.js @@ -0,0 +1,33 @@ + +/* + * Sample config file + * We use RequireJS as a module loader http://requirejs.org/docs/api.html + */ + +require.config({ + baseUrl: '/utilitybelt' +}); + +require([ + +/* Include Web UI library */ + + 'webui/config' + , 'tests/namespaces' +], function(webui) { + require([ + +/* Inlcude application files */ + + 'order!apps/example/pages/Page' + , 'order!apps/example/pages/Home' + + ], function() { + +/* Call router, which will create the instance of the relevant page */ + webui.Router('example.Home'); + + }) +}); + +var APP_LANGUAGE = 'de'; diff --git a/utilitybelt/apps/example/namespaces.js b/utilitybelt/apps/example/namespaces.js new file mode 100644 index 0000000..a1d9aad --- /dev/null +++ b/utilitybelt/apps/example/namespaces.js @@ -0,0 +1,4 @@ +/** Example application namespaces */ + +var example = {}; + diff --git a/utilitybelt/apps/example/pages/Home.js b/utilitybelt/apps/example/pages/Home.js new file mode 100644 index 0000000..85e9212 --- /dev/null +++ b/utilitybelt/apps/example/pages/Home.js @@ -0,0 +1,17 @@ + +/** Home page */ + +example.Home = example.Page.extend({ + + initialize: function() { + + example.Home.__super__.initialize.call(this); + + $(document).ready(function() { + + console.log('Home page ready'); + + }); + + } +}); \ No newline at end of file diff --git a/utilitybelt/apps/example/pages/Page.js b/utilitybelt/apps/example/pages/Page.js new file mode 100644 index 0000000..5053b7a --- /dev/null +++ b/utilitybelt/apps/example/pages/Page.js @@ -0,0 +1,12 @@ + +/** Basic application page class, contains features which should be on every page */ + +example.Page = webui.Page.extend({ + + initialize: function() { + + console.log('Common page ready'); + + } + +}); diff --git a/utilitybelt/bootstrap.js b/utilitybelt/bootstrap.js new file mode 100644 index 0000000..0862a9a --- /dev/null +++ b/utilitybelt/bootstrap.js @@ -0,0 +1,212 @@ + +/** utilitybelt.js bootstrap script + * Example usage: + * + * + * Configuragion options: + * @param {String} path Path to utilitybelt.js + * @param {String} mode 'DEV' or 'PROD', to swicth between development or production modes + * @param {Array} html5shims List of libraries enabling HTML5 features on old browsers + * @param {String} requirejs_path Path to RequireJs library (used as a UB widgets packager) + * @param {String} requirejs_mode If set to 'auto', requirejs will be included on set-up, and not when the widget is added + * @param {String} jquery_path Path to jQuery + * @param {String} jqueryui_path Path to jQueryUI + * @param {String} development_config Path to requirejs config used in development + * @param {String} production_bundle Path to single minified js bundle used in production + */ + +var ub_loader = function() { + + return { + + /** Public configuration */ + conf: { + 'paths': { + 'global': '/static/utilitybelt/utilitybelt/', + 'requirejs': 'lib/require/require.js', + 'jquery': 'lib/jquery/1.7.1/jquery.min.js', + 'jqueryui': 'lib/jqueryui/1.8.17/jquery-ui.min.js' + }, + 'html5shims': [ /*'lib/html5shiv/html5shiv.js'*/ 'lib/modernizr/2.6.2/modernizr.js', 'lib/selectivizr/1.0.2/selectivizr.js' ], + 'development_config': 'apps/Frontend_AU/config.js', + 'production_bundle': 'main-built' + }, + + /** UB widgets registry */ + widgets_registry: [], + + /** Libraries registry */ + libs_registry: {}, + + /** Setup config and enable shims */ + setup: function(conf) { + for (var field in conf) { + this.conf[field] = conf[field]; + } + if (this.conf.requirejs_mode == 'auto') { + this.add_requirejs(); + } + if (isOld()) { + this.inject_js(this.conf.html5shims); + } + }, + + /** Add library's '; + document.write(script_str); + this.libs_registry[url.src] = url; + } + } + } + } + + /** + * Wait until the test condition is true or a timeout occurs. Useful for waiting + * on a server response or for a ui change (fadeIn, etc.) to occur. + * + * @param check javascript condition that evaluates to a boolean. + * @param onTestPass what to do when 'check' condition is fulfilled. + * @param onTimeout what to do when 'check' condition is not fulfilled and 'timeoutMs' has passed + * @param timeoutMs the max amount of time to wait. Default value is 3 seconds + * @param freqMs how frequently to repeat 'check'. Default value is 250 milliseconds + */ + function waitFor(check, onTestPass, onTimeout, timeoutMs, freqMs) { + var timeoutMs = timeoutMs || 6000, + freqMs = freqMs || 100, + start = Date.now(), + condition = false, + timer = setTimeout(function() { + var elapsedMs = Date.now() - start; + if ((elapsedMs < timeoutMs) && !condition) { + condition = check(elapsedMs); + timer = setTimeout(arguments.callee, freqMs); + } else { + clearTimeout(timer); + if (!condition) { + onTimeout(elapsedMs); + } else { + onTestPass(elapsedMs); + } + } + }, freqMs); + } + + /** + * @return True if browser does not support major HTML5 and CSS3 features and needs shims + */ + function isOld() { // IE8 or worse + var docMode = document.documentMode; + return (/msie [\w.]+/.exec(navigator.userAgent.toLowerCase()) && (!docMode || docMode <= 8)); + } + +} diff --git a/utilitybelt/core/build-plain.js b/utilitybelt/core/build-plain.js new file mode 100644 index 0000000..190a4f3 --- /dev/null +++ b/utilitybelt/core/build-plain.js @@ -0,0 +1,155 @@ +require([ +/* 'order!lib/jquery/1.7.1/jquery.min' // JQuery http://jquery.com/ + // 'order!lib/jquery/1.7.1/jquery' // JQuery http://jquery.com/ + ,*/ 'order!lib/jqueryui/1.8.17/jquery-ui.min' // JQuery UI http://jqueryui.com/ + , 'order!lib/underscore/1.3.3/underscore' // Underscore.js http://documentcloud.github.com/underscore/ + , 'order!lib/backbone/0.9.2/backbone' // Backbone.js http://documentcloud.github.com/backbone/ + , 'order!lib/backbone-relational/0.5.0/backbone-relational' // Backbone-relational.js https://github.com/PaulUithol/Backbone-relational + , 'order!lib/underscore-plugins/string/underscore.string' // Underscore String plugin http://epeli.github.com/underscore.string/ + , 'order!lib/modernizr/1.7/modernizr.min' // http://www.modernizr.com/ + , 'order!lib/sinon/1.3.2/sinon' + // , 'order!lib/&log/&log' + , 'order!lib/jquery-plugins/placeholder/2.0.7/placeholder' + , 'order!lib/jquery-plugins/cookie/jquery.cookie' + // , 'order!lib/jquery-plugins/jqTransform/1.0/jquery.jqtransform' + , 'order!lib/jquery-plugins/jqTransform/1.0.1dh/jquery.jqtransform.dh' + , 'order!lib/jquery-plugins/vtabs/vtabs' + + /* List of UB core files */ + + , 'order!core/namespaces' + , 'order!core/config/messages' + , 'order!core/config/locales' + , 'order!core/config/MapConfig' + , 'order!core/utils/Class' + , 'order!core/utils/Object' + , 'order!core/utils/getText' + , 'order!core/utils/getLocale' + , 'order!core/utils/Router' + , 'order!core/utils/getTemplate' + , 'order!core/utils/LS' + , 'order!core/utils/Proxy' + , 'order!core/utils/formatPrice' + , 'order!core/utils/formatItem' + , 'order!core/utils/formatAddress' + , 'order!core/utils/formatURL' + , 'order!core/utils/getUrlParams' + , 'order!core/utils/getUrlHash' + , 'order!core/utils/getIcon' + , 'order!core/utils/buildUrl' + , 'order!core/utils/Dom' + , 'order!core/utils/redirectLocation' + , 'order!core/utils/trackingLogger' + + , 'order!core/views/View' + , 'order!core/views/Box' + , 'order!core/views/Lightbox' + , 'order!core/views/Form' + , 'order!core/views/Form/ExitPoll' + , 'order!core/views/MapInterface' + , 'order!core/views/Page' + , 'order!core/views/Tooltip' + + , 'order!core/mixins/Validate' + , 'order!core/mixins/AutosuggestLocations' + + , 'order!core/collections/Collection' + , 'order!core/models/Model' + // , 'order!core/tests/namespaces' //temporary to build list view + // , 'order!core/tests/fixtures/UserAddress' //temporary to build list view + , 'order!core/models/Location' + , 'order!core/models/Address' + , 'order!core/models/DeliveryAddress' + , 'order!core/models/CartItem' + , 'order!core/models/ItemFlavor' + , 'order!core/models/Flavor' + , 'order!core/models/Item' + , 'order!core/models/ItemSize' + , 'order!core/models/Order' + , 'order!core/models/OrderGeneral' + , 'order!core/models/OrderPriceDetails' + , 'order!core/models/Restaurant' + , 'order!core/models/Section' + , 'order!core/models/Coupon' + , 'order!core/models/Payment' + , 'order!core/models/User' + , 'order!core/models/Authorization' + , 'order!core/models/Restaurant' + , 'order!core/models/Category' + , 'order!core/models/Option' + , 'order!core/models/ExitPoll' + , 'order!core/collections/Locations' + , 'order!core/collections/Address' + , 'order!core/collections/DeliveryAddress' + , 'order!core/collections/Cart' + , 'order!core/collections/DeliveryFee' + , 'order!core/collections/Order' + , 'order!core/collections/Category' + , 'order!core/collections/Option' + , 'order!core/collections/GeoLocations' + , 'order!core/views/Lightbox/Throbber' + , 'order!core/views/Form/Login' + , 'order!core/views/Form/Password' + , 'order!core/views/View/UserAccountTrigger' + , 'order!core/views/Form/UserAddress' + , 'order!core/views/Form/Checkout' + , 'order!core/views/Form/Filter' + , 'order!core/views/Maps/GoogleMaps' + , 'order!core/views/View/Cart' + , 'order!core/views/Lightbox/Flavors' + , 'order!core/views/View/LocationSearch' + , 'order!core/views/View/MultipleLocationList' + , 'order!core/views/View/UserAddressList' + , 'order!core/views/View/ActiveAddress' + , 'order!core/views/Lightbox/Login' + , 'order!core/views/Lightbox/Password' + , 'order!core/views/Lightbox/MultipleLocation' + , 'order!core/views/Lightbox/UserAddress' + , 'order!core/views/Lightbox/UserAddressList' + , 'order!core/views/Lightbox/PasswordForgotten' + , 'order!core/views/Lightbox/ZipCodeBox' + , 'order!core/views/Lightbox/ChooseBox' + , 'order!core/views/Lightbox/Error' + , 'order!core/views/Lightbox/ExitPoll' + , 'order!core/views/View/Payment' + , 'order!core/views/View/Confirmation' + , 'order!core/views/Box/Cart' + , 'order!core/views/Box/ReadOnlyCart' + + //, 'template!core/templates/Lightbox/base.html' + , 'template!core/templates/Throbber/Throbber.html' + , 'template!core/templates/Flavors/flavors.html' + , 'template!core/templates/UserAddress/UserAddresses.html' + , 'template!core/templates/UserAddress/UserAddressForm.html' + , 'template!core/templates/UserAddress/DeleteConfirm.html' + , 'template!core/templates/MultipleLocation/MultipleLocation.html' + , 'template!core/templates/MultipleLocation/MultipleLocationList.html' + , 'template!core/templates/Search/LocationSearchSingle.html' + , 'template!core/templates/Cart/Cart.html' + , 'template!core/templates/Cart/Item.html' + , 'template!core/templates/Cart/Details.html' + , 'template!core/templates/Form/ValidationErrors.html' + , 'template!core/templates/Lightbox/small.html' + , 'template!core/templates/Lightbox/base.html' + , 'template!core/templates/Lightbox/close.html' + , 'template!core/templates/Box/base.html' + , 'template!core/templates/Payment/payment.html' + , 'template!core/templates/User/PasswordForgotten.html' + , 'template!core/templates/PLZ/zip_code_lightbox.html' + , 'template!core/templates/PLZ/choose_lightbox.html' + , 'template!core/templates/Filter/FilterForm.html' + + , 'order!apps/Frontend_AU/namespaces' + , 'order!apps/Frontend_AU/config/messages' + , 'order!apps/Frontend_AU/pages/Page' + , 'order!apps/Frontend_AU/pages/Menu' + , 'order!apps/Frontend_AU/pages/Checkout' + , 'order!apps/Frontend_AU/pages/RestaurantList' + , 'order!apps/Frontend_AU/pages/RestaurantListOpen' + , 'order!apps/Frontend_AU/pages/Confirm' + , 'order!apps/Frontend_AU/pages/LandingPage' + , 'order!apps/Frontend_AU/pages/LoggedIn' + , 'order!core/tests/fixtures/Locations' + , 'order!apps/Frontend_AU/config' + , 'order!core/config' +]); diff --git a/utilitybelt/core/collections/Address.js b/utilitybelt/core/collections/Address.js new file mode 100644 index 0000000..6fcd209 --- /dev/null +++ b/utilitybelt/core/collections/Address.js @@ -0,0 +1,46 @@ +core.define('core.collections.Address', { + + extend: 'core.Collection', + + model: core.models.Address, + + initialize: function() { +// _.bindAll(this, 'change') +// this.on('add', function() { console.log(this, 'add') }) +/* + if (arguments[1] && arguments[1].hasOwnProperty('user_id')) { + this.user_id = arguments[1].user_id; + }; +*/ + }, + + url: function() { + +/* + try { + this._assertRequiredParams(); + } catch(error) { + // Error handling... cover this this block with tests as well + console.log('Error constructing collection url!', error); + }; +*/ + + return '/api/users/' + this.getUrlParams().user_id + '/addresses/?fields=address'; + }, + + parse: function(resp, xhr) { + var parsed = core.collections.Address.__super__.parse.call(this, resp, xhr); + parsed = _.map(parsed, function(rec) { + return rec.address || rec; + }); + return parsed; + } + +/* + _assertRequiredParams: function() { + if (!this.user_id) { + throw new Error('Required parameter user_id is missing!'); + }; + }, +*/ +}); diff --git a/utilitybelt/core/collections/Cart.js b/utilitybelt/core/collections/Cart.js new file mode 100644 index 0000000..2ecdc09 --- /dev/null +++ b/utilitybelt/core/collections/Cart.js @@ -0,0 +1,257 @@ +/** + * Collection of cart items that represents a shopping cart. + * Add and remove methods understand an additional option: quantity. + * This allows to update a cart item quantity. Those methods also synchronize + * a global sum. + * + * @property {string} currency The name of the currency used by the cart + * @property {string} currencySymbol The symbol of the currency used by the cart + */ +core.define('core.collections.Cart', { + extend: 'core.Collection', + model: core.models.CartItem, + order: false, + autosave: true, + + /** + * Constructor method. Reads static properties for currency + * and configures the new instance. + */ + initialize: function() { + this.sum = 0; + + // Not every single update for the cart should trigger a call to .save right away, + // so debounce this method. + this.save = _.debounce(_.bind(this.save, this), 1000); + }, + + /** + * Helper function to wrap an item in a cart item. + * @param {core.models.Item} product The Product to wrap and add to the cart + * @param {int} quantity The quantity of the product to add + * @return {core.models.CartItem} A CartItem instance holding the given quantity of the given product + */ + pack: function(product, quantity) { + return new core.models.CartItem(product, quantity); + }, + + /** + * Helper function to update the number of product in a cart item and sync the total price. + * The change event from the cart item is delayed until the cart sum is updated. + * @param {core.models.CartItem} cartItem The CartItem to update + * @param {int} quantity The new quantity of cart item product + * @return {int} The difference between the previous and the new quantity + */ + _updateCartItem: function(cartItem, quantity) { + var oldPrice = cartItem.price(); + var delta = cartItem.update(cartItem.get('quantity') + quantity, {silent: true}); + this.sum += cartItem.price() - oldPrice; + if (this.order) { + this.order.setSubTotal(this.sum); + if (this.autosave) { + // Set order.idle flag to false right away since .save() is debounced. + this.order.idle = false; + this.save(); + } + } + this.trigger('change', cartItem); + return delta; + }, + + /** + * Overwrites the add method to add an item or update its quantity if already + * present and sync the total price of the collection. + * + * @param {Object} models A model or an array of model to add to the collection + * @param {Object} options The options + * + * @trigger item:add (CartItem, delta) when an item is added to the cart + * @trigger quantity:increase (CartItem, delta) when an item has its quantity + * increased (i.e. The item was already in the cart) + */ + add: function(models, options) { + models = _.isArray(models) ? models : [models]; + var opts = _.extend({quantity: 1, silent: false}, options); + if (opts.quantity < 0) + throw 'illegal-quantity'; + for (var i=0, bound=models.length; i 0) { + return this.add(existingCartItem.get('item'), {quantity: delta}); + } else { + return this.remove(existingCartItem.get('item'), {quantity: -delta}); + } + }, + + /** + * Wraps order.save to allow the collection to trigger the sync event. + * + * @trigger sync + */ + save: function(options) { + options || (options = {}); + var success = options.success; + var me = this; + this.order.fromCart(this); + this.order.save(null, { + success: function(order, response) { + me.trigger('sync'); + if (success) + success(order, response); + } + }); + }, + + /** + * Wraps order.search to allow feeding the collection when retrieving the order. + * + * @param {Object} options The options passed to the order search method + * @return {jqXHR} The search request xhr object + */ + fetch: function(options) { + var me = this; + this.autosave = false; + options.success = _.wrap(options.success, function(handler, success, response) { + if (handler) + handler(me.order, success, response); + me.order.toCart(me); + me.autosave = true; + }); + return this.order.search(options); + }, + + isReadyForSync: function() { + return this.order && this.order.isReadyForSync() && this.length > 0; + }, + + hasFee: function() { + return this.order && this.order.hasFee(); + } +}); diff --git a/utilitybelt/core/collections/Category.js b/utilitybelt/core/collections/Category.js new file mode 100644 index 0000000..39d4c23 --- /dev/null +++ b/utilitybelt/core/collections/Category.js @@ -0,0 +1,7 @@ +core.define('core.collections.Category', { + + extend: 'core.Collection', + + model: core.models.Category, + +}); diff --git a/utilitybelt/core/collections/Collection.js b/utilitybelt/core/collections/Collection.js new file mode 100644 index 0000000..b80e5a4 --- /dev/null +++ b/utilitybelt/core/collections/Collection.js @@ -0,0 +1,45 @@ +core.define('core.Collection', { + extend: 'Backbone.Collection', + + /** + * Returns an array JSON objects containing the attributes of the collection models. + * Similar to toJSON method, but should be preferred to use with views. + * Available to enable different JSON to be used by views and Backbone.sync. + * @return {Array of Object} An array of dumb JSON objects having the models attributes + */ + viewJSON: function() { + return this.map(function(model){ return model.viewJSON(); }); + }, + + parse: function(resp, xhr) { + var parsed = resp.data || resp.response.data; + for (var i=0, ii=parsed.length; i biggest.get('amount')) { + biggest = fee; + } + }); + return biggest; + }, + + getBiggestAmount: function() { + var biggest = this.getBiggest(); + return biggest ? biggest.get('amount') : 0; + } +}); diff --git a/utilitybelt/core/collections/ExitPoll.js b/utilitybelt/core/collections/ExitPoll.js new file mode 100644 index 0000000..e9291ae --- /dev/null +++ b/utilitybelt/core/collections/ExitPoll.js @@ -0,0 +1,18 @@ +/** + * Exit Poll Collection, represents a data object for exit poll. + */ +core.define('core.collections.ExitPoll', { + + extend: 'core.Collection', + + model: core.models.ExitPoll, + + getShuffledOptions: function(){ + return _.shuffle(this.models); + }, + + validate: function(data){ + return !jQuery.isEmptyObject(data); + } + +}); diff --git a/utilitybelt/core/collections/GeoLocations.js b/utilitybelt/core/collections/GeoLocations.js new file mode 100644 index 0000000..4c41b67 --- /dev/null +++ b/utilitybelt/core/collections/GeoLocations.js @@ -0,0 +1,44 @@ +core.define('core.collections.GeoLocations', { + + extend: 'core.Collection', + + searchParameters: ['zipcode', 'suburb', 'streetname', 'limit'], + + urlBase: '/api/geo/?', + + initialize: function() { + this.model = core.models.Location; + core.collections.GeoLocations.__super__.initialize.call(this); + }, + + /** + * Set the parameters to fetch locations + * @param {Object} params An object specifying values for the Url parameters. The keys must be included in searchParameters + */ + setUrlParams: function(params){ + var me = this; + var wrongParams = _.filter(params, function(value, key){ + return !_.include(me.searchParameters, key); + }); + if (wrongParams.length) { + throw new Error("Please specify one ore more of the following URL parameters: " + this.searchParameters.join(", ")); + } else { + core.collections.GeoLocations.__super__.setUrlParams.apply(this, arguments); + } + }, + + url: function() { + var params = this.getUrlParams(); + var url = this.urlBase; + var paramsArray = []; + _.each(this.searchParameters, function(param){ + if (params[param]) { + paramsArray.push(param + "=" + params[param]); + } + }); + url += paramsArray.join("&"); + + return url; + } + +}); diff --git a/utilitybelt/core/collections/Locations.js b/utilitybelt/core/collections/Locations.js new file mode 100644 index 0000000..cac5b32 --- /dev/null +++ b/utilitybelt/core/collections/Locations.js @@ -0,0 +1,79 @@ +core.define('core.collections.Locations', { + + extend: 'core.Collection', + + initialize: function() { + this.model = core.models.Location; + core.collections.Locations.__super__.initialize.call(this); + }, + + url: function() { + var params = this.getUrlParams(), url = '/api/locations/?address=' + params['searchLocation']; + if (typeof params['limit'] !='undefined') + url += '&limit=' + params['limit']; + return url; + }, + + parse: function(resp, xhr) { + var parsed = core.collections.Locations.__super__.parse.call(this, resp, xhr); + parsed = _.map(parsed, function(rec) { + return rec.response || rec; + }); + return parsed; + }, + + /** + * Filter collection, leaving only the records with non-empty suburb, city and zipcode and where suburb or zipcode matching the search + * TODO: implement in /locations API + * @return {Collection} Locations collection + */ + filterByRelevance: function(searchTerm) { + var me = this, term = searchTerm.toLowerCase(); + var list = me.filter( function(location) { + var address = location.get('address'); + if (address) { + if (!address.suburb || !address.city || !address.zipcode) // these fields are mandatory for us + return false; + else { + var suburb = address.suburb.toLowerCase(), zipcode = address.zipcode.toLowerCase(); + return (suburb.indexOf(term)>=0 || zipcode.indexOf(term)>=0 || term.indexOf(suburb)>=0 || term.indexOf(zipcode)>=0); + } + } + return false; + }); + return new core.collections.Locations(list); + }, + + /** + * Sort collection, so exact matches in suburb or zipcode will be on top + * TODO: implement in /locations API + * @return {Collection} Locations collection + */ + sortByRelevance: function(searchTerm) { + var me = this; + var list = me.sortBy( function(location) { + var address = location.get('address'), + term = searchTerm.toLowerCase(); + var ind = address.suburb.toLowerCase().indexOf(term) * address.zipcode.indexOf(term); + if (address.suburb.toLowerCase() == term || address.zipcode == term) + return -1; + else if (ind < 0) + return me.length; + else + return ind; + } ); + return new core.collections.Locations(list); + }, + + /** + * Create array which can be consumed by jQuery autocomplete plugin + * @return {Array} Array + */ + buildAutosuggestList: function() { + return this.map( function(location) { + var address = location.get('address'); + return { label: address.suburb + ' ' + (address.zipcode || '') + ', ' + address.state, record: address }; + }); + } + +}); diff --git a/utilitybelt/core/collections/Option.js b/utilitybelt/core/collections/Option.js new file mode 100644 index 0000000..b06b307 --- /dev/null +++ b/utilitybelt/core/collections/Option.js @@ -0,0 +1,7 @@ +core.define('core.collections.Option', { + + extend: 'core.Collection', + + model: core.models.Option, + +}); diff --git a/utilitybelt/core/collections/Order.js b/utilitybelt/core/collections/Order.js new file mode 100644 index 0000000..5d46054 --- /dev/null +++ b/utilitybelt/core/collections/Order.js @@ -0,0 +1,54 @@ +core.define('core.collections.Order', { + + extend: 'core.Collection', + + model: core.models.Order, + + initialize: function() { +// _.bindAll(this, 'change') +// this.on('add', function() { console.log(this, 'add') }) +/* + if (arguments[1] && arguments[1].hasOwnProperty('user_id')) { + this.user_id = arguments[1].user_id; + }; +*/ + }, + + url: function() { + + console.log(this.user_id) + +/* + try { + this._assertRequiredParams(); + } catch(error) { + // Error handling... cover this this block with tests as well + console.log('Error constructing collection url!', error); + }; +*/ + + return '/api/users/' + this.getUrlParams().user_id + '/orders/?fields=order'; + }, + + parse3: function(resp, xhr) { + console.log(arguments) + }, + + parse2: function(resp, xhr) { + var parsed = core.collections.Address.__super__.parse.call(this, resp, xhr); + parsed = _.map(parsed, function(rec) { + return rec.address; + }); + return parsed; + } + +/* + _assertRequiredParams: function() { + if (!this.user_id) { + throw new Error('Required parameter user_id is missing!'); + }; + }, +*/ + + +}); \ No newline at end of file diff --git a/utilitybelt/core/config.js b/utilitybelt/core/config.js new file mode 100644 index 0000000..186a1b8 --- /dev/null +++ b/utilitybelt/core/config.js @@ -0,0 +1,214 @@ +/* + * UB config file + * We use RequireJS as a module loader http://requirejs.org/docs/api.html + */ +if (typeof requireBaseUrl == 'undefined') + var requireBaseUrl = '/utilitybelt/utilitybelt'; + +require.config({ + baseUrl: requireBaseUrl +}); + +define(function() { + var coreInit = function() { + /* Don't change this part if you don't know what you're doing */ + for (var i=0,ii=arguments.length; ioder zurück zur Übersicht', + 'yes': 'Ja', + 'no': 'Nein', + 'close': 'Schliessen', + + 'error_happened_try_again': 'Ein Fehler ist aufgetreten, bitte versuche es noch einmal.', + + 'address_form_comment': 'Die mit Sternchen* gekennzeichneten Felder müssen ausgefüllt werden.', + 'save_address': 'Adresse Speichern', + 'address_list': 'Adressse ändern', + 'no_addresses': 'Keine Addresse', + 'new_address': 'Neue Adresse hinzufügen', + 'edit_address': 'Adresse Bearbeiten', + 'delete_address_confirmation': 'ADRESSE WIRKLICH LÖSCHEN?', + + 'multiple_location_found': 'WIR HABEN MEHRERE ADRESSEN GEFUNDEN', + 'choose_multiple_location': 'Wähle deine Adresse aus der Liste unten aus.', + + 'man': 'Herr', + 'woman': 'Frau', + + 'password': 'Passwort', + 'login_title': 'Logge Dich ein', + 'login_button': 'login', + 'login_error_credentials': 'Login fehlgeschlagen. Bitte probiere es noch einmal!', + 'forgotten_password': 'Passwort vergessen?', + 'header_my_account_link': 'Mein Konto', + 'header_login_link': 'Login', + 'poll_description':'', + + 'flavors_mandatory': '(Pflichtauswahl)', + 'flavors_message_choose' : 'Bitte Pflichtauswahl treffen!', + 'add_to_cart': 'IN DEN WARENKORB', + + 'search_button' : 'suchen', + 'search_input_placeholder' : 'Mohrenstrasse 60, Berlin', + 'search_input_placeholder_short' : 'Strasse Nr, Stadt', + 'search_location_hint': "Bitte eine Adresse zum Suchen eingeben", + 'search_location_hint_title' : "SUCHE", + 'search_location_title': "ADRESSE", + + 'search_not_found_title':'ADRESSE NICHT GEFUNDEN', + 'search_not_found_message':'Ups, wir konnten die von Dir angegebene Adresse nicht finden! Bitte überprüfe Deine Eingabe und versuch es noch einmal.', + + 'password_forgotten_title': 'Passwort vergessen?', + 'password_forgotten_text': 'Du hast dein Passwort vergessen? Das ist wahrlich kein Grund zur Panik. Tippe hier deine Email Adresse ein und wir schicken dir umgehend ein neues Passwort per Email zu.', + 'password_forgotten_not_found': 'Leider konnten wir keinen User mit dieser Email Adresse finden', + 'password_forgotten_submit': 'Abschicken', + + 'shopping_cart': 'Warenkorb', + 'cart_sum': 'Summe', + 'clear_cart': 'Warenkorb löschen', + 'cart_empty': 'Please put something in the cart', + 'checkout': 'Zur Kasse gehen', + 'preorder': 'Preorder', + 'read_only_cart_title': 'Deine bestellung', + 'cart_sub_total': 'Sub total', + 'cart_delivery_fee': 'Delivery fee', + 'cart_coupon_fee': 'Coupon discount', + 'cart_min_order': 'Minimum order', + 'cart_min_order_diff': 'Remaining', + + 'location_box_title' : 'Welcome to %s - please choose a suburb or postcode', + 'location_box_enter' : 'Please enter your postcode or suburb:', + 'location_box_hint' : '(to make sure we can deliver to your house)', + 'location_box_go' : 'go', + 'choose_box_different_zip' : "Choose different zip", + 'choose_box_list_restaurants' : "Show available restaurants", + 'choose_box_not_deliver' : "Sorry, this restaurant does not deliver to the given address", + 'choose_box_find_restaurants' : "Here you can find available restaurants:", + + //payment + 'payment_note': 'Hinweis:', + 'payment_cash': 'Du hast Barzahlung ausgewählt, bitte halte den entsprechenden Betrag für den Lieferanten bereit.', + 'payment_paypal': 'Du hast PayPal ausgewählt. Nach der Bestellung wirst du zu PayPal weitergeleitet, um die Bezahlung abzuschließen.', + 'payment_credit': 'Du hast Zahlung per Kreditkarte ausgewählt. Nach der Bestellung wirst du zu Adyen weitergeleitet, um deine Kreditkartenzahlung abzuschließen.', + 'payment_debit': 'Du hast Zahlung per Sofortüberweisung ausgewählt. Nach der Bestellung wirst du weitergeleitet, um mit Sofortüberweisung zu bezahlen.', + 'coupon_question': 'Hast du einen Gutscheinss?', + 'coupon_bad': 'Dieser Gutscheincode kann nicht eingelöst werden.', + 'coupon_good': '%s Rabatt', + 'loading': 'Lade...', + + 'zipcode_required': 'Please enter your Zipcode', + 'name_required': 'Please enter your Name', + 'lastname_required': 'Please enter your Last Name', + 'street_name_required': 'Please enter your Street Name', + 'street_number_required': 'Please enter your Street Number', + 'suburb_required': 'Please enter your Suburb', + 'city_required': 'Please enter your City', + 'phone_required': 'Please enter your Phone Number', + 'email_required': 'Please enter your Email Address', + 'state_required': 'Please enter your State', + 'valid_email_required': 'Please enter a valid email address', + 'this_order_has_already_been_submitted': 'Can not submit order. Order has already been submitted', + 'invalid_zip_code': 'Please enter a valid Zip Code', + 'invalid_pre_order_time' : 'You have requested a pre-order time when the restaurant is closed', + 'restaurant_still_closed': 'You have requested a pre-order time when the restaurant is closed', + 'invalid_zipcode': "The Zipcode you have entered is not recognised", + 'address_not_delivered_to' : 'The restaurant does not deliver to this address', + 'suburb_zipcode_pair_invalid': 'The zipcode does not match the entered suburb', + + + 'you_must_agree_to_terms': 'You must agree to the Terms & Conditions', + 'please_choose_your_delivery_time': 'Please choose your Delivery Time', + 'please_select_your_payment_method': 'Please select your Payment Method', + 'something_has_gone_wrong': 'There was a problem submitting your order, please try again', + + 'pizza': 'Pizza', + 'fast-food': 'Fast Food', + 'asian' : 'Asiatisch', + 'sushi' : 'Sushi', + 'indian' : 'Indisch', + 'mediterran' : 'Mediterran', + 'oriental' : 'Orientalisch', + 'gourmet' : 'Gourmet', + 'international' : 'International', + 'online_payment': 'Nur mit Onlinezahlung/Coupon', + 'box': 'Nur mit Express Box', + + 'apply_filters' : 'Apply Filters', + 'select_all' : 'Select All', + 'categories' : 'Categories', + 'options' : 'Options', + 'update' : 'Update', + 'hide_filter': 'Hide Filter', + 'show_filter': 'Show Filter', + + //change password lightbox + 'change_details': 'EINSTELLUNGEN ÄNDERN', + 'change_email_password': 'Hier kannst du deine Email Adresse und dein Passwort ändern.', + 'new_password': 'Neues Passwort', + 'repeat_password': 'Passwort wiederholen', + 'save': 'Speichern', + 'settings_saved': 'Deine Einstellungen wurden erfolgreich gespeichert.', + 'passwords_donot_match': 'Passwort und Wiederholung stimmen nicht überein.', + 'settings_not_saved': 'Deine Einstellungen wurden nicht erfolgreich gespeichert.', + + 'page_reload': 'Reload the page.', + 'global_error_sorry': 'We are sorry...', + 'global_ajax_error': 'There was a problem while loading components.' +}; + +core.messages.en = { + 'label': 'Label', + 'salutation': 'Salutation', + 'name': 'Name', + 'lastname': 'Last name', + 'street_name': 'Street', + 'street_number': 'Street number', + 'etage': 'Floor', + 'zipcode': 'ZipCode', + 'city': 'City', + 'description': 'Additional address', + 'phone': 'Phone', + 'email': 'Email', + 'company': 'Company', + 'back_to_list': 'Back to list', + 'yes': 'Yes', + 'no': 'No', + 'close': 'Close', + + 'error_happened_try_again': 'An error occurred, please try again.', + + 'address_form_comment': 'Fields marked with asterisk* are mandatory.', + 'save_address': 'Save address', + 'address_list': 'Addresses list', + 'no_addresses': 'No addresses', + 'new_address': 'New address', + 'edit_address': 'Edit address', + 'delete_address_confirmation': 'Are you sure you want to delete an address?', + + 'multiple_location_found': 'We have found more than one listing.', + 'choose_multiple_location': 'Please choose your location from the list', + + 'man': 'Mr', + 'woman': 'Ms', + + 'password': 'Password', + 'login_title': 'Please log in', + 'login_button': 'login', + 'forgotten_password': 'Forgotten your password?', + 'login_error_credentials': 'Login failed. Please try it again!', + 'header_my_account_link': 'My account', + 'header_login_link': 'Login', + 'poll_title': 'Thanks for your order!', + 'poll_error': 'Please select an answer!', + 'poll_description':'Would you mind telling us where you heard of Delivery Hero?', + + 'flavors_mandatory': '(Mandatory)', + 'flavors_message_choose' : 'Please choose one of these mandatory items!', + 'add_to_cart': 'ADD TO CART', + + 'search_button' : 'search', + 'search_input_placeholder' : 'SUBURB/POSTCODE (EG.ULTIMO 2007)', + 'search_input_placeholder_short' : 'SUBURB/POSTCODE', + 'search_location_hint': "Please provide a suburb/postcode: e.g. Ultimo 2007", + 'search_location_hint_title' : "SEARCH", + 'search_location_title': "Find a restaurant that delivers to you", + 'search_location_title_small': "Enter Your Suburb or Postcode", + + 'location_box_title' : 'Welcome to %s - please choose a suburb or postcode', + 'location_box_enter' : 'Please enter your postcode or suburb:', + 'location_box_hint' : '(to make sure we can deliver to your house)', + 'location_box_go' : 'go', + 'choose_box_different_zip' : "type in another postcode", + 'choose_box_list_restaurants' : "show available restaurants", + 'choose_box_not_deliver' : "Sorry, but %s does not deliver to %s", + 'choose_box_find_restaurants' : "Here you will find a list of available restaurants.", + + 'search_not_found_title':'Address not found', + 'search_not_found_message':'Sorry, we could not find the address', + + 'password_forgotten_title': 'Forgot Password?', + 'password_forgotten_text': 'You forgot your password? There is no reason to panic. Please type in your email address and we will send you immediately a new password by e-mail.', + 'password_forgotten_not_found': "Unfortunately we didn't find a user with this e-mail address", + 'password_forgotten_submit': 'SUBMIT', + + 'shopping_cart': 'Shopping Cart', + 'cart_sum': 'Total', + 'clear_cart': 'Clear cart', + 'cart_empty': 'Please put something in the cart', + 'checkout': 'Checkout', + 'preorder': 'Preorder', + 'read_only_cart_title': 'Your order', + 'cart_sub_total': 'Sub total', + 'cart_delivery_fee': 'Delivery fee', + 'cart_coupon_fee': 'Coupon discount', + 'cart_min_order': 'Minimum order', + 'cart_min_order_diff': 'Remaining', + + //Payment + 'payment_note': 'Note:', + 'payment_cash': 'You have chosen to pay with cash. Please have the corresponding amount ready.', + 'payment_paypal': 'You have chosen PayPal. After ordering you will be redirected to PayPal to complete the payment.', + 'payment_credit': 'You have chosen credit card. After ordering you will be redirected to complete the payment.', + 'payment_debit': 'You have selected payment via debit card. After ordering you will be redirected to pay via bank transfer.', + 'coupon_question': 'Do you have a coupon?', + 'coupon_bad': 'This coupon code cannot be redeemed.', + 'coupon_good': '%s Rebate', + 'loading': 'Loading...', + + 'zipcode_required': 'Please enter your Zipcode', + 'name_required': 'Please enter your Name', + 'lastname_required': 'Please enter your Last Name', + 'street_name_required': 'Please enter your Street Name', + 'street_number_required': 'Please enter your Street Number', + 'suburb_required': 'Please enter your Suburb', + 'city_required': 'Please enter your City', + 'phone_required': 'Please enter your Phone Number', + 'email_required': 'Please enter your Email Address', + 'state_required': 'Please enter your State', + 'valid_email_required': 'Please enter a valid email address', + 'this_order_has_already_been_submitted': 'Can not submit order. Order has already been submitted', + 'invalid_zip_code': 'Please enter a valid Zip Code', + 'invalid_pre_order_time' : 'You have requested a pre-order time when the restaurant is closed', + 'restaurant_still_closed': 'You have requested a pre-order time when the restaurant is closed', + 'invalid_zipcode': "Restaurant doesn't deliver to this Suburb/Zipcode", + 'address_not_delivered_to' : 'The restaurant does not deliver to this address', + 'suburb_zipcode_pair_invalid': 'The zipcode does not match the entered suburb', + + 'you_must_agree_to_terms': 'You must agree to the Terms & Conditions', + 'please_choose_your_delivery_time': 'Please choose your Delivery Time', + 'please_select_your_payment_method': 'Please select your Payment Method', + 'something_has_gone_wrong': 'There was a problem submitting your order, please try again', + + 'apply_filters' : 'Apply Filters', + 'select_all' : 'Select All', + 'categories' : 'Categories', + 'options' : 'Options', + 'hide_filter': 'Hide Filter', + 'show_filter': 'Show Filter', + + //change password lightbox + 'change_details' : 'CHANGE YOUR PASSWORD', + 'change_email_password': 'Here you can change your password.', //only password for now + 'new_password': 'New pasword', + 'repeat_password': 'Repeat password', + 'save' : 'Save', + 'settings_saved': 'Your settings were succesfully saved', + 'passwords_donot_match': 'The two passwords do not match', + 'settings_not_saved': 'Your settings could not be saved.', + + + 'thai': 'Thai', + 'pizza': 'Pizza', + 'indian': 'Indian', + 'chinese': 'Chinese', + 'japanese-and-sushi': 'Japanese & Sushi', + 'italian': 'Italian', + 'asian': 'Asian', + 'burgers-and-grill': 'Burgers & Grill', + 'lebanese-and-turkish': 'Lebanese & Turkish', + 'seafood': 'Seafood', + 'dietary-options': 'Dietary Options', + 'other': 'Other', + + 'online_payment': 'Online payment/Coupon only', + 'box': 'Express Box only', + 'apply' : 'Apply Filters', + 'select_all' : 'Select All', + 'categories' : 'Categories', + 'options' : 'Options', + 'update' : 'Update', + 'hide_filter': 'Hide Filter', + 'show_filter': 'Show Filter', + + 'page_reload': 'Reload the page.', + 'global_error_sorry': 'We are sorry...', + 'global_ajax_error': 'There was a problem while loading components.' +}; diff --git a/utilitybelt/core/mixins/AutosuggestLocations.js b/utilitybelt/core/mixins/AutosuggestLocations.js new file mode 100644 index 0000000..8598856 --- /dev/null +++ b/utilitybelt/core/mixins/AutosuggestLocations.js @@ -0,0 +1,148 @@ +/** +* jQueryUI autocomplete implementation for Locations +*/ +core.define('core.mixins.AutosuggestLocations', { + + /** + * Re-enable autosuggest after it was disabled + */ + enableAutosuggest: function() { + this.getSearchInputField().autocomplete('enable'); + }, + + /** + * Run when user types something into autosuggestion field (see http://jqueryui.com/demos/autocomplete/#option-source) + */ + suggestLocation: function( request, response ) { + + var term = this.cleanSearchTerm(request.term), me = this; + + if (!me.cache) + me.cache = {}; + + if (term in me.cache) { + response(me.cache[term]); + } + else { + var locations = new core.collections.Locations(); + locations.setUrlParams({ searchLocation: request.term, limit: 0 }); + locations.fetch( { silent: true, success: _.bind(this.onFoundSuggestions, this, term, response), error: _.bind(this.onEmptySuggestion, this) }); + } + + }, + + /** + * Callback for successful autosuggest request + * @param {String} term Search term + * @param {Function} response jQueryUI autocomplete callback + * @param {core.collections.Locations} locations Locations collection + * @param {Object} rawData from ajax response + */ + onFoundSuggestions: function( term, response, locations, rawData ) { + if (!locations || locations.length == 0) { + this.onEmptySuggestion(); + return; + } + else { + locations = locations.filterByRelevance(term).sortByRelevance(term); + var list = locations.viewJSON(); + this.cache[term] = list; + response(list); + } + }, + + /** + * Callback for empty autosuggest request + */ + onEmptySuggestion: function() { + this.searchInputField.autocomplete('close'); + }, + + /** + * Run when user select something from the autosuggest list (see http://jqueryui.com/demos/autocomplete/#event-select) + */ + onSelectSuggestedLocation: function(event, ui) { + this.trigger("locationFound", { + 'name': 'locationFound', + 'collection': new core.collections.Locations([{ 'address': ui.item } ]) + }); + }, + + /** + * starting the search when the formular is send via button click or pressing enter in the input field + * @param {Object} event Object for using it in prevent Default + */ + suggestOnEnter: function(event) { + + if (this.__disabled) { + event.preventDefault(); + return; + } + var searchLocationValue = this.cleanSearchTerm(this.getSearchInputField().val()); + var me = this; + + core.utils.trackingLogger.log("search", "search_event_searchbox_address_submit", searchLocationValue); + + //nothing typed in the search input field + if (searchLocationValue.length == 0) { + this.onEmptyLocation(); + event.preventDefault(); + return; + } + + //check if there is an entry in the input field + //if not then let it 3x glow => jquery + var locationsCollection = new core.collections.Locations(); + + me.getSearchInputField().autocomplete('close'); + me.getSearchInputField().autocomplete('disable'); + + locationsCollection.setUrlParams({ + searchLocation: searchLocationValue, + limit: 0 + }); + + var throbber = new core.views.Throbber({ auto: false }); + throbber.show(); + + locationsCollection.fetch({ + success: function(collection, response) { + throbber.hide(); + collection = collection.filterByRelevance(searchLocationValue).sortByRelevance(searchLocationValue); + if (response.data.length > 0) { + me.trigger("locationFound", { + name: 'locationFound', + response: response, + collection: collection, + searchTerm: searchLocationValue + }); + } else { + core.utils.trackingLogger.logError('search_error_nomatch_address', searchLocationValue); + me.trigger("locationNotFound", { + 'name': 'locationNotFound', + 'searchValue': searchLocationValue + }); + } + }, + error: function(collection, xhr, options) { + throbber.hide(); + if (_.indexOf([500, 502], xhr.status) === -1) { + me.trigger("locationFetchError", { + 'name': 'locationFetchError' + }); + } + } + }); + event.preventDefault(); + }, + + /** + * Clean-up search term before sending + * @param {String} term Search term + * @return {String} + */ + cleanSearchTerm: function(term) { + return _.str.trim(_.str.clean(term)); + } + +}) \ No newline at end of file diff --git a/utilitybelt/core/mixins/Validate.js b/utilitybelt/core/mixins/Validate.js new file mode 100644 index 0000000..e4fa7fe --- /dev/null +++ b/utilitybelt/core/mixins/Validate.js @@ -0,0 +1,151 @@ +/** + * Validate Mixin. + * Implements core mechanisms for validation. + */ +core.define('core.mixins.Validate', { + + /** + * Property describing the validation scheme, e.g.: + * + * validation: { + * name: { + * check: 'required', + * msg: 'name is required' + * }, + * age: [{ + * check: function(value) { return value > 0 && value < 100 }, + * scenario: 'save' + * }, { + * check: 'notEmpty' + * }] + * } + */ + + validationRules: _.extend({}, { + regexp: function(val, pattern) { + return pattern.test(val); + }, + notEmpty: function(val) { + return !(val === '' || _.isUndefined(val) || _.isNull(val)); + }, + required: function(val) { + return !(_.isUndefined(val) || _.isNull(val)); + }, + email: function(val) { + var pattern = /^\s*[\w\-\+_]+(\.[\w\-\+_]+)*\@[\w\-\+_]+\.[\w\-\+_]+(\.[\w\-\+_]+)*\s*$/; + return pattern.test(val); + } + }), + + + /** + * Provides a default validate definition. + * The return value is Backbone-compliant: it returns validation errors. + * @param {Array} attributes The list of attributes to validate (defaults to all attributes) + * @param {Object} options A hash that can hold the scenario information + * @return {mixed} false if no validation error, an array of validation errors otherwise + */ + validate: function(attributes, options) { + attributes || (attributes = this.attributes); + var opts = _.extend({scenario: this.scenario || ''}, options), + self = this; + this.validationErrors = []; + var rules = this.getRules(attributes, opts.scenario); + var failures = this.applyRules(attributes, rules); + if (failures.length) { + this.validationErrors = failures; + _.each(failures, function(failure) { + self.trigger('error:' + failure.attr, self, failure, options); + }); + } + return failures.length ? failures : false; + }, + + /** + * Overwrites Backobne.isValid() to check if the model has + * validation errors or not. + * @return Boolean True if the model is valid, false otherwise + */ + isValid: function() { + return !this.validationErrors || !this.validationErrors.length; + }, + + /** + * Gets the rule set for given attributes and scenario. + * Flatten the rules: every entry holds its attribute name + * (instead of lying in a parent object). + * @param {Array} attributes The list of attributes to be validated + * @param {String} scenario The scenario to filter the rules + * @return {Array} A flat array of validation rules + * + * TODO handle attribute-wide scenario + */ + getRules: function(attributes, scenario) { + var activeRules = []; + _.each(this.validation, function(ruleSet, attr) { + _.isArray(ruleSet) || (ruleSet = [ruleSet]); + _.each(ruleSet, function(rule) { + if (!rule.scenario || rule.scenario.indexOf(scenario) > -1) { + activeRules.push(_.extend({attr: attr}, rule)); + } + }); + }); + return activeRules; + }, + + /** + * Applies a rule set by creating validators, running them + * and returning the failures. + * @param {Object} attributes The hash of attributes + values to validate + * @param {Array} rules A flat array of validation rules + * @return {Array} An array of unverified rules + */ + applyRules: function(attributes, rules) { + var invalids = []; + for (var r=0, bound=rules.length; r -1; + }); + _.each(relations, function(rel) { + var related = me.get(rel.key); + if (related && related.validate) { + // has one (item has one flavor) + validationErrors = _.union(validationErrors, related.validate(data, options) || []); + } else if (related) { + // has many (flavor has many items) + related.each(function(subrelated) { + validationErrors = _.union(validationErrors, subrelated.validate(data, options) || []); + }); + } + }); + } + return validationErrors; + } +}); diff --git a/utilitybelt/core/models/ItemSize.js b/utilitybelt/core/models/ItemSize.js new file mode 100644 index 0000000..4b2b23c --- /dev/null +++ b/utilitybelt/core/models/ItemSize.js @@ -0,0 +1,43 @@ +/** + * Item Model, represents a menu item, related to its flavors and sizes + */ +core.define('core.models.ItemSize', { + + extend: 'core.Model', + + idAttribute: false, + + fields: { + 'name': { 'dataType': 'string' }, + 'description': { 'dataType': 'string', 'default': '' }, + 'price': { 'dataType': 'number' } + }, + + validations: [ + ], + + initialize: function() { + }, + + attrPath: function(attr, sep, allNodes, withSize, withFlavor) { + return withSize ? this.get('name') : false; + }, + + /** + * Gets the price of the item size. + * If no price is available, the return value is 0. + * @return {number} The price of the item size (defaults to 0) + */ + price: function() { + return this.get('price') || 0; + }, + + /** + * Returns a lightweight dumb object representing the instance. + * Explictely ignore any ID as this model doesn't have one. + * @return {Object} A lightweight and dumb representation of the instance + */ + toJSON: function() { + return { name: this.get('name'), price: this.get('price') }; + } +}); diff --git a/utilitybelt/core/models/Location.js b/utilitybelt/core/models/Location.js new file mode 100644 index 0000000..2aba8e4 --- /dev/null +++ b/utilitybelt/core/models/Location.js @@ -0,0 +1,57 @@ +core.define('core.models.Location', { + + extend: 'core.Model', + + fields: { + 'zipcode': { 'dataType': 'number', 'default': '' }, + 'city': { 'dataType': 'string', 'default': '' }, + 'street_number': { 'dataType': 'string', 'default': '' }, + 'street_name': { 'dataType': 'string', 'default': '' }, + 'country': { 'dataType': 'string', 'default': '' }, + 'longitude': { 'dataType': 'string', 'default': '' }, //? not a number? + 'latitude': { 'dataType': 'string', 'default': '' } //? not a number? + }, + + validations: [], + + initialize: function(){ + }, + + url: function(){ + return "/api/locations/"; + }, + + parse: function(resp, xhr){ + /* TODO - maybe some other way, this is done in order to use one model for both collection + * and individual data API call + */ + if(typeof resp.response === 'undefined'){ + return resp; + }else{ + var serverResponse = resp.response; + var parsed = { + user_id: parseInt(serverResponse.user_id), + id: parseInt(serverResponse.id) + }; + parsed = _.extend(parsed, serverResponse.address || serverResponse); + return parsed; + }; + }, + + toJSON: function(){ + var result = _.clone(this.attributes); + if(!result.toCollection){ + delete result.user_id; + }; + return result; + }, + + viewJSON: function(){ + var address = this.get('address'), result = _.clone(address); + result.label = core.utils.formatAddress(address, 'full'); + return result; + }, + + urlRoot: '/addresses' + +}); \ No newline at end of file diff --git a/utilitybelt/core/models/Model.js b/utilitybelt/core/models/Model.js new file mode 100644 index 0000000..71a34dd --- /dev/null +++ b/utilitybelt/core/models/Model.js @@ -0,0 +1,175 @@ +/** + * Model is a representation of the data structures used in your application. Model defined as a set of fields. + * For more info see [http://documentcloud.github.com/backbone/#Model](http://documentcloud.github.com/backbone/#Model) + * + * Example: + * + * core.define('core.models.Address', { + * + * extend: 'core.Model', + * + * fields: { + * 'zipcode': { 'dataType': 'number', 'default': '' }, + * 'name': { 'dataType': 'string', 'default': '' } + * }, + * + * validations: [ + * { field: 'zipcode', type: 'required', message_key: 'zipcode_required' }, + * { field: 'name', type: 'required', message_key: 'required' } + * ] + * }); + * + * @class core.Model + * @requires Backbone.Model + * @param {String} self Class name + * @param {Array} validations Array of validations object + * @param {Array} fields Array of Model fields + */ + +core.define('core.Model', { + + extend: 'Backbone.RelationalModel', + + mixins: ['core.mixins.Validate'], + + /** + * Tells if save has been invoked. + * If it's idle, the boolean is true and false otherwise. + */ + idle: true, + + /** + * Remembers the number of time save was called for update. + * The sync method is responsible of incrementing the counter + + and he successful save handler is reponsible of decrementing it. + */ + syncCount: 0, + + /** + * Returns a JSON object containing the attributes of the model. + * Similar to toJSON method, but should be preferred to use with views. + * @return {Object} A dumb JSON object having the instance attributes + */ + viewJSON: function() { + return core.Model.__super__.toJSON.call(this); + }, + + /** + * Generic getter offering easy per-attribute getter overwritting. + * Will call getAttribute method (camelcase) if present, or + * return the attribute value. + * @param {string} attr The name of the attribute to get + * @return {mixed} The result of the overwritten getter or the attribute value + */ + get: function(attr) { + var method = _.str.camelize('get_' + attr); + return method in this ? this[method].call(this) : core.Model.__super__.get.call(this, attr); + }, + + /** + * Generic has method offering easy per-attribute overwritting. + * Will call hasAttribute method (camelcase) if present, or + * return the attribute value. + * @param {string} attr The name of the attribute to check existence + * @return {mixed} The result of the overwritten has method or the attribute existence + * @see http://documentcloud.github.com/backbone/#Model-has + */ + has: function(attr) { + var method = _.str.camelize('has_' + attr); + return method in this ? this[method].call(this) : core.Model.__super__.has.call(this, attr); + }, + + /** + * Returns the relative URL where the model's resource would be located on the server. + * @method url + * @returns String URL + */ + url: function() { + return '/model_api'; + }, + + /** + * Initializes the Model by setting undefined attributes to the + * defaults specified by the fields property. + * @method initialize + */ + initialize: function() { + var defaults = {}; + _.each(this.fields, function(definition, field) { + defaults[field] = definition['default']; + }); + this.set(_.extend(defaults, this.attributes)); + }, + + /** + * Improves Backbone.sync by providing a counter of update sync call. + * As this counter is incremented with sync call, this method can + * safely embedded (debounced, ...). + * @see http://backbonejs.org/#Sync + */ + sync: _.wrap(Backbone.sync, function(sync, method, model, options) { + if ('update' == method) { + model.syncCount += 1; + + var success = options.success; + options.success = _.wrap(success, _.bind(this.syncCallbackWrapper, model)); + var error = options.error; + options.error = _.wrap(error, _.bind(this.syncCallbackWrapper, model)); + } + return sync.apply(this, _.tail(arguments)); + }), + + /** + * Wrapper for ajax event callbacks that keeps idle and syncCount properties + * in a consistent state. + * Bear in mind that callback order is important if you plug behavior + * on Backbone events (e.g. sync, change). + * Make sure you put the context to the model instance. + * + * @see http://docs.jquery.com/Ajax_Events + * + * @param handler {Function} The callback to wrap + */ + syncCallbackWrapper: function(handler, resp, status, error) { + this.syncCount -= 1; + this.idle = 0 === this.syncCount && 'error' != status; + if (handler) + handler.apply(handler, _.tail(arguments)); + }, + + validate: function(data) { + var errors = this.getValidationErrors(data); + if (errors.length > 0) + return errors[0]; + }, + + /** + * Overwritten method providing a status: + * true if no sync is ongoing, false otherwise. + * @see http://backbonejs.org/#Model-save + */ + save: function(key, value, options) { + // if keys is an explicit empty string, overwrite options + // to comply with API and Backbone (empty data + json content type) + if ('' === key) { + // if key is empty, second param is options + _.extend(value, { + data: '', + contentType: 'application/json' + }); + key = null; + } + + this.idle = false; + return core.Model.__super__.save.call(this, key, value, options); + }, + + /** + * Tells if a model is ready for a sync call, i.e. if + * no sync is currently active or planned. + * @return {Boolean} True if no sync is currently active, false otherwise + */ + isReadyForSync: function() { + return this.idle && !this.syncCount; + } +}); diff --git a/utilitybelt/core/models/Option.js b/utilitybelt/core/models/Option.js new file mode 100644 index 0000000..57bcbff --- /dev/null +++ b/utilitybelt/core/models/Option.js @@ -0,0 +1,8 @@ +/** + * Option Model + */ +core.define('core.models.Option', { + + extend: 'core.Model', + +}); diff --git a/utilitybelt/core/models/Order.js b/utilitybelt/core/models/Order.js new file mode 100644 index 0000000..1a523eb --- /dev/null +++ b/utilitybelt/core/models/Order.js @@ -0,0 +1,881 @@ +/** + * Order Model, represents an user order, related to sections + * @see http://mockapi.lieferheld.de/doc/reference/users/user_orders_details.html + */ +core.define('core.models.Order', { + + extend: 'core.Model', + + fields: { + 'id': { 'dataType': 'string' }, + 'uri': { 'dataType': 'string' }, + 'number': { 'dataType': 'string' }, + // 'coupon': { 'dataType': '', 'default': '' }, + // 'delivery_address': { 'dataType': '', 'default': '' }, + // 'order_price_details': { 'dataType': '', 'default': '' }, + 'delivery_time': { 'dataType': 'datetime' }, + 'estimated_minutes': { 'dataType': 'string' }, + // 'payment': { 'dataType': '', 'default': '' }, + // 'validity': { 'dataType': '', 'default': '' }, + // 'user_location': { 'dataType': '', 'default': '' }, + 'operation': { 'dataType': 'string', 'default': 'validate' }, + 'status': { 'dataType': 'string' } + }, + + relations: [ + { + type: Backbone.HasMany, + key: 'sections', + relatedModel: 'core.models.Section' + }, + { + type: Backbone.HasOne, + key: 'price_details', + relatedModel: 'core.models.OrderPriceDetails', + reverseRelation: { + key: 'order', + includeInJSON: false + } + }, + { + type: Backbone.HasOne, + key: 'general', + relatedModel: 'core.models.OrderGeneral' + }, + { + type: Backbone.HasOne, + key: 'coupon', + relatedModel: 'core.models.Coupon' + }, + { + type: Backbone.HasOne, + key: 'payment', + relatedModel: 'core.models.Payment', + reverseRelation: { + key: 'order' + } + }, + { + type: Backbone.HasOne, + key: 'delivery_address', + relatedModel: 'core.models.DeliveryAddress' + } + ], + + validation: { + + 'delivery_time': [ + { check: 'required', msg: 'please_choose_your_delivery_time' } + ], + 'payment': [ + { check: 'required', msg: 'please_select_your_payment_method' }, + { check: 'notEmpty' } + ], + 'email': [ + { check: 'required', msg: 'email_required' }, + { check: 'notEmpty' }, + { check: 'email', msg: 'valid_email_required' } + ] + }, + + backEndValidationErrorMap: { + 'delivery_time': { + '1': { + attr: 'delivery_time', + msg: 'invalid_pre_order_time' + }, + '2': { + attr: 'delivery_time', + msg: 'restaurant_still_closed' + } + }, + 'delivery_address': { + '1': { + attr: ['zipcode','suburb'], + msg: 'invalid_zipcode' + }, + '2': { + attr: 'city', + msg: 'city_not_specified' + }, + '3': { + attr: 'street_name', + msg: 'street_name_not_specified' + }, + '4': { + attr: 'street_number', + msg: 'street_number_required' + }, + '5': { + attr: 'name', + msg: 'name_required' + }, + '6': { + attr: 'lastname', + msg: 'lastname_required' + }, + '7': { + attr: 'phone', + msg: 'phone_required' + }, + '8': { + attr: 'email', + msg: 'email_required' + }, + '9': { + attr: ['zipcode','suburb'], + msg: 'address_not_delivered_to' + }, + '10': { + attr: ['zipcode','suburb'], + msg: 'suburb_zipcode_pair_invalid' + } + + }, + 'payment_method': { + attr: 'payment_method', + msg: 'payment_method_invalid' + }, + 'generic': { + '615': function(message) { + var matched = message.match(/<(.*)>/g); + switch (matched[0]) { + case "": + var error = { + 'attr': 'email', + 'msg': 'valid_email_required', + 'supressi18n': true + }; + break; + } + return error; + }, + '616': function(message) { + var attrs = message.match(/<([^>]*)>/g); + if (attrs && attrs.length > 0) { + attrs = attrs.join(",").replace(/[&<>]/g, '').split(","); + } else { + attrs = ''; + } + + var error = { + 'attr': attrs, + 'msg' : message.replace(/[&<>]/g, ''), + 'supressi18n': true + } + + return error; + }, + '618': function(message) { + var error = { + 'attr': ['zipcode','suburb'], + 'msg' : message.replace(/[&<>]/g, ''), + 'supressi18n': true + } + return error; + } + } + }, + + /** + * Constructor + * + */ + initialize: function() { + core.models.Order.__super__.initialize.call(this); + if (!this.has('price_details')) { + this.set('price_details', new core.models.OrderPriceDetails()); + } + if (!this.has('payment')) { + this.set('payment', new core.models.Payment()); + } + }, + + /** + * Override the sync method to account for https://devheld.jira.com/browse/RGP-2096 + * Here we force whichever active address is saved client-side back to the server, if it differs from the address currently saved with the order + */ + sync: function(method, model, options) { + var me = this; + var oldSuccess = options.success; + var modelSet = false; + options.success = function(model, res, xhr){ + var delivery_address = model.delivery_address; + if (delivery_address) { + var address = delivery_address.address; + var storedAddress = me.get("delivery_address").get("address"); + if ( storedAddress && !_.isEqual(address, storedAddress.toJSON()) ) { + delete model.delivery_address; //don't overwrite the model with outdated server-side address + oldSuccess.apply(this, arguments); //hydrate the model so we can save it with its new address + modelSet = true; + me.save({}, { + success: function(){ + me.trigger('order:changeAddress'); + } + }); + } + } + if (!modelSet) { + oldSuccess.apply(this, arguments); + } + } + core.Model.prototype.sync.apply(this, arguments); + }, + + /** + * TODO refactor the order_price_details name modificatio in the model parse method. + */ + set: function(key, value, options) { + core.models.Order.__super__.set.call(this, key, value, options); + if (this.has('order_price_details')) { + core.models.Order.__super__.set.call(this, 'price_details', new core.models.OrderPriceDetails(this.get('order_price_details')), {silent: true}); + this.unset('order_price_details'); + } + return this; + }, + + /** + * Returns the resource address so that Backbone is able to sync the instance. + * @see http://mockapi.lieferheld.de/doc/reference/users/user_orders_details.html + */ + url: function() { + var uid = this.get('general').get('user_id'); + if (!uid) { + throw 'missing-user-id'; + } + var url = ['/api/users/', uid, '/orders/']; + if (!this.isNew()) { + url = url.concat([this.id, '/']); + } + return url.join(''); + }, + + /** + * Searches an order using the user id and a restaurant id. + * When such a restaurant is found, the model is then fetched normally + * @param {Object} param The options for the loading, if search was successful + * @see http://mockapi.lieferheld.de/doc/reference/users/user_orders_details.html#general + */ + search: function(options) { + var uid = this.get('general').get('user_id'), + rid = this.get('general').get('restaurant_id'); + if (!uid) { + throw 'missing-user-id'; + } + var base = [ + '/api/users/', uid, '/orders/', + '?fields=general&general:restaurant_id=', + rid, '&status=created' + ]; + this.fetch(_.extend({}, options, { + url: base.join(''), + success: function(order, response) { + var data = response.data[0] || {}; + if (data.id) { + order.set(data); + order.fetch(options); + } else { + order.set(order.previousAttributes()); + if (options.success) { + options.success(order, response); + } + } + } + })); + }, + + /** + * Initiate the Order Checkout process + * @param {Object} formData Checkout Form Data + * @param {Boolean} validateOnly If true, the form is just validated and not submitted. + */ + checkout: function(formData, validateOnly) { + var self = this, + payment; + + if (this.get('status') != 'created') { + this.validationErrors = [{ + 'attr': 'name', + 'msg': 'this_order_has_already_been_submitted' + }]; + this.trigger("error:checkout", this); + return; + } + + this.isCheckout = true; + this.setFormData(formData); + this.validate(); + + if (this.validationErrors && this.validationErrors.length > 0) { + return false; + } + + if(!validateOnly){ + this.updateUser(this.user); + } + }, + + /** + * Updates the model reading the given form data from the Checkout page + * @param {Object} formData The serialized form data (see core.views.Form.getValues) + */ + setFormData: function(formData){ + this.formData = formData; + this.setData(); + }, + + /** + * Delivery Address Handler + * + */ + handleAddress: function() { + var deliveryAddress, address, order; + + order = this; + + deliveryAddress = this.get('delivery_address'); + if (deliveryAddress) { + address = deliveryAddress.get('address'); + + if (!address) { + throw new Error('order.handleAddress(): Address model not found.'); + } + + if (address && _.values(address.attributes).length > 0) { + var geoLocations = new core.collections.GeoLocations(); + geoLocations.setUrlParams({ + suburb: address.get("suburb"), + zipcode: address.get("zipcode") + }); + + geoLocations.fetch({ + success: function(locations) { + var addresses = new core.collections.DeliveryAddress(); + var normalisedAddressParts = locations.models[0].get('geo_area'); + + addresses.setUrlParams({ + 'user_id': order.get('general').get('user_id') + }); + + addresses.on('noMatch', function() { + deliveryAddressAddress = deliveryAddress.get('address'); + deliveryAddress.post = true; + + deliveryAddressAddress.set({ + suburb: normalisedAddressParts.suburb, + city: normalisedAddressParts.city, + state: normalisedAddressParts.state + },{silent: true}); + + deliveryAddress.save(deliveryAddressAddress, { + success: function(model, response) { + if (response.errors && response.errors.length > 0) { + if (!_.isArray(order.validationErrors)) { + order.validationErrors = []; + } + order.parseBackendValidationErrors(response); + } else { + order.set({ + 'delivery_address': deliveryAddress + }); + order.checkoutWithAddress(); + } + }, + error: function() { + order.validationErrors = [{ + attr: '', + msg: 'something_has_gone_wrong' + }]; + order.trigger("error:checkout", order); + throw new Error('order.handleAddress(): Could not save delivery address'); + } + }); + }); + + addresses.on('match multipleMatches', function(matches) { + deliveryAddress = _.isArray(matches) ? matches[0] : matches; + order.set({ + 'delivery_address': deliveryAddress + }, { + silent: true + }); + order.checkoutWithAddress(); + }); + + addresses.search(address, { + error: function() { + order.validationErrors = [{ + attr: '', + msg: 'something_has_gone_wrong' + }]; + order.trigger("error:checkout", order); + throw new Error('order.handleAddress(): Could not fetch addresses'); + } + }); + }, + error: function() { + order.validationErrors = [{ + attr: '', + msg: 'something_has_gone_wrong' + }]; + order.trigger("error:checkout", order); + throw new Error('order.handleAddress(): Could not find location'); + } + }); + } + } + }, + + /** + * Set Data relevant to order Checkout + * + */ + setData: function() { + this.setPayment(); + this.setDeliveryTime(); + this.setDeliveryAddress(); + this.set({ + 'terms': this.formData.terms, + 'email': this.formData.email + }, { silent: true }); + }, + + /** + * Set User + * @param {Object} user User Model + */ + setUser: function(user) { + this.user = user; + this.get("general").set({ 'user_id': user.id }); + }, + + /** + * Set Delivery Time + * + */ + setDeliveryTime: function() { + var deliveryTimeISO = (this.formData.delivery_time === 'preorder') ? this.formData.preorder_time : ''; + this.set('delivery_time', deliveryTimeISO, {silent: true}); + }, + + /** + * Set Delivery Address + * + */ + setDeliveryAddress: function() { + var address, deliveryAddress; + + address = new core.models.Address(); + address.set({ + 'state': this.formData.state, + 'zipcode': this.formData.zipcode, + 'suburb': this.formData.suburb, + 'street_name': this.formData.street_name, + 'street_number': this.formData.street_number, + 'unit_number': this.formData.door, + 'phone': this.formData.phone, + 'name': this.formData.name, + 'lastname': this.formData.lastname, + 'other': this.formData.other, + 'company': this.formData.company, + 'comments': this.formData.comments + }, {silent: true}); + + deliveryAddress = new core.models.DeliveryAddress(); + deliveryAddress.set({ + 'user_id': this.get('general').get('user_id'), + 'address': address + }); + + this.set('delivery_address', deliveryAddress, {silent: true}); + }, + + /** + * Set payment type + * + */ + setPayment: function() { + var payment; + if (this.formData.centralpayment) { + payment = { + 'method': { + id: this.formData.centralpayment + } + }; + } + this.set({'payment': payment},{silent: true}); + }, + + /** + * Save User / Sync with API + * @param {core.models.User} user User Model + */ + updateUser: function(user) { + var order = this; + user.save({ + 'name': this.formData.name, + 'lastname': this.formData.lastname, + 'phone': this.formData.phone, + 'email': this.formData.email + }, { + success: function(model, response) { + if (response.errors && response.errors.length > 0) { + order.parseBackendValidationErrors(response); + } else { + order.handleAddress(); + } + }, + silent: true + }); + }, + + /** + * Complete Checkout with a alidated Delivery Address + * + */ + checkoutWithAddress: function() { + var order = this; + + this.save({}, { + success: function(model, response) { + var errors = []; + if (response.error) { + order.validationErrors = [{ + 'attr': '', + 'msg': 'something_has_gone_wrong' + }]; + order.trigger("error:checkout", order); + + return; + } + // Check for, parse and display Backend Validation Errors + errors = model.parseBackendValidationErrors(response); + + // If all is well Finalise the order: + if (!errors && response.status && response.status === 'created') { + order.set('operation', 'final'); + order.save({}, { + success: function(model, response) { + errors = model.parseBackendValidationErrors(response); + if (!errors) { + order.finalizeCheckout(response); + } + }, + error: function() { + order.validationErrors = [{ + attr: '', + msg: 'something_has_gone_wrong' + }]; + order.trigger("error:checkout", order); + throw new Error('order.checkoutWithAddress: problem saving (operation:final) order'); + } + }); + } + }, + + error: function(model, errors) { + if (errors.length > 0) { + order.validationErrors = [{ + attr: '', + msg: 'something_has_gone_wrong' + }]; + order.trigger("error:checkout", order); + throw new Error('order.checkoutWithAddress: problem saving (operation:validate) order'); + } + } + }); + }, + + /** + * Finalizes the checkout depending on the payment method. At the end of the payment, redirects to the order confirmation page. + */ + finalizeCheckout: function(order){ + var confirmation_url = core.utils.formatURL('/order_confirmation/') + this.get('id'); + this.trigger('finalized'); + if (this.get('payment').isPaypal()) { + this.trigger('payWithPaypal', order.payment); + } else { + if (order.status && _.indexOf(['success', 'progress', 'paying'], order.status) > -1) { + window.location.href = confirmation_url; + } + } + }, + + parse: function(response, xhr) { + + if (_.has(response, 'operation')) { + delete response.operation; + } + var resp = _.extend({},response); + if (response.delivery_address) { + if (response.delivery_address.address && _.isEmpty(response.delivery_address.address)) { + delete response.delivery_address; + } + } + return response; + }, + + /** + * Parse Backend Validation Errors + * @param {Object} response JSON response object. + */ + parseBackendValidationErrors: function(response) { + var backendValidationErrors = {}, parsedErrors = []; + var me = this; + + if (response.validity) { + // Handle Order Model Validations + _.each(response.validity, function(isValid, orderSection, validities){ + var errors = validities.errors; + if (isValid === false) { + backendValidationErrors[orderSection] = errors && errors[orderSection] ? errors[orderSection] : orderSection; + } + }); + + _.each(backendValidationErrors, function(sectionErrors, orderSection){ + switch (orderSection) { + case 'delivery_time': + case 'delivery_address': + _.each(backendValidationErrors[orderSection], function(errorCode, index){ + var err = me.backEndValidationErrorMap[orderSection]; + var errorKey = backendValidationErrors[orderSection][index]; + if (errorKey !== undefined){ + parsedErrors.push(err[errorKey]); + } + }); + break; + case 'payment_method': + parsedErrors.push(me.backEndValidationErrorMap.payment_method); + break; + } + }); + } + + /** + * @doc http://mockapi.lieferheld.de/beta/doc/reference/extended_errors.html#reference-response-codes-example + */ + if (response.errors) { + _.each(response.errors, function(errorObj, index){ + var errorMessage = errorObj.error_message; + var errorCode = errorObj.error_code; + var errorHandler = me.backEndValidationErrorMap.generic[errorCode]; + if (errorHandler){ + var errorData = errorHandler(errorMessage); + parsedErrors.push(errorData); + } + }); + } + + if (parsedErrors.length > 0) { + this.validationErrors = parsedErrors; + this.trigger('error:checkout', this); + return parsedErrors; + } else { + return false; + } + }, + + /** + * Overriden save method to allow special handling for new records. + * @see http://documentcloud.github.com/backbone/#Model-save + */ + save: function(key, value, options) { + if (this.isNew()) { + return this.create.apply(this, arguments); + } else { + return core.models.Order.__super__.save.apply(this, arguments); + } + }, + + /** + * Issues a manual Backbone sync call to create the resource. + * As it saves on success, no handlers passed in the options + * are executed on creation success. They are deferred to the + * consecutive save() call. + * @return jqXHR The XHR issued by the sync + */ + create: function(key, value, options) { + var me = this; + var sections = me.get('sections').toJSON(); + return Backbone.sync('create', { + url: me.url(), + toJSON: function() { + return { restaurant_id: me.get('general').get('restaurant_id') }; + } + }, { + success: function(response) { + me.parse(response); + if (me.has('delivery_address')){ + delete response.delivery_address; //if creating a new order, and there is an active address, use that and not the empty address returned by the POST + } + response.sections = sections; + me.set(response); + me.save(key, value, options); + } + }); + }, + + /** + * Adds the operation attribute to the JSON object for sending to the backend. + * @return {Object} JSON representation of the instance + */ + toJSON: function() { + var json = { id: this.id, operation: this.get('operation') }; + + if (this.has('general')) { + json.general = this.get('general').toJSON(); + } + + if (this.has('delivery_address')) { + json.delivery_address = this.get('delivery_address').toJSON(); + } + json = _.extend( + json, + {sections: this.get('sections').toJSON()}, + {delivery_time: this.get('delivery_time')}, + {delivery_address: this.get('delivery_address')} + ); + + if (this.has('payment')){ + json.payment = this.get('payment').toJSON(); + } + if (this.has('coupon')){ + json.coupon = this.get('coupon').toJSON(); + } + return json; + }, + + /** + * Populates an order model using the content of the provided cart. + * @param cart {core.models.Order} The used to generate the order + * @return this {core.models.Order} this + */ + fromCart: function(cart) { + var sections = this.get('sections'); + sections.reset(); + var section = new core.models.Section(); + sections.add(section); + cart.each(function(cartItem, i) { + // backend does not support multiple section when submitting an order + // var section = sectionFactory(cartItem.get('sectionId')), + var items = section.get('items'), + item = cartItem.get('item'); + item.set('quantity', cartItem.get('quantity'), {silent: true}); + items.add(item); + }); + return this; + }, + + /** + * Generates a cart based on an order. + * @return {core.collections.Cart} A cart having the order items + */ + toCart: function(existingCart) { + var cart = existingCart || new core.collections.Cart(); + cart.order = this; + this.get('sections').each(function(section) { + section.get('items').each(function(item) { + cart.add(item, { quantity: item.get('quantity') }); + }); + }); + return cart; + }, + + changeOwner: function(user, options) { + this.isCheckout = false; + this.set({ + operation: this.fields.operation['default'] + }, { silent: true }); + if (!this.ownedBy(user)) { + this.setUser(user); + this.unset('id'); + this.save(null, options); + } else if (options.success) { + options.success(this); + } + }, + + ownedBy: function(user) { + return this.get("general").get('user_id') == user.id; + }, + + /** + * Resets the order's coupon to empty and udpates the price_details accordingly + */ + resetCoupon: function(){ + this.set("coupon", { + code: "" + }); + var price_details = this.get('price_details'); + price_details.set('coupon_fee', 0); + this.set('price_details', price_details); + this.trigger('change:price_details'); + }, + + /** + * Front End Validations + * Override mixed in validate method to accomodate different levels of validation + * @param {Array} attributes The list of attributes to validate (defaults to all attributes) + * @param {Object} options A hash that can hold the scenario information + * @return {mixed} false if no validation error, an array of validation errors otherwise + */ + validate: function(attributes, options) { + var failures = [], + address, + deliveryAddress, + orderFailures; + + deliveryAddress = this.get('delivery_address'); + if (deliveryAddress && deliveryAddress.self) { + + address = deliveryAddress.get('address'); + + if (this.isCheckout) { + addressFailures = address.validate(); + + if(addressFailures.length > 0) { + failures = failures.concat(addressFailures); + } + + orderFailures = core.models.Order.__super__.validate.call(this, attributes, options); + + if (orderFailures.length > 0) { + failures = failures.concat(orderFailures); + } + + this.validationErrors = failures; + this.trigger('error:checkout', this); + return failures.length ? failures : false; + } + } + return false; + }, + + /** + * Sets the order's subtotal + */ + setSubTotal: function(sum) { + this.get('price_details').set('subtotal', sum); + }, + + hasFee: function() { + return this.has('price_details') && this.get('price_details').hasFee(); + }, + + setRestaurant: function(restaurant) { + if (!this.checkRestaurantId(restaurant.get('id'))) { + throw 'wrong-restaurant-for-order'; + } + this.restaurant = restaurant; + }, + + checkRestaurantId: function(id) { + if (!this.has('general')) { + return false; + } + if (!this.get('general').has('restaurant_id')) { + return false; + } + return this.get('general').get('restaurant_id') == id; + } +}); diff --git a/utilitybelt/core/models/Order2.js b/utilitybelt/core/models/Order2.js new file mode 100644 index 0000000..8bf1ed4 --- /dev/null +++ b/utilitybelt/core/models/Order2.js @@ -0,0 +1,869 @@ +/** + * Order Model, represents an user order, related to sections + * @see http://mockapi.lieferheld.de/doc/reference/users/user_orders_details.html + */ + +function sync(method, model, options) { + var me = this; + var oldSuccess = options.success; + var modelSet = false; + options.success = function(model, res, xhr){ + var delivery_address = model.delivery_address; + if (delivery_address) { + var address = delivery_address.address; + var storedAddress = me.get("delivery_address").get("address"); + if ( storedAddress && !_.isEqual(address, storedAddress.toJSON()) ) { + delete model.delivery_address; //don't overwrite the model with outdated server-side address + oldSuccess.apply(this, arguments); //hydrate the model so we can save it with its new address + modelSet = true; + me.save({}, { + success: function(){ + me.trigger('order:changeAddress'); + } + }); + } + } + if (!modelSet) { + oldSuccess.apply(this, arguments); + } + } + core.Model.prototype.sync.apply(this, arguments); + }; + +function zzz() { + + + +return { + + extend: 'core.Model', + + fields: { + 'id': { 'dataType': 'string' }, + 'uri': { 'dataType': 'string' }, + 'number': { 'dataType': 'string' }, + // 'coupon': { 'dataType': '', 'default': '' }, + // 'delivery_address': { 'dataType': '', 'default': '' }, + // 'order_price_details': { 'dataType': '', 'default': '' }, + 'delivery_time': { 'dataType': 'datetime' }, + 'estimated_minutes': { 'dataType': 'string' }, + // 'payment': { 'dataType': '', 'default': '' }, + // 'validity': { 'dataType': '', 'default': '' }, + // 'user_location': { 'dataType': '', 'default': '' }, + 'operation': { 'dataType': 'string', 'default': 'validate' }, + 'status': { 'dataType': 'string' } + }, + + relations: [ + { + type: Backbone.HasMany, + key: 'sections', + relatedModel: 'core.models.Section' + }, + { + type: Backbone.HasOne, + key: 'price_details', + relatedModel: 'core.models.OrderPriceDetails', + reverseRelation: { + key: 'order', + includeInJSON: false + } + }, + { + type: Backbone.HasOne, + key: 'general', + relatedModel: 'core.models.OrderGeneral' + }, + { + type: Backbone.HasOne, + key: 'coupon', + relatedModel: 'core.models.Coupon' + }, + { + type: Backbone.HasOne, + key: 'payment', + relatedModel: 'core.models.Payment', + reverseRelation: { + key: 'order' + } + }, + { + type: Backbone.HasOne, + key: 'delivery_address', + relatedModel: 'core.models.DeliveryAddress' + } + ], + + validation: { + + 'delivery_time': [ + { check: 'required', msg: 'please_choose_your_delivery_time' } + ], + 'payment': [ + { check: 'required', msg: 'please_select_your_payment_method' }, + { check: 'notEmpty' } + ], + 'email': [ + { check: 'required', msg: 'email_required' }, + { check: 'notEmpty' }, + { check: 'email', msg: 'valid_email_required' } + ] + }, + + backEndValidationErrorMap: { + 'delivery_time': { + '1': { + attr: 'delivery_time', + msg: 'invalid_pre_order_time' + }, + '2': { + attr: 'delivery_time', + msg: 'restaurant_still_closed' + } + }, + 'delivery_address': { + '1': { + attr: ['zipcode','suburb'], + msg: 'invalid_zipcode' + }, + '2': { + attr: 'city', + msg: 'city_not_specified' + }, + '3': { + attr: 'street_name', + msg: 'street_name_not_specified' + }, + '4': { + attr: 'street_number', + msg: 'street_number_required' + }, + '5': { + attr: 'name', + msg: 'name_required' + }, + '6': { + attr: 'lastname', + msg: 'lastname_required' + }, + '7': { + attr: 'phone', + msg: 'phone_required' + }, + '8': { + attr: 'email', + msg: 'email_required' + } + + }, + 'payment_method': { + attr: 'payment_method', + msg: 'payment_method_invalid' + }, + 'generic': { + '615': function(message) { + var matched = message.match(/<(.*)>/g); + switch (matched[0]) { + case "": + var error = { + 'attr': 'email', + 'msg': 'valid_email_required', + 'supressi18n': true + }; + break; + } + return error; + }, + '616': function(message) { + var attrs = message.match(/<([^>]*)>/g); + if (attrs && attrs.length > 0) { + attrs = attrs.join(",").replace(/[&<>]/g, '').split(","); + } else { + attrs = ''; + } + + var error = { + 'attr': attrs, + 'msg' : message.replace(/[&<>]/g, ''), + 'supressi18n': true + } + + return error; + }, + '618': function(message) { + var error = { + 'attr': ['zipcode','suburb'], + 'msg' : message.replace(/[&<>]/g, ''), + 'supressi18n': true + } + return error; + } + } + }, + + /** + * Constructor + * + */ + initialize: function() { + core.models.Order.__super__.initialize.call(this); + if (!this.has('price_details')) { + this.set('price_details', new core.models.OrderPriceDetails()); + } + if (!this.has('payment')) { + this.set('payment', new core.models.Payment()); + } + }, + + /** + * Override the sync method to account for https://devheld.jira.com/browse/RGP-2096 + * Here we force whichever active address is saved client-side back to the server, if it differs from the address currently saved with the order + */ + + /** + * TODO refactor the order_price_details name modificatio in the model parse method. + */ + set: function(key, value, options) { + core.models.Order.__super__.set.call(this, key, value, options); + if (this.has('order_price_details')) { + core.models.Order.__super__.set.call(this, 'price_details', new core.models.OrderPriceDetails(this.get('order_price_details')), {silent: true}); + this.unset('order_price_details'); + } + return this; + }, + + /** + * Returns the resource address so that Backbone is able to sync the instance. + * @see http://mockapi.lieferheld.de/doc/reference/users/user_orders_details.html + */ + url: function() { + var uid = this.get('general').get('user_id'); + if (!uid) { + throw 'missing-user-id'; + } + var url = ['/api/users/', uid, '/orders/']; + if (!this.isNew()) { + url = url.concat([this.id, '/']); + } + return url.join(''); + }, + + /** + * Searches an order using the user id and a restaurant id. + * When such a restaurant is found, the model is then fetched normally + * @param {Object} param The options for the loading, if search was successful + * @see http://mockapi.lieferheld.de/doc/reference/users/user_orders_details.html#general + */ + search: function(options) { + var uid = this.get('general').get('user_id'), + rid = this.get('general').get('restaurant_id'); + if (!uid) { + throw 'missing-user-id'; + } + var base = [ + '/api/users/', uid, '/orders/', + '?fields=general&general:restaurant_id=', + rid, '&status=created' + ]; + this.fetch(_.extend({}, options, { + url: base.join(''), + success: function(order, response) { + var data = response.data[0] || {}; + if (data.id) { + order.set(data); + order.fetch(options); + } else { + order.set(order.previousAttributes()); + if (options.success) { + options.success(order, response); + } + } + } + })); + }, + + /** + * Initiate the Order Checkout process + * @param {Object} formData Checkout Form Data + * @param {Boolean} validateOnly If true, the form is just validated and not submitted. + */ + checkout: function(formData, validateOnly) { + var self = this, + payment; + + if (this.get('status') != 'created') { + this.validationErrors = [{ + 'attr': 'name', + 'msg': 'this_order_has_already_been_submitted' + }]; + this.trigger("error:checkout", this); + return; + } + + this.isCheckout = true; + this.setFormData(formData); + this.validate(); + + if (this.validationErrors && this.validationErrors.length > 0) { + return false; + } + + if(!validateOnly){ + this.updateUser(this.user); + this.handleAddress(); + } + }, + + /** + * Updates the model reading the given form data from the Checkout page + * @param {Object} formData The serialized form data (see core.views.Form.getValues) + */ + setFormData: function(formData){ + this.formData = formData; + this.setData(); + }, + + /** + * Delivery Address Handler + * + */ + handleAddress: function() { + var deliveryAddress, address, order; + + order = this; + + deliveryAddress = this.get('delivery_address'); + if (deliveryAddress) { + address = deliveryAddress.get('address'); + + if (!address) { + throw new Error('order.handleAddress(): Address model not found.'); + } + + if (address && _.values(address.attributes).length > 0) { + + var locations = new core.collections.Locations(); + + locations.setUrlParams({ + searchLocation: address.get("street_name") + " " + address.get("suburb") + " " + address.get("zipcode") + }); + + locations.fetch({ + success: function(locations) { + var addresses = new core.collections.DeliveryAddress(), + normalisedAddressParts = locations.models[0].get('address'); + + addresses.setUrlParams({ + 'user_id': order.get('general').get('user_id') + }); + + addresses.on('noMatch', function() { + deliveryAddressAddress = deliveryAddress.get('address'); + deliveryAddress.post = true; + + deliveryAddressAddress.set({ + suburb: normalisedAddressParts.suburb, + city: normalisedAddressParts.city, + state: normalisedAddressParts.state + },{silent: true}); + + deliveryAddress.save(deliveryAddressAddress, { + success: function(model, response) { + if (response.errors && response.errors.length > 0) { + if (!_.isArray(order.validationErrors)) { + order.validationErrors = []; + } + order.parseBackendValidationErrors(response); + } else { + order.set({ + 'delivery_address': deliveryAddress + }); + order.checkoutWithAddress(); + } + }, + error: function() { + order.validationErrors = [{ + attr: '', + msg: 'something_has_gone_wrong' + }]; + order.trigger("error:checkout", order); + throw new Error('order.handleAddress(): Could not save delivery address'); + } + }); + }); + + addresses.on('match multipleMatches', function(matches) { + deliveryAddress = _.isArray(matches) ? matches[0] : matches; + order.set({ + 'delivery_address': deliveryAddress + }, { + silent: true + }); + order.checkoutWithAddress(); + }); + + addresses.search(address, { + error: function() { + order.validationErrors = [{ + attr: '', + msg: 'something_has_gone_wrong' + }]; + order.trigger("error:checkout", order); + throw new Error('order.handleAddress(): Could not fetch addresses'); + } + }); + }, + error: function() { + order.validationErrors = [{ + attr: '', + msg: 'something_has_gone_wrong' + }]; + order.trigger("error:checkout", order); + throw new Error('order.handleAddress(): Could not find location'); + } + }); + } + } + }, + + /** + * Set Data relevant to order Checkout + * + */ + setData: function() { + this.setPayment(); + this.setDeliveryTime(); + this.setDeliveryAddress(); + this.set({ + 'terms': this.formData.terms, + 'email': this.formData.email + }, { silent: true }); + }, + + /** + * Set User + * @param {Object} user User Model + */ + setUser: function(user) { + this.user = user; + this.get("general").set({ 'user_id': user.id }); + }, + + /** + * Set Delivery Time + * + */ + setDeliveryTime: function() { + var deliveryTimeISO = (this.formData.delivery_time === 'preorder') ? this.formData.preorder_time : ''; + this.set('delivery_time', deliveryTimeISO, {silent: true}); + }, + + /** + * Set Delivery Address + * + */ + setDeliveryAddress: function() { + var address, deliveryAddress; + + address = new core.models.Address(); + address.set({ + 'state': this.formData.state, + 'city': '_', + 'zipcode': this.formData.zipcode, + 'suburb': this.formData.suburb, + 'street_name': this.formData.street_name, + 'street_number': this.formData.street_number, + 'unit_number': this.formData.door, + 'phone': this.formData.phone, + 'name': this.formData.name, + 'lastname': this.formData.lastname, + 'other': this.formData.other, + 'company': this.formData.company, + 'comments': this.formData.comments + }, {silent: true}); + + deliveryAddress = new core.models.DeliveryAddress(); + deliveryAddress.set({ + 'user_id': this.get('general').get('user_id'), + 'address': address + }); + + this.set('delivery_address', deliveryAddress, {silent: true}); + }, + + /** + * Set payment type + * + */ + setPayment: function() { + var payment; + if (this.formData.centralpayment) { + payment = { + 'method': { + id: this.formData.centralpayment + } + }; + } + this.set({'payment': payment},{silent: true}); + }, + + /** + * Save User / Sync with API + * @param {core.models.User} user User Model + */ + updateUser: function(user) { + var order = this; + user.save({ + 'name': this.formData.name, + 'lastname': this.formData.lastname, + 'phone': this.formData.phone, + 'email': this.formData.email + }, { + success: function(model, response) { + if (response.errors && response.errors.length > 0) { + order.parseBackendValidationErrors(response); + return; + } + }, + silent: true + }); + }, + + /** + * Complete Checkout with a alidated Delivery Address + * + */ + checkoutWithAddress: function() { + var order = this; + + this.save({}, { + success: function(model, response) { + var errors = []; + if (response.error) { + order.validationErrors = [{ + 'attr': '', + 'msg': 'something_has_gone_wrong' + }]; + order.trigger("error:checkout", order); + + return; + } + // Check for, parse and display Backend Validation Errors + errors = model.parseBackendValidationErrors(response); + + // If all is well Finalise the order: + if (!errors && response.status && response.status === 'created') { + order.set('operation', 'final'); + order.save({}, { + success: function(model, response) { + errors = model.parseBackendValidationErrors(response); + if (!errors) { + order.finalizeCheckout(response); + } + }, + error: function() { + order.validationErrors = [{ + attr: '', + msg: 'something_has_gone_wrong' + }]; + order.trigger("error:checkout", order); + throw new Error('order.checkoutWithAddress: problem saving (operation:final) order'); + } + }); + } + }, + + error: function(model, errors) { + if (errors.length > 0) { + order.validationErrors = [{ + attr: '', + msg: 'something_has_gone_wrong' + }]; + order.trigger("error:checkout", order); + throw new Error('order.checkoutWithAddress: problem saving (operation:validate) order'); + } + } + }); + }, + + /** + * Finalizes the checkout depending on the payment method. At the end of the payment, redirects to the order confirmation page. + */ + finalizeCheckout: function(order){ + var confirmation_url = core.utils.formatURL('/order_confirmation/') + this.get('id'); + this.trigger('finalized'); + if (this.get('payment').isPaypal()) { + this.trigger('payWithPaypal', order.payment); + } else { + if (order.status && _.indexOf(['success', 'progress', 'paying'], order.status) > -1) { + window.location.href = confirmation_url; + } + } + }, + + parse: function(response, xhr) { + + if (_.has(response, 'operation')) { + delete response.operation; + } + var resp = _.extend({},response); + if (response.delivery_address) { + if (response.delivery_address.address && _.isEmpty(response.delivery_address.address)) { + delete response.delivery_address; + } + } + return response; + }, + + /** + * Parse Backend Validation Errors + * @param {Object} response JSON response object. + */ + parseBackendValidationErrors: function(response) { + var backendValidationErrors = {}, errors = []; + + if (response.validity) { + // Handle Order Model Validations + for (validation in response.validity) { + if (response.validity[validation] === false) { + backendValidationErrors[validation] = response.validity.errors && response.validity.errors[validation] ? response.validity.errors[validation] : validation; + } + } + + for (validationError in backendValidationErrors) { + switch (validationError) { + case 'delivery_time': + case 'delivery_address': + for (key in backendValidationErrors[validationError]) { + var err = this.backEndValidationErrorMap[validationError]; + errors.push(this.backEndValidationErrorMap[validationError][backendValidationErrors[validationError][key]]); + } + break; + case 'payment_method': + errors.push(this.backEndValidationErrorMap.payment_method); + break; + } + } + } + + if (response.errors) { + // Handle generic 'ERROR' reponse, (probably a user/address) + for (error in response.errors) { + errors.push(this.backEndValidationErrorMap.generic[response.errors[error].error_code](response.errors[error].error_message)); + } + } + + if (errors.length > 0) { + this.validationErrors = errors; + this.trigger('error:checkout', this); + return errors; + } else { + return false; + } + }, + + /** + * Overriden save method to allow special handling for new records. + * @see http://documentcloud.github.com/backbone/#Model-save + */ + save: function(key, value, options) { + if (this.isNew()) { + return this.create.apply(this, arguments); + } else { + return core.models.Order.__super__.save.apply(this, arguments); + } + }, + + /** + * Issues a manual Backbone sync call to create the resource. + * As it saves on success, no handlers passed in the options + * are executed on creation success. They are deferred to the + * consecutive save() call. + * @return jqXHR The XHR issued by the sync + */ + create: function(key, value, options) { + var me = this; + var sections = me.get('sections').toJSON(); + return Backbone.sync('create', { + url: me.url(), + toJSON: function() { + return { restaurant_id: me.get('general').get('restaurant_id') }; + } + }, { + success: function(response) { + me.parse(response); + if (me.has('delivery_address')){ + delete response.delivery_address; //if creating a new order, and there is an active address, use that and not the empty address returned by the POST + } + response.sections = sections; + me.set(response); + me.save(key, value, options); + } + }); + }, + + /** + * Adds the operation attribute to the JSON object for sending to the backend. + * @return {Object} JSON representation of the instance + */ + toJSON: function() { + var json = { id: this.id, operation: this.get('operation') }; + + if (this.has('general')) { + json.general = this.get('general').toJSON(); + } + + if (this.has('delivery_address')) { + json.delivery_address = this.get('delivery_address').toJSON(); + } + json = _.extend( + json, + {sections: this.get('sections').toJSON()}, + {delivery_time: this.get('delivery_time')}, + {delivery_address: this.get('delivery_address')} + ); + + if (this.has('payment')){ + json.payment = this.get('payment').toJSON(); + } + if (this.has('coupon')){ + json.coupon = this.get('coupon').toJSON(); + } + return json; + }, + + /** + * Populates an order model using the content of the provided cart. + * @param cart {core.models.Order} The used to generate the order + * @return this {core.models.Order} this + */ + fromCart: function(cart) { + var sections = this.get('sections'); + sections.reset(); + var section = new core.models.Section(); + sections.add(section); + cart.each(function(cartItem, i) { + // backend does not support multiple section when submitting an order + // var section = sectionFactory(cartItem.get('sectionId')), + var items = section.get('items'), + item = cartItem.get('item'); + item.set('quantity', cartItem.get('quantity'), {silent: true}); + items.add(item); + }); + return this; + }, + + /** + * Generates a cart based on an order. + * @return {core.collections.Cart} A cart having the order items + */ + toCart: function(existingCart) { + var cart = existingCart || new core.collections.Cart(); + cart.order = this; + this.get('sections').each(function(section) { + section.get('items').each(function(item) { + cart.add(item, { quantity: item.get('quantity') }); + }); + }); + return cart; + }, + + changeOwner: function(user, options) { + this.isCheckout = false; + this.set({ + operation: this.fields.operation['default'] + }, { silent: true }); + if (!this.ownedBy(user)) { + this.setUser(user); + this.unset('id'); + this.save(null, options); + } else if (options.success) { + options.success(this); + } + }, + + ownedBy: function(user) { + return this.get("general").get('user_id') == user.id; + }, + + /** + * Resets the order's coupon to empty and udpates the price_details accordingly + */ + resetCoupon: function(){ + this.set("coupon", { + code: "" + }); + var price_details = this.get('price_details'); + price_details.set('coupon_fee', 0); + this.set('price_details', price_details); + this.trigger('change:price_details'); + }, + + /** + * Front End Validations + * Override mixed in validate method to accomodate different levels of validation + * @param {Array} attributes The list of attributes to validate (defaults to all attributes) + * @param {Object} options A hash that can hold the scenario information + * @return {mixed} false if no validation error, an array of validation errors otherwise + */ + validate: function(attributes, options) { + var failures = [], + address, + deliveryAddress, + orderFailures; + + deliveryAddress = this.get('delivery_address'); + if (deliveryAddress && deliveryAddress.self) { + + address = deliveryAddress.get('address'); + + if (this.isCheckout) { + addressFailures = address.validate(); + + if(addressFailures.length > 0) { + failures = failures.concat(addressFailures); + } + + orderFailures = core.models.Order.__super__.validate.call(this, attributes, options); + + if (orderFailures.length > 0) { + failures = failures.concat(orderFailures); + } + + this.validationErrors = failures; + this.trigger('error:checkout', this); + return failures.length ? failures : false; + } + } + return false; + }, + + /** + * Sets the order's subtotal + */ + setSubTotal: function(sum) { + this.get('price_details').set('subtotal', sum); + }, + + hasFee: function() { + return this.has('price_details') && this.get('price_details').hasFee(); + }, + + setRestaurant: function(restaurant) { + if (!this.checkRestaurantId(restaurant.get('id'))) { + throw 'wrong-restaurant-for-order'; + } + this.restaurant = restaurant; + }, + + checkRestaurantId: function(id) { + if (!this.has('general')) { + return false; + } + if (!this.get('general').has('restaurant_id')) { + return false; + } + return this.get('general').get('restaurant_id') == id; + } +} +} diff --git a/utilitybelt/core/models/OrderGeneral.js b/utilitybelt/core/models/OrderGeneral.js new file mode 100644 index 0000000..0ec718a --- /dev/null +++ b/utilitybelt/core/models/OrderGeneral.js @@ -0,0 +1,42 @@ +/** + * "General" section from the Order Model + * @see http://mockapi.lieferheld.de/doc/reference/users/user_orders_details.html#general + */ +core.define('core.models.OrderGeneral', { + + extend: 'core.Model', + + fields: { + 'created_at': { 'dataType': 'datetime' }, + 'coupon_fee': { 'dataType': 'number' }, + 'delivery_fee': { 'dataType': 'number', 'default': 0 }, + 'min_order_fee': { 'dataType': 'number', 'default': 0 }, + 'price': { 'dataType': 'number', 'default': 0 }, + 'restaurant_id': { 'dataType': 'string' }, + 'restaurant_uri': { 'dataType': 'string' }, + 'submitted_at': { 'dataType': 'datetime' }, + 'arriving_at': { 'dataType': 'datetime' }, + 'user_id': { 'dataType': 'string' }, + 'user_uri': { 'dataType': 'string' } + }, + + validations: [ +// { field: 'user_id', type: 'required', message_key: 'user_id_required' } + ], + + initialize: function() { + core.models.OrderGeneral.__super__.initialize.call(this); + }, + + /** + * Returns a lightweight dumb object representing the instance. + * Stripped down to comply with API and consume less weight. + * @return {Object} A lightweight and dumb representation of the instance + */ + toJSON: function() { + return { + restaurant_id: this.get('restaurant_id'), + user_id: this.get('user_id') + }; + } +}); diff --git a/utilitybelt/core/models/OrderPriceDetails.js b/utilitybelt/core/models/OrderPriceDetails.js new file mode 100644 index 0000000..c952eb5 --- /dev/null +++ b/utilitybelt/core/models/OrderPriceDetails.js @@ -0,0 +1,53 @@ +/** + * Price details section from the Order Model + * @see http://mockapi.lieferheld.de/doc/reference/users/user_orders_details.html#order-price-details + */ +core.define('core.models.OrderPriceDetails', { + extend: 'core.Model', + + fields: { + 'min_order_value': { 'dataType': 'number', 'default': 0 }, + 'subtotal': { 'dataType': 'number', 'default': 0 }, + 'difference': { 'dataType': 'number', 'default': 0 }, + 'delivery_fee': { 'dataType': 'number', 'default': 0 }, + 'price': { 'dataType': 'number', 'default': 0 }, + 'coupon_fee': { 'dataType': 'number', 'default': 0 }, + 'total_price': { 'dataType': 'number', 'default': 0 } + }, + + initialize: function() { + core.models.OrderPriceDetails.__super__.initialize.apply(this, arguments); + }, + + set: function(key, value, options) { + core.models.OrderPriceDetails.__super__.set.call(this, key, value, options); + return this.updatePriceDetails(options); + }, + + /** + * Update price details by re-evaluating and setting the value of computed properties. + * @ref http://mockapi.lieferheld.de/beta/doc/reference/users/user_orders_details.html#order-price-details + */ + updatePriceDetails: function(options) { + var min_order_value = this.get('min_order_value'), + subtotal = this.get('subtotal'), + coupon_fee = this.get('coupon_fee'), + delivery_fee = this.get('delivery_fee'); + + var difference = subtotal < min_order_value ? min_order_value - subtotal : 0; + var price = subtotal + coupon_fee + delivery_fee; + var total_price = price + difference; //this is equal to either price, or min_order_value + core.models.OrderPriceDetails.__super__.set.call(this, { + difference: difference, + price: price, + total_price: total_price + }, options); + return this; + }, + + hasFee: function() { + return this.attributes['delivery_fee'] + || this.attributes['min_order_value'] + || this.attributes['coupon_fee']; + } +}); diff --git a/utilitybelt/core/models/Payment.js b/utilitybelt/core/models/Payment.js new file mode 100644 index 0000000..41474a8 --- /dev/null +++ b/utilitybelt/core/models/Payment.js @@ -0,0 +1,33 @@ +/** + * Payment Model, represents a payment method for an order. + */ +core.define('core.models.Payment', { + + extend: 'core.Model', + + idAttribute: false, + + fields: { + 'gateway': { 'dataType': 'object' }, + 'method': { 'dataType': 'object' } + }, + + relations: [ + { + type: Backbone.HasOne, + key: 'order', + relatedModel: 'core.models.Order', + includeInJSON: false + } + ], + + isPaypal: function(){ + return this.get('method').name == "paypal"; + }, + + toJSON: function(){ + return { + method: this.get('method') + } + } +}); diff --git a/utilitybelt/core/models/Restaurant.js b/utilitybelt/core/models/Restaurant.js new file mode 100644 index 0000000..eca1ba7 --- /dev/null +++ b/utilitybelt/core/models/Restaurant.js @@ -0,0 +1,65 @@ +/** + * Restaurant Model, represents a restaurant. + */ +core.define('core.models.Restaurant', { + extend: 'core.Model', + relations: [{ + type: Backbone.HasMany, + key: 'delivery_fees', + relatedModel: 'core.Model', + collectionType: 'core.collections.DeliveryFee' + }, + { + type: Backbone.HasOne, + key: "address", + relatedModel: "core.models.Address" + }], + + initialize: function() { + core.models.Restaurant.__super__.initialize.call(this); + }, + + /** + * @see: http://mockapi.lieferheld.de/beta/doc/reference/restaurants/restaurants.html#restaurant-details for more details + * + * FOR GERMAN API: + * http://mockapi.lieferheld.de/v2/restaurants/r2/?lat=21.34589&lon=12.456677 + * + * FOR AUSTRALIAN API: + * http://mockapi.lieferheld.de/v2/restaurants/r2/?state=[STATE]&city=[CITY]&zipcode=[ZIPCODE]&suburb=[SUBURB] + */ + url: function() { + var url = ['/api/restaurants/']; + if (!this.isNew()) { + url = url.concat([this.id, '/']); + } + var add = this.get("address"); + if (add != null){ + url = url.concat(["?state=",add.get("state"),"&city=",add.get("city"),"&zipcode=",add.get("zipcode"),"&suburb=",add.get("suburb")]); + } + return url.join(''); + }, + + /** + * Overwritten delivery_fee getter. + * Returns the minimal delivery fee known for the restaurant, defaulting the value to 0. + * + * @return {number} The minimal delivery fee known for the restaurant + */ + getDeliveryFee: function() { + if (!this.has('delivery_fees')) { + return 0; + } + return this.attributes['delivery_fees'].getSmallestAmount(); + }, + + /** + * Overwritten min_order_value getter. + * Returns the min_order_value, defaulting the value to 0. + * + * @return {number} The minimum order value for the restaurant + */ + getMinOrderValue: function() { + return this.attributes['min_order_value'] || 0; + } +}); diff --git a/utilitybelt/core/models/Section.js b/utilitybelt/core/models/Section.js new file mode 100644 index 0000000..da6b2ca --- /dev/null +++ b/utilitybelt/core/models/Section.js @@ -0,0 +1,39 @@ +/** + * Menu's Section Model, represents a section in the menu or user order (http://mockapi.lieferheld.de/doc/reference/users/user_orders_details.html#section) + */ +core.define('core.models.Section', { + + extend: 'core.Model', + + fields: { + 'id': { 'dataType': '', 'default': '' }, + 'name': { 'dataType': '', 'default': '' }, + 'pic': { 'dataType': '', 'default': '' } + }, + + relations: [ + { + type: Backbone.HasMany, + key: 'items', + relatedModel: 'core.models.Item' + } + ], + + validations: [ + ], + + initialize: function() { + }, + + /** + * Returns a lightweight dumb object representing the instance. + * Stripped down to comply with API and consume less weight. + * @return {Object} A lightweight and dumb representation of the instance + */ + toJSON: function() { + return { + id: this.id, + items: this.get('items').toJSON() + }; + } +}); diff --git a/utilitybelt/core/models/User.js b/utilitybelt/core/models/User.js new file mode 100644 index 0000000..aeb5bd1 --- /dev/null +++ b/utilitybelt/core/models/User.js @@ -0,0 +1,168 @@ +/** + * User model. + */ +core.define('core.models.User', { + extend: 'core.Model', + + fields: { + 'id': { 'dataType': 'number' }, + 'name': { 'dataType': 'string' }, + 'last_name': { 'dataType': 'string' }, + 'email': { 'dataType': 'string' }, + 'pwd': { 'dataType': 'string' }, + 'phone': { 'dataType': 'string' }, + 'created_at': { 'dataType': 'string' }, + 'op': { 'dataType': 'string' } + }, + + validation: { + 'email': [ + { check: 'required' }, + { check: 'email' } + ], + 'pwd': [ + { check: 'required' }, + { check: 'notEmpty' } + ] + }, + + initialize: function(attributes, options) { + this.token = false; + }, + + /** + * Authorization function. + * Tries to fetch a user having the user email and password. + */ + authorize: function() { + return this.fetch({ + url: ['/api/authorization/?email=', this.get('email'), '&pwd=', this.get('pwd')].join(''), + success: function(user, response) { user.authorizeGrant.call(user, response); }, + error: function(user, xhr) { user.authorizeDeny.call(user, xhr); } + }); + }, + + /** + * Authorization granted handler. + * Sets the user token and fires a grant event. + * + * @param {Object} response The JSON response + * @trigger authorize:grant (User, token) when the authorization is successful + * @throw error when no token is available + */ + authorizeGrant: function(response) { + if (!response.token) { + throw "authorization-has-no-token"; + } + this.token = response.token; + this.trigger('authorize:grant', this, response.token); + }, + + /** + * Authorization denied handler. + * Resets the user token to false and fires a deny event. + * + * @param {Object} xhr The jqXHR + * @trigger authorize:deny (User, reponse) when the authorization failed + */ + authorizeDeny: function(xhr) { + this.token = false; + this.trigger('authorize:deny', this, jQuery.parseJSON(xhr.responseText)); + }, + + /** + * Tells if the user is authorized. + * + * @return {Boolean} A boolean telling if the user is autorized or not + */ + isAuthorized: function() { + return Boolean(this.token) && this.has('email'); + }, + + /** + * Revokes the authorization. + * + * @trigger authorize:revoke (User) when the authorization is revoked + */ + revoke: function() { + return this.save(null, { + url: '/api/authorization/?op=logout', + success: function(user, response) { user.revokeGrant.call(user, response); }, + // error: function(user, xhr) { user.revokeDeny.call(user, xhr); }, + // rely on server response, do not validate anything + silent: true + }); + }, + + /** + * Authorization revoked handler. + * Resets the user token to false and fires a deny event. + * + * @param {Object} response The JSON response + * @trigger authorize:grant (User) when the authorization was successfully revoked + */ + revokeGrant: function(response) { + this.unset('email', {silent: true}); + this.token = false; + this.trigger('authorize:revoke', this); + }, + + authorizeExpire: function() { + this.unset('id', {silent: true}); + this.token = false; + this.trigger('authorize:expire', this); + }, + + /** + * Turns the user to an anonym one. That means it's temporary authorized + * by the API but does not have credentials. + * When the token or ID is lost, such a user cannot be used anymore. + */ + makeAnonym: function() { + this.save('', { + silent: true, + success: function(user, response) { + user.token = response.token; + user.trigger('sync'); + user.trigger('authorize:anonym', user, response.token); + } + }); + }, + + url: function() { + if (this.has('op')) + return '/api/users/?op=' + this.get("op") + "&email=" + this.get("email"); + else + return '/api/users/' + (this.isNew() ? '' : this.id + '/'); + }, + + /** + * Prepares a settable attribute set from the API JSON. + * @param {Object} response The JSON response from the API + * @param {jqXHR} xhr The jqXHR + */ + parse: function(response, xhr) { + if (response.user && response.user.general) { + var attrs = _.extend({ + id: response.user.id + }, response.user.general); + return attrs; + } + return response; + }, + + toJSON: function() { + var json = { + "name": this.get('name'), + "lastname": this.get('lastname'), + "birthdate": this.get('birthday'), + "phone": this.get('phone'), + "email": this.get('email'), + "op": this.get('op') + }; + if(this.get('pwd')){ + json['pwd'] = this.get('pwd'); + } + return json; + } +}); diff --git a/utilitybelt/core/namespaces.js b/utilitybelt/core/namespaces.js new file mode 100644 index 0000000..19eb1a6 --- /dev/null +++ b/utilitybelt/core/namespaces.js @@ -0,0 +1,13 @@ + +/* core namespaces */ + +var core = {}; + +core.views = {}; +core.utils = {}; +core.mixins = {}; +core.models = {}; +core.collections = {}; +core.templates = {}; +core.messages = {}; +core.locales = {}; diff --git a/utilitybelt/core/templates/Box/base.html b/utilitybelt/core/templates/Box/base.html new file mode 100644 index 0000000..91fc145 --- /dev/null +++ b/utilitybelt/core/templates/Box/base.html @@ -0,0 +1,9 @@ +
      +
      +

      +
      +
      +
      +
      +
      +
      \ No newline at end of file diff --git a/utilitybelt/core/templates/Cart/Cart.html b/utilitybelt/core/templates/Cart/Cart.html new file mode 100644 index 0000000..e15ffdc --- /dev/null +++ b/utilitybelt/core/templates/Cart/Cart.html @@ -0,0 +1,8 @@ +
        +
      + +{% if (!cart.readOnly) { %} + +{{ jsGetText(cart.preorder ? 'preorder' : 'checkout') }} + +{% } %} diff --git a/utilitybelt/core/templates/Cart/Details.html b/utilitybelt/core/templates/Cart/Details.html new file mode 100644 index 0000000..7734b02 --- /dev/null +++ b/utilitybelt/core/templates/Cart/Details.html @@ -0,0 +1,47 @@ +{% if (min_order_value || delivery_fee) { %} +
        +{% } %} + +{% if (min_order_value || delivery_fee) { %} +
      • +
        +
        {{ jsGetText("cart_sub_total") }}
        +

        {{ jsFormatPrice(subtotal) }}

        +
      • +{% } %} + +{% if (min_order_value) { %} +
      • +
        +
        + {{ jsGetText('cart_min_order') }} {{ jsFormatPrice(min_order_value) }}, {{ jsGetText('cart_min_order_diff') }} +
        +

        {{ jsFormatPrice(difference) }}

        +
      • +{% } %} + +{% if (delivery_fee) { %} +
      • +
        +
        {{ jsGetText("cart_delivery_fee") }}
        +

        {{ jsFormatPrice(delivery_fee) }}

        +
      • +{% } %} + +{% if (min_order_value || delivery_fee) { %} +
      +{% } %} + +{% if (coupon_fee) { %} +
    • +
      +
      {{ jsGetText("cart_coupon_fee") }}
      +

      {{ jsFormatPrice(coupon_fee) }}

      +
    • +{% } %} + +
    • +
      +
      {{ jsGetText("cart_sum") }}
      +
      {{ jsFormatPrice(total_price) }}
      +
    • diff --git a/utilitybelt/core/templates/Cart/Item.html b/utilitybelt/core/templates/Cart/Item.html new file mode 100644 index 0000000..e72cd2b --- /dev/null +++ b/utilitybelt/core/templates/Cart/Item.html @@ -0,0 +1,21 @@ +
    • + + {% if (cart.readOnly) { %} + +
      +
      {{ cartItem.quantity }}
      + + {% } else { %} + +
      {{ cartItem.quantity }}
      +
      + + +
      + + {% } %} + + +
      {{ core.utils.formatItem(cartItem.itemName) }}
      +

      {{ cartItem.price }}

      +
    • diff --git a/utilitybelt/core/templates/ExitPoll/ExitPollForm.html b/utilitybelt/core/templates/ExitPoll/ExitPollForm.html new file mode 100644 index 0000000..e56debc --- /dev/null +++ b/utilitybelt/core/templates/ExitPoll/ExitPollForm.html @@ -0,0 +1,15 @@ +
      +

      {{ jsGetText("poll_description") }}

      + +
      + {% _.each(exit_poll, function(val, i) { %} +
      {{ val.get('display') }}
      + {% }); %} +
      +
      + +
      +
      +
      diff --git a/utilitybelt/core/templates/Filter/FilterForm.html b/utilitybelt/core/templates/Filter/FilterForm.html new file mode 100644 index 0000000..3638ffa --- /dev/null +++ b/utilitybelt/core/templates/Filter/FilterForm.html @@ -0,0 +1,72 @@ +
      +
      +
      +
      +
      +

      + +

      +
      + +
      +
      +
      + +
      +
      +
      +
      +
      +

      {{ jsGetText('categories') }}

      +

      {{ jsGetText('options') }}

      +
      +
      +
        + {% _.each(categories, function(category, key, list) { %} +
      • + + +
      • + {% }); %} +
      +
      +
      +
        + {% _.each(options, function(option) { %} +
      • + + +
      • + {% }); %} +
      +
      +
      +
      +
      +
        +
      • + + +
      • +
      +
      +
      +
      + {{ jsGetText('apply_filters') }} +
      +
      +
      +
      +
      +
      +
      \ No newline at end of file diff --git a/utilitybelt/core/templates/Flavors/flavors.html b/utilitybelt/core/templates/Flavors/flavors.html new file mode 100644 index 0000000..cb1eb80 --- /dev/null +++ b/utilitybelt/core/templates/Flavors/flavors.html @@ -0,0 +1,49 @@ +

      + {{ description }} +

      +{% _.each(flavors.items, function(flavorContainer) { %} +
      +
      +
      + {{ flavorContainer.name }} + {{ flavorContainer.flavors.structure == "1" ? " " + jsGetText("flavors_mandatory") : "" }} +
      + + {% _.each(flavorContainer.flavors.items, function(singleFlavor, i) { %} +
      +
      + + +
      +
      + + {% if (i%2==1){ %} +
      + {% } %} + {% }); %} +
      +
      +
      +{% }); %} + +
      + +
      +
      +
      +

      {{ jsGetLocale('currency.symbol') }}

      +

      0.00

      +
      +
      +
      + +
      diff --git a/utilitybelt/core/templates/Form/ValidationErrors.html b/utilitybelt/core/templates/Form/ValidationErrors.html new file mode 100644 index 0000000..4fa3bf4 --- /dev/null +++ b/utilitybelt/core/templates/Form/ValidationErrors.html @@ -0,0 +1,10 @@ +
        +

        Attention!

        + {% _.each(errors, function(error, index) { %} + {% if (error.supressi18n) { %} +
      • {{ error.msg }}
      • + {% } else { %} +
      • {{ jsGetText(error.msg) }}
      • + {% } %} + {% }); %} +
      diff --git a/utilitybelt/core/templates/Lightbox/base.html b/utilitybelt/core/templates/Lightbox/base.html new file mode 100644 index 0000000..ca62933 --- /dev/null +++ b/utilitybelt/core/templates/Lightbox/base.html @@ -0,0 +1,14 @@ + + diff --git a/utilitybelt/core/templates/Lightbox/close.html b/utilitybelt/core/templates/Lightbox/close.html new file mode 100644 index 0000000..1c3f200 --- /dev/null +++ b/utilitybelt/core/templates/Lightbox/close.html @@ -0,0 +1,5 @@ +
      +
      + {{ jsGetText("close") }} +
      +
      \ No newline at end of file diff --git a/utilitybelt/core/templates/Lightbox/small.html b/utilitybelt/core/templates/Lightbox/small.html new file mode 100644 index 0000000..032aa4e --- /dev/null +++ b/utilitybelt/core/templates/Lightbox/small.html @@ -0,0 +1,13 @@ + diff --git a/utilitybelt/core/templates/Login/LoginForm.html b/utilitybelt/core/templates/Login/LoginForm.html new file mode 100644 index 0000000..6c33883 --- /dev/null +++ b/utilitybelt/core/templates/Login/LoginForm.html @@ -0,0 +1,10 @@ +
      +
      + + + {{ jsGetText("login_button") }} +
      +

      + +
      +
      diff --git a/utilitybelt/core/templates/MultipleLocation/MultipleLocation.html b/utilitybelt/core/templates/MultipleLocation/MultipleLocation.html new file mode 100644 index 0000000..0cb54ae --- /dev/null +++ b/utilitybelt/core/templates/MultipleLocation/MultipleLocation.html @@ -0,0 +1,18 @@ + diff --git a/utilitybelt/core/templates/MultipleLocation/MultipleLocationList.html b/utilitybelt/core/templates/MultipleLocation/MultipleLocationList.html new file mode 100644 index 0000000..e3a8674 --- /dev/null +++ b/utilitybelt/core/templates/MultipleLocation/MultipleLocationList.html @@ -0,0 +1,13 @@ +
        + {% + locations.each(function(current, index) { + var address = current.get('address'); + %} +
      • +
        {{ core.utils.formatAddress(address) }}
        +

        + {{ address.zipcode + " " + address.city }} +

        +
      • {% + }); %} +
      diff --git a/utilitybelt/core/templates/PLZ/choose_lightbox.html b/utilitybelt/core/templates/PLZ/choose_lightbox.html new file mode 100644 index 0000000..e32dc8e --- /dev/null +++ b/utilitybelt/core/templates/PLZ/choose_lightbox.html @@ -0,0 +1,26 @@ + diff --git a/utilitybelt/core/templates/PLZ/zip_code_lightbox.html b/utilitybelt/core/templates/PLZ/zip_code_lightbox.html new file mode 100644 index 0000000..f68436e --- /dev/null +++ b/utilitybelt/core/templates/PLZ/zip_code_lightbox.html @@ -0,0 +1,24 @@ + diff --git a/utilitybelt/core/templates/Password/PasswordForm.html b/utilitybelt/core/templates/Password/PasswordForm.html new file mode 100644 index 0000000..7e842c3 --- /dev/null +++ b/utilitybelt/core/templates/Password/PasswordForm.html @@ -0,0 +1,33 @@ +
      + +

      {{ jsGetText('change_email_password') }}
      +

      + +
      +

      + +
      +
      + + +
      +
      +
      + + +
      +
      +
      +
      +
      + + + {{ jsGetText('save') }} +
      +
      +
      + +

      + + {{ jsGetText('settings_saved') }} +

      \ No newline at end of file diff --git a/utilitybelt/core/templates/Payment/payment.html b/utilitybelt/core/templates/Payment/payment.html new file mode 100644 index 0000000..303e713 --- /dev/null +++ b/utilitybelt/core/templates/Payment/payment.html @@ -0,0 +1,91 @@ +
      +
      +
      +
        + {% _.each(payment_methods, function(method){ %} +
      • + + + + +
      • + {% }); %} +
      + +
      + + {% if (selected_payment) { %} +
      +
      +

      {{ jsGetText('payment_note') }}

      +

      + {{ jsGetText("payment_"+selected_payment.name) }} +

      +
      + + {% if(canDisplayCoupon()){ %} +
      +
      + +
      +
      + {{ jsGetText('coupon_question') }} +
      + +
      +
      +
      +
      + + + {% if(validity.coupon && coupon.code ) { %} +
      + + {{jsGetText('coupon_good', core.utils.formatPrice(price_details.coupon_fee)) }} +
      + {% } else if(!validity.coupon && enteredCoupon ) { %} +
      + + {{jsGetText('coupon_bad') }} +
      + {% } %} + +
      +
      + {% } %} +
      + {% } %} +
      +
      +
      +
      +
      The total
      +
      +
      +
      +

      {{ core.utils.formatPrice(price_details.total_price) }} +

      +
      +
      +
      +
      +
      +
      + diff --git a/utilitybelt/core/templates/Search/LocationSearchSingle.html b/utilitybelt/core/templates/Search/LocationSearchSingle.html new file mode 100644 index 0000000..1f5b492 --- /dev/null +++ b/utilitybelt/core/templates/Search/LocationSearchSingle.html @@ -0,0 +1,25 @@ +
      + {% if (wide) { %} +
      +

      {{ jsGetText('search_location_title') }}

      +
      +
      +
      +
      + {% } else { %} +
      +
      +

      {{ jsGetText('search_location_title_small') }}

      +
      +
      + {% } %} +
      +
      + +
      + +
      +
      +
      + Close +
      diff --git a/utilitybelt/core/templates/Throbber/Throbber.html b/utilitybelt/core/templates/Throbber/Throbber.html new file mode 100644 index 0000000..282430c --- /dev/null +++ b/utilitybelt/core/templates/Throbber/Throbber.html @@ -0,0 +1,3 @@ +
      + +
      \ No newline at end of file diff --git a/utilitybelt/core/templates/User/PasswordForgotten.html b/utilitybelt/core/templates/User/PasswordForgotten.html new file mode 100644 index 0000000..c17bb5e --- /dev/null +++ b/utilitybelt/core/templates/User/PasswordForgotten.html @@ -0,0 +1,21 @@ + diff --git a/utilitybelt/core/templates/UserAddress/DeleteConfirm.html b/utilitybelt/core/templates/UserAddress/DeleteConfirm.html new file mode 100644 index 0000000..c08d239 --- /dev/null +++ b/utilitybelt/core/templates/UserAddress/DeleteConfirm.html @@ -0,0 +1,5 @@ +
    • + {{jsGetText('delete_address_confirmation')}} + {{jsGetText('yes')}}     + {{jsGetText('no')}} +
    • diff --git a/utilitybelt/core/templates/UserAddress/UserAddressForm.html b/utilitybelt/core/templates/UserAddress/UserAddressForm.html new file mode 100644 index 0000000..4dbe19b --- /dev/null +++ b/utilitybelt/core/templates/UserAddress/UserAddressForm.html @@ -0,0 +1,91 @@ +

      {{ jsGetText("address_form_comment") }}

      + +
      +
      +
      + + +
      +
      + + +
      +
      +
      +
      +
      + + +
      +
      + + +
      +
      +
      +
      +
      + + +
      +
      +
      +
      +
      + + +
      +
      + + +
      +
      +
      +
      +
      + + +
      +
      +
      +
      +
      + + +
      +
      + + +
      +
      +
      +
      +
      + + +
      +
      +
      +
      +
      + + +
      +
      + + +
      +
      + +
      + +
      + {{ jsGetText("save_address") }} + {{ jsGetText("back_to_list") }} +
      + +
      diff --git a/utilitybelt/core/templates/UserAddress/UserAddresses.html b/utilitybelt/core/templates/UserAddress/UserAddresses.html new file mode 100644 index 0000000..f482768 --- /dev/null +++ b/utilitybelt/core/templates/UserAddress/UserAddresses.html @@ -0,0 +1,19 @@ +
      +
        + + {% _.each(addresses, function(address) { %} +
      • +
        +
        {{ address.name }}
        +
        {{ address.lastname }}
        +
        {{ get_full_address(address) }}
        +
        + + +
        +
      • + {% }); %} + +
      +
      +
      diff --git a/utilitybelt/core/tests/collections/Cart.js b/utilitybelt/core/tests/collections/Cart.js new file mode 100644 index 0000000..91b6278 --- /dev/null +++ b/utilitybelt/core/tests/collections/Cart.js @@ -0,0 +1,225 @@ +describe("core.collections.Cart", function() { + describe("Shopping", function() { + beforeEach(function() { + this.data = { + products: _.clone(tests.collections.Item.records.main), + // subproducts: _.clone(tests.collections.Item.records.sub), + flavors: _.clone(tests.models.Item.records.flavors) + }; + }); + + describe("Fill the cart", function() { + beforeEach(function() { + this.cart = new core.collections.Cart(); + }); + + it("Items can be added", function() { + var p1 = new core.models.Item(this.data.products[0]); + var p2 = new core.models.Item(this.data.products[1]); + p2.set('flavors', new core.models.Flavor(this.data.flavors[0])); + this.cart.add(p1); + expect(this.cart.sum).toBeCloseTo(5); + expect(this.cart.length).toEqual(1); + this.cart.add(p2); + expect(this.cart.sum).toBeCloseTo(25.3); + expect(this.cart.length).toEqual(2); + this.cart.add(p2); + expect(this.cart.sum).toBeCloseTo(45.6); + expect(this.cart.length).toEqual(2); + }); + + it("Items with different flavors are separated", function() { + // make 2 different cart items from the same product + var p1 = new core.models.Item(this.data.products[0]); + var p2 = new core.models.Item(this.data.products[0]); + p2.set('flavors', new core.models.Flavor(this.data.flavors[0])); + this.cart.add(p1); + expect(this.cart.length).toEqual(1); + this.cart.add(p2); + expect(this.cart.length).toEqual(2); + // add a 3rd cart item based on the same product + var p3 = new core.models.Item(this.data.products[0]); + var p3Flavor = this.data.flavors[0]; + p3Flavor.items.pop(); + p3.set('flavors', new core.models.Flavor(p3Flavor)); + this.cart.add(p3); + expect(this.cart.length).toEqual(3); + }); + + it("Adding product triggers the right event", function() { + var cart = this.cart, + p1 = new core.models.Item(this.data.products[0]); + + var spyAdd = sinon.spy(); + cart.bind('item:add', spyAdd); + var spyIncrease = sinon.spy(); + cart.bind('quantity:increase', spyIncrease); + cart.add(p1); + expect(spyAdd.called).toBeTruthy(); + expect(spyIncrease.called).toBeFalsy(); + + var spyArgs = spyAdd.args.shift(); + expect(spyArgs[0].self).toEqual('core.models.CartItem'); + expect(spyArgs[0].id).toEqual(p1.fullID()); + expect(spyArgs[1]).toEqual(1); + }); + + it("Increasing product quantity triggers the right event", function() { + var cart = this.cart, + p1 = new core.models.Item(this.data.products[0]); + cart.add(p1); + + var spyAdd = sinon.spy(); + cart.bind('item:add', spyAdd); + var spyIncrease = sinon.spy(); + cart.bind('quantity:increase', spyIncrease); + cart.add(p1); + expect(spyAdd.called).toBeFalsy(); + expect(spyIncrease.called).toBeTruthy(); + + var spyArgs = spyIncrease.args.shift(); + expect(spyArgs[0].self).toEqual('core.models.CartItem'); + expect(spyArgs[0].id).toEqual(p1.fullID()); + expect(spyArgs[1]).toEqual(1); + }); + + it("Adding product with negative quantity is forbidden", function() { + var cart = this.cart, + p1 = new core.models.Item(this.data.products[0]); + + expect(function() { cart.add(p1, {quantity: -2}); }).toThrow('illegal-quantity'); + }); + }); + + describe("Empty the cart", function() { + beforeEach(function() { + this.cart = new core.collections.Cart(); + var p1 = new core.models.Item(this.data.products[0]); + this.cart.add(p1, {quantity: 3}); + this.product = p1; + }); + + it("Items can be removed", function() { + var cart = this.cart, + p1 = this.product, + p1FullName = p1.fullID(); + expect(cart.sum).toBeCloseTo(5*3); + expect(cart.length).toEqual(1); + cart.remove(p1); + expect(cart.sum).toBeCloseTo(5*2); + expect(cart.length).toEqual(1); + cart.remove(p1, {quantity: 2}); + expect(cart.get(p1FullName)).toBeFalsy(); + expect(function() { cart.remove(p1); }).toThrow('unknown-item'); + }); + + it("Decreasing product quantity triggers the right event", function() { + var cart = this.cart; + var spyDecrease = sinon.spy(); + cart.bind('quantity:decrease', spyDecrease); + var spyRemove = sinon.spy(); + cart.bind('item:remove', spyRemove); + cart.remove(this.product); + expect(spyRemove.called).toBeFalsy(); + expect(spyDecrease.called).toBeTruthy(); + + var spyArgs = spyDecrease.args.shift(); + expect(spyArgs[0].self).toEqual('core.models.CartItem'); + expect(spyArgs[0].id).toEqual(this.product.fullID()); + expect(spyArgs[1]).toEqual(1); + }); + + it("Removing product triggers the right event", function() { + var cart = this.cart; + var spyDecrease = sinon.spy(); + cart.bind('quantity:decrease', spyDecrease); + var spyRemove = sinon.spy(); + cart.bind('item:remove', spyRemove); + cart.remove(this.product, {quantity: 3}); + expect(spyRemove.called).toBeTruthy(); + expect(spyDecrease.called).toBeFalsy(); + + var spyArgs = spyRemove.args.shift(); + expect(spyArgs[0].self).toEqual('core.models.CartItem'); + expect(spyArgs[0].id).toEqual(this.product.fullID()); + expect(spyArgs[1]).toEqual(3); + }); + + it("Removing product with negative quantity is forbidden", function() { + var cart = this.cart, + p1 = this.product; + + expect(function() { cart.remove(p1, {quantity: -2}); }).toThrow('illegal-quantity'); + }); + + it("Remove a product no matter what's its quantity", function() { + var cart = this.cart, + p1 = this.product; + var spyDecrease = sinon.spy(); + cart.bind('quantity:decrease', spyDecrease); + var spyRemove = sinon.spy(); + cart.bind('item:remove', spyRemove); + cart.erase(this.product); + expect(spyRemove.called).toBeTruthy(); + expect(spyDecrease.called).toBeFalsy(); + + var spyArgs = spyRemove.args.shift(); + expect(spyArgs[0].self).toEqual('core.models.CartItem'); + expect(spyArgs[0].id).toEqual(p1.fullID()); + expect(spyArgs[1]).toEqual(3); + + expect(function() { cart.remove(p1); }).toThrow('unknown-item'); + }); + + it("Empty removes all at once", function() { + var cart = this.cart, + p1 = this.product; + cart.empty(); + expect(cart.sum).toEqual(0); + expect(cart.length).toEqual(0); + }); + + it("Emptying an empty cart has no effect", function() { + var cart = new core.collections.Cart(); + cart.empty(); + expect(cart.sum).toEqual(0); + expect(cart.length).toEqual(0); + }); + }); + + describe("Updating cart items by id", function() { + beforeEach(function() { + this.cart = new core.collections.Cart(); + var p1 = new core.models.Item(this.data.products[0]); + this.cart.add(p1, {quantity: 3}); + this.product = p1; + spyOn(this.cart, 'add'); + spyOn(this.cart, 'remove'); + }); + + it("Updating to an unknown item throws an exception", function() { + var cart = this.cart, + p1FullName =this.product.fullID(); + expect(function() { cart.update('Inexisting item ID', 5); }).toThrow('unknown-item'); + expect(cart.add).not.toHaveBeenCalled(); + expect(cart.remove).not.toHaveBeenCalled(); + }); + + it("Updating to a greater quantity calls add()", function() { + var cart = this.cart, + p1FullName =this.product.fullID(); + cart.update(p1FullName, 5); + expect(cart.add).toHaveBeenCalled(); + expect(cart.remove).not.toHaveBeenCalled(); + }); + + it("Updating to a lower quantity calls remove()", function() { + var cart = this.cart, + p1FullName =this.product.fullID(); + cart.update(p1FullName, 1); + expect(cart.add).not.toHaveBeenCalled(); + expect(cart.remove).toHaveBeenCalled(); + }); + }); + }); +}); diff --git a/utilitybelt/core/tests/collections/Collection.js b/utilitybelt/core/tests/collections/Collection.js new file mode 100644 index 0000000..f596205 --- /dev/null +++ b/utilitybelt/core/tests/collections/Collection.js @@ -0,0 +1,106 @@ + +describe("Collection", function() { + + describe("It can be created, initialized and validated", function() { + + it("Creates", function() { + var rec = new core.Collection(); + expect(rec).toBeDefined(); + }); + + it("Can be extended", function() { + + var mycoll_class = core.Collection.extend(); + var coll = new mycoll_class(); + + expect(mycoll_class).toBeDefined(); + expect(coll).toBeDefined(); + + }); + + it("Correctly fills with data", function() { + + core.define('mymdl_class', { + extend: 'core.Model', + fields: { field1: { default: '' } } + }); + var rec = new mymdl_class({ field1: 'bar', id: 1 }); + + core.define('mycoll_class', { + model: mymdl_class, + extend: 'core.Collection' + }); + + var coll = new mycoll_class(); + coll.add(rec); + + expect(coll.get(1).get('field1')).toEqual('bar'); + + }); + + describe("It can work with remote datasource", function() { + + beforeEach(function() { + this.srv = sinon.fakeServer.create(); + this.url = '/api'; + this.headers = {"Content-Type": "application/json"}; + + core.define('mymdl_class', { + extend: 'core.Model', + fields: { field1: { default: '' } }, + url: this.url + }); + + core.define('mycoll_class', { + model: mymdl_class, + extend: 'core.Collection', + url: this.url + }); + + this.coll = new mycoll_class(); + + }); + + afterEach(function() { + this.srv.restore(); + }); + + it("Fetch data with success", function() { + + this.srv.respondWith("GET", this.url, + [200, this.headers, '{"data": [{"id":1, "field1": "bar2"}]}']); + + var callback = sinon.spy(); + + this.coll.fetch({ success: callback }); + + this.srv.respond(); + + expect(callback.called).toBeTruthy(); + + expect(callback.getCall(0).args[0].get(1).get('field1')).toEqual('bar2'); + + }); + + it("Fetch data with error", function() { + + this.srv.respondWith("GET", this.url, + [500, this.headers, '']); + + var callback = sinon.spy(); + + this.coll.fetch({ error: callback }); + + this.srv.respond(); + + expect(callback.called).toBeTruthy(); + + expect(callback.getCall(0).args[1].status).toEqual(500); + + }); + + }); + + }); + +}); \ No newline at end of file diff --git a/utilitybelt/core/tests/collections/ExitPoll.js b/utilitybelt/core/tests/collections/ExitPoll.js new file mode 100644 index 0000000..3d8911f --- /dev/null +++ b/utilitybelt/core/tests/collections/ExitPoll.js @@ -0,0 +1,30 @@ +describe("core.collections.ExitPoll", function() { + beforeEach(function() { + this.collection = new core.collections.ExitPoll(); + }); + + describe('When getShuffledOptions method is called', function(){ + it('Should call a _.shuffle method with the collection models', function(){ + this.shuffleSpy = sinon.spy(_, 'shuffle'); + + this.collection.getShuffledOptions(); + + expect(this.shuffleSpy.calledWith(this.collection.models)).toBeTruthy(); + + _.shuffle.restore(); + }); + }); + + describe('When validate method is called with input data', function(){ + it('Should return false when data object is empty', function(){ + var data = {}; + expect(this.collection.validate(data)).not.toBeTruthy(); + }); + + it('Should return true when data object is not empty', function(){ + var data = { 'a': 'b' }; + expect(this.collection.validate(data)).toBeTruthy(); + }); + }); + +}); diff --git a/utilitybelt/core/tests/collections/GeoLocations.js b/utilitybelt/core/tests/collections/GeoLocations.js new file mode 100644 index 0000000..624b490 --- /dev/null +++ b/utilitybelt/core/tests/collections/GeoLocations.js @@ -0,0 +1,58 @@ + +describe("GeoLocations", function() { + + describe("It can be created and initialized", function() { + + it("Creates", function() { + var loc = new core.collections.GeoLocations(); + expect(loc).toBeDefined(); + }); + + }); + + describe("Behaves as expected", function() { + + beforeEach(function() { + this.srv = sinon.fakeServer.create(); + this.url = /\/api\/geo/; + this.headers = {"Content-Type": "application/json"}; + this.srv.respondWith("GET", this.url, [ + 200, + this.headers, + '{"pagination": {"total_items": 3, "total_pages": 1, "limit": 10, "page": 1, "offset": 0}, "data": [{"geo_area": {"city": "Sydney", "state": "NSW", "zipcode": "2060", "suburb": "Sydney"}}, {"geo_area": {"city": "Sydney", "state": "NSW", "zipcode": "2000", "suburb": "North Sydney"}}, {"geo_area": {"city": "Sydney", "state": "NSW", "zipcode": "2127", "suburb": "Sydney Olympic Park"}}]}' + ]); + }); + + afterEach(function() { + this.srv.restore(); + }); + + + it("Only sets correct URL parameters", function() { + var geoLocations = new core.collections.GeoLocations(); + + expect(function(){ + geoLocations.setUrlParams({ foo: 'bar' }); + }).toThrow(); + }); + + it("Correctly parses data from the API", function(){ + var geoLocations = new core.collections.GeoLocations(); + geoLocations.setUrlParams({ + suburb: "Sydney" + }); + + var goodMatch = false; + geoLocations.fetch({ + success: function(locations){ + var normalisedAddressParts = locations.models[0].get('geo_area'); + goodMatch = normalisedAddressParts['suburb'] == "Sydney"; + } + }); + + this.srv.respond(); + + expect(goodMatch).toBeTruthy(); + }); + }); +}); \ No newline at end of file diff --git a/utilitybelt/core/tests/fixtures/ActiveAddress.html b/utilitybelt/core/tests/fixtures/ActiveAddress.html new file mode 100644 index 0000000..ae0168b --- /dev/null +++ b/utilitybelt/core/tests/fixtures/ActiveAddress.html @@ -0,0 +1,5 @@ +
      +
      +

      Delivery address Berlin change

      +
      +
      \ No newline at end of file diff --git a/utilitybelt/core/tests/fixtures/Item.js b/utilitybelt/core/tests/fixtures/Item.js new file mode 100644 index 0000000..a3758bd --- /dev/null +++ b/utilitybelt/core/tests/fixtures/Item.js @@ -0,0 +1,163 @@ +tests.models.Item = { + records: {} +}; +tests.collections.Item = { + records: {} +}; + +tests.collections.Item.records.main = [ + { + "description": "inkl. 0,15\u20ac Pfand", + "sizes": [{ + "price": 5, + "name": "L" + }], + "pic": "", + "main_item": true, + "sub_item": false, + "id": "mp1", + "name": "Fanta*1,3,5,7 0,5L " + }, { + "description": "inkl. 0,15\u20ac Pfand", + "sizes": [{ + "price": 20, + "name": "XL" + }], + "pic": "", + "main_item": true, + "sub_item": false, + "id": "mp2", + "name": "Sprite*1,2,3,5,7 0,5L" + } +]; + +tests.collections.Item.records.sub = [ + { + "description": "", + "sizes": [{ + "price": 0.10, + "name": "sosmall" + }], + "pic": "", + "main_item": false, + "sub_item": true, + "id": "sp1", + "name": "Bombastic " + }, { + "description": "", + "sizes": [{ + "price": 0.20, + "name": "sohuge" + }], + "pic": "", + "main_item": false, + "sub_item": true, + "id": "sp2", + "name": "Deluxe " + } +]; + +tests.models.Item.records.flavors = [{ + "items": tests.collections.Item.records.sub, + "id": "f1", + "structure": "1" +}]; + +tests.models.ItemWithNestedFlavors = { + "flavors": { + "items": [{ + "flavors": { + "items": [{ + "description": "", + "sizes": [{ + "price": 0.60, + "name": "normal" + }], + "pic": "", + "main_item": false, + "sub_item": true, + "id": "1131061", + "name": "Balsamico" + }, { + "description": "", + "sizes": [{ + "price": 0.60, + "name": "normal" + }], + "pic": "", + "main_item": false, + "sub_item": true, + "id": "1131062", + "name": "Caesar*1" + }], + "id": "76666", + "structure": "0" + }, + "description": "", + "sizes": [], + "pic": "", + "main_item": false, + "sub_item": false, + "id": "76666", + "name": "Extra Dressing" + }, { + "flavors": { + "items": [{ + "description": "", + "sizes": [{ + "price": 0.00, + "name": "normal" + }], + "pic": "", + "main_item": false, + "sub_item": true, + "id": "1131059", + "name": "Balsamico" + }, { + "description": "", + "sizes": [{ + "price": 0.00, + "name": "normal" + }], + "pic": "", + "main_item": false, + "sub_item": true, + "id": "1131060", + "name": "Caesar*1 " + }, { + "description": "", + "sizes": [{ + "price": 0.00, + "name": "normal" + }], + "pic": "", + "main_item": false, + "sub_item": true, + "id": "1131057", + "name": "ohne Dressing" + }], + "id": "76665", + "structure": "1" + }, + "description": "", + "sizes": [], + "pic": "", + "main_item": false, + "sub_item": false, + "id": "76665", + "name": "Dressing" + }], + "id": "1633809", + "structure": "-1" + }, + "description": "und Parmesan (inkl. 1 Dressing) ", + "sizes": [{ + "price": 4.50, + "name": "normal" + }], + "pic": "", + "main_item": true, + "sub_item": false, + "id": "1633809", + "name": "Gemischter Salat mit H\u00e4hnchenbrustfilet " +} \ No newline at end of file diff --git a/utilitybelt/core/tests/fixtures/Locations.js b/utilitybelt/core/tests/fixtures/Locations.js new file mode 100644 index 0000000..510d8df --- /dev/null +++ b/utilitybelt/core/tests/fixtures/Locations.js @@ -0,0 +1,49 @@ + +if (typeof (tests) == 'undefined') + tests = {}; + +if (!tests.models) + tests.models = {}; + +tests.models.locations = [ + +{ + "city": "Aberden", + "state": "TAS", + "street_number": "222", + "suburb": "Aberdeen", + "latitude": 52.5120081, + "country": "AU", + "street_name": "Penrose Rd", + "zipcode": "7310", + "longitude": 13.1899752, + "address": "australia aberdeen" +}, +{ + "city": "Aberdeen", + "state": "NSW", + "street_number": "15g", + "suburb": "Aberdeen", + "latitude": 52.5120081, + "country": "AU", + "street_name": "New England Hwy/National Highway", + "zipcode": "2336", + "longitude": 13.3899752, + "address": "australia aberdeen" +}, +{ + "city": "Ultimo", + "state": "TAS", + "street_number": "222", + "suburb": "Ultimo", + "latitude": 52.5120081, + "country": "AU", + "street_name": "Melrose Rd", + "zipcode": "7310", + "longitude": 13.1899752, + "address": "ultimo 2007" +} + +]; + + diff --git a/utilitybelt/core/tests/fixtures/User.js b/utilitybelt/core/tests/fixtures/User.js new file mode 100644 index 0000000..e40ae8d --- /dev/null +++ b/utilitybelt/core/tests/fixtures/User.js @@ -0,0 +1,27 @@ +tests.models.User = { + authorize: {} +}; + +tests.models.User.records = [ + { + 'addresses': '/users/1/delivery_areas', + 'comments': '/users/1/comments', + 'favorites': '/users/1/restaurants', + 'id': 1, + 'uri': '/users/1', + 'orders': '/users/1/orders', + 'general': { + 'birthdate': '1984-12-09', + 'created_at': '2012-04-04T04:04:04Z', + 'email': 'e@mail.com', + 'lastname': 'Dynamite', + 'name': 'Napoleon', + 'phone': '+49152377877878' + } + } +]; + +tests.models.User.authorize.grant = { + 'token': '123abc', + 'user': tests.models.User.records[0] +}; diff --git a/utilitybelt/core/tests/fixtures/UserAddress.js b/utilitybelt/core/tests/fixtures/UserAddress.js new file mode 100644 index 0000000..ad5ac58 --- /dev/null +++ b/utilitybelt/core/tests/fixtures/UserAddress.js @@ -0,0 +1,177 @@ +/* + * Tests related data should be listed here... + * + */ + +tests.models.exampleAPIResponse = { + "user_uri": "http://mockapi.lieferheld.de/users/1/", + "user_id": "1", + "uri": "http://mockapi.lieferheld.de/users/1/addresses/100/", + "id": "100", + "uri_search": "http://mockapi.lieferheld.de/restaurants/?city=berlin&lat=45.23234&lon=37.77234", + "address": { + "city_slug": "berlin", + "door": "1", + "etage": "1", + "lastname": "The last name", + "street_name": "Mohrenstrasse", + "phone": "01717171717", + "comments": "please, call the ....", + "city": "Berlin", + "name": "The name", + "street_number": "60", + "country": "DE", + "zipcode": "10117", + "suburb": "Berlin", + "state": "Germany", + "longitude": 37.77234, + "latitude": 45.232340000000001 + } + }; + +tests.models.exampleModelData = { + user_id: 1, + id: 100, + city_slug: "berlin", + door: "1", + etage: "1", + lastname: "The last name", + street_name: "Mohrenstrasse", + phone: "01717171717", + comments: "please, call the ....", + city: "Berlin", + name: "The name", + street_number: "60", + country: "DE", + zipcode: "10117", + suburb: "Berlin", + state: "Germany", + longitude: 37.77234, + latitude: 45.232340000000001 +}; + +tests.models.exampleModelData2 = { + user_id: 1, + id: 101, + city_slug: "berlin", + door: "1", + etage: "1", + lastname: "The last name", + street_name: "Greifswalder Straße", + phone: "01717171717", + comments: "please, don not call the ....", + city: "Berlin", + name: "The name", + street_number: "164", + country: "DE", + zipcode: "10409", + suburb: "Berlin", + state: "Germany", + longitude: 37.77231 , + latitude: 45.23236 +}; + +tests.pagination.page1 = { + "total_items": 2, + "limit": 10, + "total_pages": 1, + "page": 1, + "offset": 0 + }; + +tests.collections.addresses = [tests.models.exampleModelData, tests.models.exampleModelData2]; + +tests.models.exampleUpdateRequest = { + "id": 100, + "user_id": 1, + "city": "Berlin", + "city_slug": "berlin", + "comments": "please, call the ....", + "country": "DE", + "etage": "1", + "door": "1", + "latitude": 45.232340000000001, + "longitude": 37.77234, + "lastname": "The last name", + "name": "The name", + "phone": "01717171717", + "street_name": "Mohrenstrasse", + "street_number": "60", + //"tags": [], optional for API, not required in UI so far + "zipcode": "10117", + "suburb": "Berlin", + "state": "Germany" +}; + +tests.models.exampleUpdateRequest2 = { + "id": 101, + "city": "Berlin", + "city_slug": "berlin", + "comments": "please, don not call the ....", + "country": "DE", + "etage": "1", + "door": "1", + "latitude": 45.23236, + "longitude": 37.77231, + "lastname": "The last name", + "name": "The name", + "phone": "01717171717", + "street_name": "Greifswalder Straße", + "street_number": "164", + //"tags": [], optional for API, not required in UI so far + "zipcode": "10409", + "suburb": "Berlin", + "state": "Germany" +}; + +tests.collections.exampleAPIResponse = { + "pagination": { + "total_items": 2, + "limit": 10, + "total_pages": 1, + "page": 1, + "offset": 0 + }, + "data": [ + { + "id": 100, + "city": "Berlin", + "city_slug": "berlin", + "comments": "please, call the ....", + "country": "DE", + "etage": "", + "latitude": 45.23234, + "longitude": 37.77234, + "street_name": "Mohrenstrasse", + "street_number": "60", + "zipcode": "10117", + "suburb": "Berlin", + "state": "Germany", + /*TODO: better fix for API inconsistencies, these are here just to make ver 1 work */ + "lastname": "The last name", + "name": "The name", + "phone": "01717171717", + "door":"1" + }, + { + "id": 101, + "city": "Berlin", + "city_slug": "berlin", + "comments": "please, don not call the ....", + "country": "DE", + "etage": "", + "latitude": 45.23236, + "longitude": 37.77231, + "street_name": "Greifswalder Straße", + "street_number": "164", + "zipcode": "10409", + "suburb": "Berlin", + "state": "Germany", + /*TODO: better fix for API inconsistencies, these are here just to make ver 1 work */ + "lastname": "The last name", + "name": "The name", + "phone": "01717171717", + "door":"1" + } + ] +}; \ No newline at end of file diff --git a/utilitybelt/core/tests/mixins/Validate.js b/utilitybelt/core/tests/mixins/Validate.js new file mode 100644 index 0000000..364656f --- /dev/null +++ b/utilitybelt/core/tests/mixins/Validate.js @@ -0,0 +1,137 @@ +/** + * core.mixins.Validate tests + */ +describe("core.mixins.Validate", function() { + beforeEach(function() { + core.define('core.ValidateModel', { + extend: 'Backbone.Model', + mixins: ['core.mixins.Validate'] + }); + }); + + describe('Scheme setup', function() { + it('A single rule can be written without array notation', function() { + var m = new core.ValidateModel(); + m.set('name', ''); + m.validation = { name: {check: 'notEmpty'} }; + var rules = m.getRules(['name'], false); + + var n = new core.ValidateModel(); + n.set('name', ''); + n.validation = { name: [{check: 'notEmpty'}] }; + expect(n.getRules(['name'], false)).toEqual(rules); + }); + + it('Multiple rules can be defined', function() { + var m = new core.ValidateModel(); + m.validation = { name: [{check: 'notEmpty'}, {check: 'required'}] }; + expect(m.getRules().length).toEqual(2); + m.set('name', 'oui'); + expect(m.validationErrors.length).toEqual(0); + m.set('name', ''); + expect(m.validationErrors.length).toEqual(1); + expect(m.validationErrors[0].rule).toEqual('notEmpty'); + m.unset('name'); + expect(m.validationErrors.length).toEqual(1); + expect(m.validationErrors[0].rule).toEqual('required'); + }); + + it('Specifying a scenario fetches related rules', function() { + var m = new core.ValidateModel(); + m.set('name', ''); + m.validation = { name: [{check: 'notEmpty', scenario: 'peach coconut'}, {check: 'required', scenario: 'peach'}] }; + expect(m.getRules(null, 'peach').length).toEqual(2); + expect(m.getRules(null, 'coconut').length).toEqual(1); + }); + + it('Specifying a scenario fetches generic rules', function() { + var m = new core.ValidateModel(); + m.set('name', ''); + m.validation = { name: [{check: 'notEmpty'}, {check: 'required', scenario: 'peach'}] }; + expect(m.getRules(null, 'peach').length).toEqual(2); + }); + }); + + describe('Core rules', function() { + it('Required needs the attribute to be set', function() { + var m = new core.ValidateModel(); + m.validation = { name: {check: 'required'} }; + var errors = m.validate(); + expect(errors.length).toEqual(1); + expect(errors[0].attr).toEqual('name'); + expect('value' in errors[0]).toBeTruthy(); + expect(errors[0].msg).toBeDefined(); + expect(errors[0].rule).toEqual('required'); + expect(m.validationErrors).toBeDefined(); + expect(m.validationErrors).toEqual(errors); + + var n = new core.ValidateModel(); + n.set('name', ''); + expect(n.validate()).toEqual(false); + expect(n.validationErrors).toBeDefined(); + expect(n.validationErrors.length).toEqual(0); + }); + + it('Any rule (but required) considers not set attributes as valid', function() { + var m = new core.ValidateModel(); + m.validation = { name: {check: 'notEmpty'} }; + expect(m.validate()).toEqual(false); + expect(m.validationErrors).toBeDefined(); + expect(m.validationErrors.length).toEqual(0); + }); + + it('NotEmpty needs attribute to not be empty', function() { + var m = new core.ValidateModel(); + m.set('name', ''); + m.validation = { name: {check: 'notEmpty'} }; + var errors = m.validate(); + expect(errors.length).toEqual(1); + expect(errors[0].attr).toEqual('name'); + expect('value' in errors[0]).toBeTruthy(); + expect(errors[0].msg).toBeDefined(); + expect(errors[0].rule).toEqual('notEmpty'); + expect(m.validationErrors).toBeDefined(); + expect(m.validationErrors).toEqual(errors); + + var n = new core.ValidateModel(); + n.set('name', 'aNotEmptyName'); + m.validation = { name: {check: 'notEmpty'} }; + expect(n.validate()).toEqual(false); + expect(n.validationErrors).toBeDefined(); + expect(n.validationErrors.length).toEqual(0); + }); + + it('RegExp tests the attribute with the pattern', function() { + var m = new core.ValidateModel(); + m.set('name', ''); + m.validation = { name: {check: /a/} }; + var errors = m.validate(); + expect(errors.length).toEqual(1); + expect(errors[0].attr).toEqual('name'); + expect('value' in errors[0]).toBeTruthy(); + expect(errors[0].msg).toBeDefined(); + expect(errors[0].rule).toEqual('regexp'); + expect(m.validationErrors).toBeDefined(); + expect(m.validationErrors).toEqual(errors); + + var n = new core.ValidateModel(); + n.set('name', 'aNotEmptyName'); + m.validation = { name: {check: /a/} }; + expect(n.validate()).toEqual(false); + expect(n.validationErrors).toBeDefined(); + expect(n.validationErrors.length).toEqual(0); + }); + }); + + describe('Events triggering', function() { + it('Error events are triggered on validation failures', function() { + var m = new core.ValidateModel(); + m.set('name', ''); + m.validation = { name: {check: 'notEmpty'} }; + var spyError = sinon.spy(); + m.bind('error:name', spyError); + m.validate(); + expect(spyError.called).toBeTruthy(); + }); + }); +}); diff --git a/utilitybelt/core/tests/models/Address.js b/utilitybelt/core/tests/models/Address.js new file mode 100644 index 0000000..1f49deb --- /dev/null +++ b/utilitybelt/core/tests/models/Address.js @@ -0,0 +1,121 @@ +/* + * UserAddressModel Tests + * + */ +describe("core.models.Address", function() { + + describe("When initialized", function() { + + beforeEach(function() { + this.record = new core.models.Address({ user_id: 1, id: 100 }); + }); + + it("UserAddressModel is empty", function() { + expect(this.record).toBeTruthy(); + expect(this.record.attributes).toBeDefined(); + }); + + describe("Basic properties", function() { + it("Extends the core.Model and has its basic methods", function() { + var mdl_class = core.models.Address; + var rec = new mdl_class(); + expect(mdl_class.__super__.self).toEqual('core.Model'); + expect(rec.fetch).toBeDefined(); + expect(rec.save).toBeDefined(); + expect(rec.destroy).toBeDefined(); + }); + }); + +/* + describe('Needs to be initialized with user_id attribute provided', function(){ + it('Has attribute user_id', function(){ + expect(this.record.get('user_id')).toBeDefined(); + }); + it('Throws an exception if user_id is not provided', function(){ + var test = this.record; + + expect(function(){test.requiredParams();}).toThrow(new Error('UserAddressModel user_id not provided')); + }); + + it('Throws an exception if user_id is not integer', function(){ + this.record.set('user_id', 'string'); + var test = this.record; + expect(function(){test.requiredParams();}).toThrow(new Error('UserAddressModel user_id wrong type')); + }); + }); + + }); + + describe('When called with required user_id parameter', function(){ + + beforeEach(function() { + this.record = new website.models.UserAddressModel({user_id:2}); + }); + + describe('Forms url based on user_id and id parameters', function(){ + it('Formats url to fetch data from server properly', function(){ + this.record.set({'id':1, 'user_id':2}); + expect(this.record.url()).toEqual('/users/2/addresses/1/'); + }); + + it('Formats url to send save request for new model to server properly', function(){ + this.record.set({'user_id':2}); + expect(this.record.url()).toEqual('/users/2/addresses/'); + }); + }); +*/ + + describe('Data are parsed appropriately between API and frontend', function(){ + + describe('When data from backend API are fetched', function(){ + + beforeEach(function(){ + this.server = sinon.fakeServer.create(); + this.server.respondWith( + "GET", + "/users/1/addresses/100/", + [ + 200, + {"Content-Type": "application/json"}, + '{"response": ' + JSON.stringify(tests.models.exampleAPIResponse) + '}' + ] + ); + this.record.set({'id':100, 'user_id':1}); + + }); + + it('sends appropriate request to server', function(){ + var request = this.record.fetch(); + expect(this.server.requests.length).toEqual(1); + expect(this.server.requests[0].method).toEqual("GET"); +// expect(this.server.requests[0].url).toEqual("/users/1/addresses/100/"); + }); + + it('parses data to transform them to format appropriate for Backbone model', function(){ + var request = this.record.fetch(); + this.server.respond(); + expect(this.record.attributes).toEqual(tests.models.exampleModelData); + }); + + afterEach(function(){ + this.server.restore(); + }); + }); + + it('Model parse() method returns appropriate response', function(){ + var mockServerResponse = {response: tests.models.exampleAPIResponse}; + var parsed = this.record.parse(mockServerResponse); + expect(parsed).toEqual(tests.models.exampleModelData); + }); + + describe('When model data are sent to backend API', function(){ + it('formats JSON as expected by backend API', function(){ + this.record.set(tests.models.exampleModelData); + var result = this.record.toJSON(); + expect(result).toEqual(tests.models.exampleUpdateRequest); + }); + }); + + }); + }); +}); \ No newline at end of file diff --git a/utilitybelt/core/tests/models/CartItem.js b/utilitybelt/core/tests/models/CartItem.js new file mode 100644 index 0000000..d291a7f --- /dev/null +++ b/utilitybelt/core/tests/models/CartItem.js @@ -0,0 +1,64 @@ +/** + * core.models.CartItem tests + */ +describe("core.models.CartItem", function() { + it('Uses the item full name as ID ', function() { + var item = _.clone(tests.collections.Item.records.main[1]); + item.flavors = tests.models.Item.records.flavors[0]; + item = new core.models.Item(item); + var record = new core.models.CartItem(item, 1); + expect(record.get('id')).toEqual(record.get('item').fullID()); + }); + + describe('Quantity updating', function() { + beforeEach(function() { + var item = _.clone(tests.collections.Item.records.main[1]); + item.flavors = tests.models.Item.records.flavors[0]; + item = new core.models.Item(item); + this.record = new core.models.CartItem(item, 1); + }); + + it('Instantiated with the right quantity', function() { + expect(this.record.get('quantity')).toEqual(1); + }); + + it('Returns the difference between old and new quantity', function() { + var delta = this.record.update(5); + expect(this.record.get('quantity')).toEqual(5); + expect(delta).toEqual(5-1); + }); + + it('Triggers a change event with a different amount', function() { + var spy = sinon.spy(); + this.record.bind('change', spy); + this.record.update(5); + expect(this.record.get('quantity')).toEqual(5); + expect(spy.called).toBeTruthy(); + }); + + it('Does not trigger a change event with a identical amount', function() { + var spy = sinon.spy(); + this.record.bind('change', spy); + this.record.update(1); + expect(this.record.get('quantity')).toEqual(1); + expect(spy.called).toBeFalsy(); + }); + }); + + describe('Price computing', function() { + beforeEach(function() { + var item = _.clone(tests.collections.Item.records.main[1]); + item = new core.models.Item(item); + this.record = new core.models.CartItem(item, 1); + }); + + it('Initial price is correct', function() { + expect(this.record.price()).toBeCloseTo(20, 2); + }); + + it('Price is correct after update', function() { + var delta = this.record.update(5); + expect(this.record.price()).toBeCloseTo(20*5, 2); + }); + }); +}); diff --git a/utilitybelt/core/tests/models/Item.js b/utilitybelt/core/tests/models/Item.js new file mode 100644 index 0000000..e8258fa --- /dev/null +++ b/utilitybelt/core/tests/models/Item.js @@ -0,0 +1,82 @@ +/** + * core.models.CartItem tests + */ +describe("core.models.Item", function() { + describe('Generates a correct full name', function() { + describe('With no flavors', function() { + it('Builds a correct compound ID', function() { + this.record = new core.models.Item(tests.collections.Item.records.main[0]); + expect(this.record.fullID()).toEqual('mp1-L'); + }); + + it('Builds another correct compound ID', function() { + this.record = new core.models.Item(tests.collections.Item.records.main[1]); + expect(this.record.fullID()).toEqual('mp2-XL'); + }); + + it('Builds a correct compound ID without a size', function() { + var record = _.clone(tests.collections.Item.records.main[1]); + record.sizes = null; + this.record = new core.models.Item(record); + expect(this.record.fullID()).toEqual('mp2'); + }); + }); + + describe('With flavors', function() { + it('Builds a correct compound ID', function() { + var record = _.clone(tests.collections.Item.records.main[1]); + record.flavors = tests.models.Item.records.flavors[0]; + record = new core.models.Item(record); + // API is broken and gives a wrong flavor ID. + // TODO Revert code and test if the new test fails + // expect(record.fullID()).toEqual('mp2-XL-f1-sp1-sosmall-sp2-sohuge'); + expect(record.fullID()).toEqual('mp2-XL-sp1-sosmall-sp2-sohuge'); + }); + }); + }); + + describe('Generates a correct price', function() { + describe('With no size', function() { + it('Throw an exception', function() { + var record = _.clone(tests.collections.Item.records.main[1]); + record.sizes = null; + record = new core.models.Item(record); + expect(function() { record.price(); }).toThrow('no-size-available'); + }); + }); + + describe('With no flavors', function() { + it('Price only depends on the size', function() { + var record = new core.models.Item(tests.collections.Item.records.main[0]); + expect(record.price()).toBeCloseTo(5, 2); + record = new core.models.Item(tests.collections.Item.records.main[1]); + expect(record.price()).toBeCloseTo(20, 2); + }); + }); + + describe('With flavors', function() { + it('Price should contain any sub item price', function() { + var record = _.clone(tests.collections.Item.records.main[1]); + record.flavors = tests.models.Item.records.flavors[0]; + record = new core.models.Item(record); + expect(record.price()).toBeCloseTo(20.3, 2); + }); + }); + }); + + describe('Correctly handles flavors', function(){ + var item = new core.models.Item(tests.models.ItemWithNestedFlavors); + + it("Should find an item's sub-items", function(){ + var subItems = item.getSubItems(); + var subItem = subItems.find(function(el){return el.get('id')=='76666';}); + expect(subItem).toBeDefined(); + }); + + it("Should find an item's sub-items (recursively)", function(){ + var subItems = item.getAllSubItems(); + var subItem = subItems.find(function(el){return el.get('id')=='1131059';}); + expect(subItem).toBeDefined(); + }); + }) +}); diff --git a/utilitybelt/core/tests/models/Model.js b/utilitybelt/core/tests/models/Model.js new file mode 100644 index 0000000..b481059 --- /dev/null +++ b/utilitybelt/core/tests/models/Model.js @@ -0,0 +1,188 @@ +/* + * core.Model Tests + * + */ + +describe("Model", function() { + + describe("It can be created, initialized and validated", function() { + + it("Creates", function() { + var rec = new core.Model(); + expect(rec).toBeDefined(); + }); + + it("Can be extended", function() { + + var mymdl_class = core.Model.extend(); + var rec = new mymdl_class(); + + expect(mymdl_class).toBeDefined(); + expect(rec).toBeDefined(); + + }); + + it("Child has core.Model methods", function() { + core.define('core.models.test', {extend:'core.Model'}); + var rec = new core.models.test(); + expect(core.models.test.__super__.self).toEqual('core.Model'); + expect(rec.parent.self).toEqual('core.Model'); + expect(rec.fetch).toBeDefined(); + expect(rec.save).toBeDefined(); + expect(rec.destroy).toBeDefined(); + }); + + it("Correctly fills with data", function() { + + var mymdl_class = core.Model.extend( { + fields: { field1: { default: '' } } + }); + var rec = new mymdl_class({ field1: 'bar' }); + + expect(rec.get('field1')).toEqual('bar'); + + }); + + it("Initialize fields", function() { + + var mymdl_class = core.Model.extend({ + fields: { field1: { default: 'bar' }, field2: {} } + }); + var rec = new mymdl_class(); + + expect(rec.has('field1')).toBeTruthy(); + expect(rec.has('field2')).toBeFalsy(); + + }); + }); + + describe("Mixins can be defined", function() { + beforeEach(function() { + core.mixins.Mix = { mix: function() { return 'core.mixins.Mix';} }; + core.mixins.SubMix = { submix: function() { return 'core.mixins.SubMix';} }; + core.define('core.models.Mix', { + extend: 'Backbone.Model', + mixins: ['core.mixins.Mix'] + }); + }); + + it('Model owns mixin members', function() { + var m = new core.models.Mix(); + expect(m.mix).toBeDefined(); + expect(m.mixins).toEqual(['core.mixins.Mix']); + }); + + it('SubModel owns parent and own mixin members', function() { + core.define('core.models.SubMix', { + extend: 'core.models.Mix', + mixins: ['core.mixins.SubMix'] + }); + var m = new core.models.SubMix(); + expect(m.mix).toBeDefined(); + expect(m.mix()).toEqual('core.mixins.Mix'); + expect(m.submix).toBeDefined(); + expect(m.submix()).toEqual('core.mixins.SubMix'); + expect(m.mixins).toEqual(['core.mixins.Mix', 'core.mixins.SubMix']); + }); + + it('SubModel can overwrite parent mixin members', function() { + core.define('core.models.SubMix', { + extend: 'core.models.Mix', + mixins: ['core.mixins.SubMix'], + mix: function() { return 'overwritten mixin'; } + }); + var m = new core.models.SubMix(); + expect(m.mix).toBeDefined(); + expect(m.mix()).toEqual('overwritten mixin'); + }); + }); + + describe("It can work with remote datasource", function() { + + beforeEach(function() { + this.srv = sinon.fakeServer.create(); + this.url = '/api'; + this.headers = {"Content-Type": "application/json"}; + }); + + afterEach(function() { + this.srv.restore(); + }); + + it("Fetch data with success", function() { + + var mymdl_class = core.Model.extend({ + fields: { field1: { default: 'bar' }, field2: {} } + , url: this.url + }); + + var rec = new mymdl_class({id: 1}); + + this.srv.respondWith("GET", this.url, + [200, this.headers, '{"id":1, "field1": "bar2"}']); + + var callback = sinon.spy(); + + rec.fetch({ success: callback }); + + this.srv.respond(); + + expect(callback.called).toBeTruthy(); + + expect(callback.getCall(0).args[0].attributes.field1).toEqual('bar2'); + + }); + + it("Fetch data with error", function() { + + var mymdl_class = core.Model.extend({ + fields: { field1: { default: 'bar' }, field2: {} } + , url: this.url + }); + + var rec = new mymdl_class({id: 1}); + + this.srv.respondWith("GET", this.url, + [500, this.headers, '{"id":1, "field1": "bar2"}']); + + var callback = sinon.spy(); + + rec.fetch({ error: callback }); + + this.srv.respond(); + + expect(callback.called).toBeTruthy(); + + expect(callback.getCall(0).args[1].status).toEqual(500); + + }); + + it("Save data with success", function() { + + var mymdl_class = core.Model.extend({ + fields: { field1: { default: 'bar' }, field2: {} } + , url: this.url + }); + + var rec = new mymdl_class({id: 1, field1: 'foo'}); +// rec.set({ field1: 'foo2' }); + + this.srv.respondWith("PUT", this.url, + [200, this.headers, '{"id":1, "field1": "foo"}']); + +// rec.fetch(); + + var callback = sinon.spy(); + + rec.save({}, { success: callback }); + + this.srv.respond(); + + expect(callback.called).toBeTruthy(); + expect(callback.getCall(0).args[0].attributes.field1).toEqual('foo'); + + }); + + }); + +}); \ No newline at end of file diff --git a/utilitybelt/core/tests/models/Order.js b/utilitybelt/core/tests/models/Order.js new file mode 100644 index 0000000..8e9b4af --- /dev/null +++ b/utilitybelt/core/tests/models/Order.js @@ -0,0 +1,46 @@ +/** + * Order model tests + */ +describe("core.models.Order", function() { + beforeEach(function() { + this.order = new core.models.Order({ + general: new core.models.OrderGeneral({restaurant_id: '123', user_id: '1234'}), + coupon: new core.models.Coupon({code: ""}), + validity: { + coupon: true + }, + price_details:{ + coupon_fee: 0, + delivery_fee: 0, + difference: 0, + min_order_value: 0, + price: 100, + subtotal: 100 + } + }); + this.record = new core.models.Address(); + }); + + it("Correctly computes price details", function() { + var price_details = this.order.get('price_details'); + expect(price_details).toBeDefined(); + expect(price_details.get('total_price')).toBeDefined(); + expect(price_details.get('total_price')).toEqual(100); + + price_details.set('delivery_fee', 20); + expect(price_details.get('price')).toEqual(120); + + price_details.set('coupon_fee', -5); + expect(price_details.get('price')).toEqual(115); + + price_details.set('min_order_value', 150); + expect(price_details.get('difference')).toEqual(50); + expect(price_details.get('price')).toEqual(115); + expect(price_details.get('total_price')).toEqual(150 + 20 - 5); + + price_details.set('min_order_value', 50); + expect(price_details.get('difference')).toEqual(0); + expect(price_details.get('price')).toEqual(115); + expect(price_details.get('total_price')).toEqual(115); + }); +}); \ No newline at end of file diff --git a/utilitybelt/core/tests/models/User.js b/utilitybelt/core/tests/models/User.js new file mode 100644 index 0000000..dfee3df --- /dev/null +++ b/utilitybelt/core/tests/models/User.js @@ -0,0 +1,102 @@ +/** + * core.models.User tests + */ +describe("core.models.User", function() { + describe('Authentification process', function() { + + beforeEach(function() { + this.record = new core.models.User(); + this.server = sinon.fakeServer.create(); + this.server.respondWith('GET', '/api/authorization/?email=foo@bar.com&pwd=wrong', [ + 618, + {'Content-Type': 'application/json'}, + JSON.stringify({error: 'something is wrong'}) + ]); + this.server.respondWith('GET', '/api/authorization/?email=foo@bar.com&pwd=foobar', [ + 200, + {'Content-Type': 'application/json'}, + JSON.stringify(tests.models.User.authorize.grant) + ]); + }); + + afterEach(function() { + this.server.restore(); + }); + + describe('With valid credentials', function() { + + beforeEach(function() { + this.record = new core.models.User({email: 'foo@bar.com', pwd: 'foobar'}); + }); + + it('Auth succeeds', function() { + expect(this.record.token).toBeFalsy(); + var spyDeny = sinon.spy(), + spyGrant = sinon.spy(); + spyOn(this.record, 'authorizeDeny'); + spyOn(this.record, 'authorizeGrant'); + this.record.authorize(); + this.server.respond(); + expect(this.record.authorizeDeny).not.toHaveBeenCalled(); + expect(this.record.authorizeGrant).toHaveBeenCalled(); + }); + + it('User has a token', function() { + expect(this.record.token).toBeFalsy(); + var spyDeny = sinon.spy(), + spyGrant = sinon.spy(); + this.record.authorize(); + this.server.respond(); + expect(this.record.isAuthorized()).toBeTruthy(); + expect(this.record.token).not.toBeFalsy(); + }); + + it("Auth fires a grant event", function() { + expect(this.record.token).toBeFalsy(); + var spyDeny = sinon.spy(), + spyGrant = sinon.spy(); + this.record.bind('authorize:deny', spyDeny); + this.record.bind('authorize:grant', spyGrant); + this.record.authorize(); + this.server.respond(); + expect(spyDeny.called).toBeFalsy(); + expect(spyGrant.called).toBeTruthy(); + }); + + }); + + describe('With wrong credentials', function() { + + beforeEach(function() { + this.record = new core.models.User({email: 'foo@bar.com', pwd: 'wrong'}); + }); + + it('Auth fails', function() { + expect(this.record.token).toBeFalsy(); + var spyDeny = sinon.spy(), + spyGrant = sinon.spy(); + spyOn(this.record, 'authorizeDeny'); + spyOn(this.record, 'authorizeGrant'); + this.record.authorize(); + this.server.respond(); + expect(this.record.authorizeDeny).toHaveBeenCalled(); + expect(this.record.authorizeGrant).not.toHaveBeenCalled(); + expect(this.record.isAuthorized()).toBeFalsy(); + }); + + it("Auth fires a deny event", function() { + expect(this.record.token).toBeFalsy(); + var spyDeny = sinon.spy(), + spyGrant = sinon.spy(); + this.record.bind('authorize:deny', spyDeny); + this.record.bind('authorize:grant', spyGrant); + this.record.authorize(); + this.server.respond(); + expect(spyDeny.called).toBeTruthy(); + expect(spyGrant.called).toBeFalsy(); + }); + + }); + + }); +}); diff --git a/utilitybelt/core/tests/pages/LoggedIn.js b/utilitybelt/core/tests/pages/LoggedIn.js new file mode 100644 index 0000000..d2a318e --- /dev/null +++ b/utilitybelt/core/tests/pages/LoggedIn.js @@ -0,0 +1,22 @@ +describe("app.LandingPage", function() { + + afterEach(function() { + $('div.lightbox').remove(); + }); + + it("When locationNotFound is triggered, .showError() is called", function() { + var spy = sinon.spy(app.LoggedIn.prototype, 'showError'); + var page = core.instantiate('app.LoggedIn', { user: new core.models.User() }); + page.locationSearchWidget.trigger('locationNotFound'); + expect(spy.calledOnce).toBeTruthy(); + }); + + it("When locationFound is triggered, .locationFound() is called", function() { + var spy = sinon.spy(app.LoggedIn.prototype, 'locationFound'); + var page = core.instantiate('app.LoggedIn', { user: new core.models.User() }); + page.locationSearchWidget.trigger('locationFound'); + expect(spy.calledOnce).toBeTruthy(); + }); + +}) + diff --git a/utilitybelt/core/tests/views/Form/ExitPoll.js b/utilitybelt/core/tests/views/Form/ExitPoll.js new file mode 100644 index 0000000..364cd92 --- /dev/null +++ b/utilitybelt/core/tests/views/Form/ExitPoll.js @@ -0,0 +1,182 @@ +describe("core.views.ExitPollForm", function() { + beforeEach(function() { + this.deliveryAddress = new core.models.Address(); + this.exitPollCollection = new core.collections.ExitPoll(); + }); + + describe("When instantiated", function() { + beforeEach(function() { + this.getShuffledOptionsSpy = sinon.spy(this.exitPollCollection, 'getShuffledOptions'); + this.getTemplateSpy = sinon.spy(core.utils, 'getTemplate'); + + this.view = new core.views.ExitPollForm({ + collection: this.exitPollCollection, + deliveryAddress: this.deliveryAddress + }); + }); + + afterEach(function(){ + this.exitPollCollection.getShuffledOptions.restore(); + core.utils.getTemplate.restore(); + }); + + it("should create an element", function() { + expect(this.view.el.nodeName).toEqual("DIV"); + }); + + it("should call collection.getShuffledOptions()", function(){ + expect(this.getShuffledOptionsSpy.called).toBeTruthy(); + }); + + it("should call core.utils.getTemplate", function(){ + expect(this.getTemplateSpy.called).toBeTruthy(); + }); + }); + + describe("When rendered", function(){ + beforeEach(function() { + this.view = new core.views.ExitPollForm({ + collection: this.exitPollCollection, + deliveryAddress: this.deliveryAddress + }); + + this.logFirstTimeDisplaySpy = sinon.spy(this.view, 'logFirstTimeDisplay'); + + this.view.render(); + }); + + afterEach(function(){ + this.view.logFirstTimeDisplay.restore(); + }); + + it("Should create a form element", function(){ + var $el = $(this.view.el); + this.view.on('render', function() { + expect($el.find('form')).toExist(); + }); + }) ; + + it("Should call logFirstTimeDisplay() method", function(){ + expect(this.logFirstTimeDisplaySpy.called).toBeTruthy(); + }); + }); + + describe("When submit method called", function(){ + beforeEach(function() { + this.view = new core.views.ExitPollForm({ + collection: this.exitPollCollection, + deliveryAddress: this.deliveryAddress + }); + //mock the hide coming from lightbox + this.view.options.hide = function(){}; + }); + + it("Should call collection validate method", function(){ + this.collectionValidateSpy = sinon.spy(this.exitPollCollection, 'validate'); + this.view.submit(); + expect(this.collectionValidateSpy.called).toBeTruthy(); + this.exitPollCollection.validate.restore(); + }); + + describe("Depending on the result of collection validate method", function(){ + it('Should call send() method if collection validate returns true', function(){ + this.sendSpy = sinon.spy(this.view, 'send'); + this.collectionValidateSpy = sinon.stub(this.exitPollCollection, 'validate', function(){ + return true; + }); + this.view.submit(); + expect(this.sendSpy.called).toBeTruthy(); + + this.view.send.restore(); + this.exitPollCollection.validate.restore(); + }); + + it('Should call markInvalid() method if collection validate returns false', function(){ + this.markInvalidSpy = sinon.spy(this.view, 'markInvalid'); + this.collectionValidateSpy = sinon.stub(this.exitPollCollection, 'validate', function(){ + return false; + }); + this.view.submit(); + expect(this.markInvalidSpy.called).toBeTruthy(); + + this.view.markInvalid.restore(); + this.exitPollCollection.validate.restore(); + }); + }); + }) + + describe("When send method called", function(){ + beforeEach(function() { + this.view = new core.views.ExitPollForm({ + collection: this.exitPollCollection, + deliveryAddress: this.deliveryAddress + }); + //mock the hide coming from lightbox + this.view.options.hide = function(){}; + + this.logSendDisplaySpy = sinon.spy(this.view, 'logSendDisplay'); + + this.view.send(); + }); + + afterEach(function(){ + this.view.logSendDisplay.restore(); + }); + + it('Should call the logSendDisplay() method', function(){ + expect(this.logSendDisplaySpy.called).toBeTruthy(); + }); + }); + + describe("When logFirstTimeDisplay method called", function(){ + beforeEach(function() { + this.trackingLoggerSpy = sinon.spy(core.utils.trackingLogger, 'log'); + + this.view = new core.views.ExitPollForm({ + collection: this.exitPollCollection, + deliveryAddress: this.deliveryAddress + }); + + }); + + afterEach(function(){ + core.utils.trackingLogger.log.restore(); + }); + + it('Should call the core.utils.trackingLogger.log method with appropriate data', function(){ + //mock data + var $el = $(this.view.el); + $el.html(''); + this.view.options.deliveryAddress.attributes['suburb'] = 'a'; + this.view.options.deliveryAddress.attributes['city'] = 'b'; + + this.view.logFirstTimeDisplay(); + expect(this.trackingLoggerSpy.calledWith('polls', 'exit_poll_first_find_display', 'a,b', 1)).toBeTruthy(); + }); + }); + + describe("When logSendDisplay method called", function(){ + beforeEach(function() { + this.trackingLoggerSpy = sinon.spy(core.utils.trackingLogger, 'log'); + + this.view = new core.views.ExitPollForm({ + collection: this.exitPollCollection, + deliveryAddress: this.deliveryAddress + }); + }); + + afterEach(function(){ + core.utils.trackingLogger.log.restore(); + }); + + it('Should call the core.utils.trackingLogger.log method with appropriate data', function(){ + var $el = $(this.view.el); + $el.html(''); + this.view.options.deliveryAddress.attributes['suburb'] = 'a'; + this.view.options.deliveryAddress.attributes['city'] = 'b'; + this.view.logSendDisplay(); + expect(this.trackingLoggerSpy.calledWith('polls', 'exit_poll_first_find_submit', 'c', 1)).toBeTruthy(); + + }); + }); +}); diff --git a/utilitybelt/core/tests/views/Form/Filter.js b/utilitybelt/core/tests/views/Form/Filter.js new file mode 100644 index 0000000..c6c39a6 --- /dev/null +++ b/utilitybelt/core/tests/views/Form/Filter.js @@ -0,0 +1,197 @@ +describe("core.views.FilterForm", function() { + + beforeEach(function() { + + $('section.filter').remove(); + $('body').append('
      '); + $.fx.off = true; + spyOn(core.views.FilterForm.prototype, 'render').andCallThrough(); + this.filter = new core.views.FilterForm({ + categories: new core.collections.Category([ + {name: 'pizza'}, + {name: 'fast-food'}, + {name: 'asian'}, + {name: 'sushi'}, + {name: 'indian'}, + {name: 'mediterran'}, + {name: 'oriental'}, + {name: 'gourmet'}, + {name: 'international'} + ]), + options: new core.collections.Option([ + {name: 'online_payment'}, + {name: 'box'} + ]), + el: 'section.filter' + }); + }); + + describe("on instantiation", function() { + it("should load template and call render method", function() { + expect(this.filter.render).toHaveBeenCalled(); + }); + }); + + describe("when rendered", function() { + beforeEach(function() { + $.cookie('showFilter', null); + }); + it("should render categories as form inputs", function() { + expect(this.filter.$('input.category:checkbox').length).toEqual(this.filter.categoriesCollection.length); + }); + it("should render options as form inputs", function() { + expect(this.filter.$('input:checkbox').not('.category, .filter-category-select-all').length).toEqual(this.filter.optionsCollection.length); + }); + it("should replace language placeholders with translated string", function() { + + this.filter.categoriesCollection.each( function(cat) { + expect(this.filter.$('label[for=' + cat.get('name') + ']').text()).toEqual(jsGetText(cat.get('name'))); + }, this); + this.filter.optionsCollection.each( function(opt) { + expect(this.filter.$('label[for="category-' + opt.get('name') + '"]').text()).toEqual(jsGetText(opt.get('name'))); + }, this); + + expect(this.filter.$('.hideshow .label').text()).toEqual(jsGetText('hide_filter')); + expect(this.filter.$('.span-8 b').text()).toEqual(jsGetText('categories')); + expect(this.filter.$('.span-3 b').text()).toEqual(jsGetText('options')); + expect(this.filter.$('label[for=all]').text()).toEqual(jsGetText('select_all')); + }); + + describe("if $.cookie('showFilter) === 'false'", function() { + beforeEach(function() { + $.cookie('showFilter', false); + this.filter = new core.views.FilterForm({ + categories: new core.collections.Category([ + {name: 'pizza'}, + {name: 'fast-food'}, + {name: 'asian'}, + {name: 'sushi'}, + {name: 'indian'}, + {name: 'mediterran'}, + {name: 'oriental'}, + {name: 'gourmet'}, + {name: 'international'} + ]), + options: new core.collections.Option([ + {name: 'online_payment'}, + {name: 'box'} + ]), + el: 'section.filter' + }); + }); + + it("should hide the filter widget", function() { + expect(this.filter.$('.body-filter')).toBeHidden(); + }); + + afterEach(function() { + $.cookie('showFilter', null); + }) + }); + + describe("if $.cookie('showFilter) === 'true'", function() { + beforeEach(function() { + $.cookie('showFilter', true); + this.filter = new core.views.FilterForm({ + categories: new core.collections.Category([ + {name: 'pizza'}, + {name: 'fast-food'}, + {name: 'asian'}, + {name: 'sushi'}, + {name: 'indian'}, + {name: 'mediterran'}, + {name: 'oriental'}, + {name: 'gourmet'}, + {name: 'international'} + ]), + options: new core.collections.Option([ + {name: 'online_payment'}, + {name: 'box'} + ]), + el: 'section.filter' + }); + }); + + it("should hide the filter widget", function() { + expect(this.filter.$('.body-filter')).toBeVisible(); + }); + + afterEach(function() { + $.cookie('showFilter', null); + }) + }); + + }); + + describe("when update button receives click event", function() { + beforeEach(function() { + spyOn(core.views.FilterForm.prototype, "submit"); + this.filter.$("input[type=checkbox]")[0].click(); + this.filter.$("input[type=checkbox]")[1].click(); + this.filter.$("input[type=checkbox]")[2].click(); + this.filter.$("input[type=checkbox]").not('.category, .filter-category-select-all')[0].click(); + this.filter.submit(); + }); + + it("should call the submit method", function() { + expect(this.filter.submit).toHaveBeenCalled(); + }); + + it("getSelected should match checked inputs", function() { + var selectedCategories = [] + selectedOptions = []; + + _.each(this.filter.$("input[type=checkbox].category:checked"), function(input) { + selectedCategories.push($(input).attr('name')); + }); + expect(selectedCategories).toEqual(this.filter.getSelected().categories); + + _.each(this.filter.$("input[type=checkbox]:checked").not(".category, .filter-category-select-all"), function(opt) { + selectedOptions.push($(opt).attr('name')); + }); + expect(selectedOptions).toEqual(this.filter.getSelected().options); + }); + + it("should generate correct URL", function() { + var selectedCategories = [] + selectedOptions = []; + + + var url = this.filter.buildUrl(); + + var params = core.utils.getUrlParams(url); + + // Grab the categories + var catsString = params.categories || ''; + var categories = catsString.split(','); + + // Delete categories from params hash + delete params.categories; + + _.each(this.filter.$("input[type=checkbox].category:checked"), function(input) { + selectedCategories.push($(input).attr('name')); + }); + + expect(selectedCategories).toEqual(categories); + + _.each(this.filter.$("input[type=checkbox]:checked").not(".category, .filter-category-select-all"), function(opt) { + selectedOptions.push($(opt).attr('name')); + }); + + var options = []; + _.each(params, function(param, key) { + options.push(key); + }) + + expect(selectedOptions).toEqual(options); + expect(url).toEqual(window.location.href.split("?")[0] + "?categories=" + selectedCategories.join(',') + '&' + options[0] + '=' + params[options[0]]); + }); + + }); + + afterEach(function() { + $('section.filter').remove(); + }); + + +}); \ No newline at end of file diff --git a/utilitybelt/core/tests/views/Form/UserAddress.js b/utilitybelt/core/tests/views/Form/UserAddress.js new file mode 100644 index 0000000..e5af32c --- /dev/null +++ b/utilitybelt/core/tests/views/Form/UserAddress.js @@ -0,0 +1,75 @@ +describe("core.views.UserAddressForm", function() { + + describe("When instantiated without predefined data", function() { + + beforeEach(function() { + this.view = new core.views.UserAddressForm({ id: 1, user_id: 1 }); + }); + + it("should create an element", function() { + expect(this.view.el.nodeName).toEqual("DIV"); + }); + + it("should assign id, user_id and model", function() { + expect(this.view.id).toEqual(1); + expect(this.view.user_id).toEqual(1); + expect(this.view.model).toBeDefined(); + expect(this.view.model.self).toEqual('core.models.Address'); + }); + + }); + + describe("When rendered", function() { + + beforeEach(function() { + this.view = new core.views.UserAddressForm(); + }); + + it("should render a form element", function() { + var $el = $(this.view.el); + this.view.on('render', function() { + expect($el.find('form')).toExist(); + }) + }); + + it("should render a submit button", function() { + var $el = $(this.view.el); + this.view.on('render', function() { + expect($el.find('a.button')).toExist(); + }) + }); + + }); + + describe('When formSubmit function is called', function() { + + beforeEach(function() { + this.view = new core.views.UserAddressForm({ id: 1, user_id: 1 }); + }); + +/* + it('submits the form', function(){ + var view = this.view; + + waitsFor( function() { + return view.rendered; + }, 'View is not rendered', 1000) + + runs( function() { + spyOn(view, 'formValidate'); + view.$el.find('form .submit').trigger('click'); + expect(view.formValidate).toHaveBeenCalled(); + } ); + + }); +*/ + + it('calls UserAddressModel validate method', function(){ + spyOn(this.view.model, 'validate'); + this.view.formValidate(); + expect(this.view.model.validate).toHaveBeenCalled(); + }); + + }); + +}); diff --git a/utilitybelt/core/tests/views/Lightbox/Flavors.js b/utilitybelt/core/tests/views/Lightbox/Flavors.js new file mode 100644 index 0000000..0ac7e9b --- /dev/null +++ b/utilitybelt/core/tests/views/Lightbox/Flavors.js @@ -0,0 +1,71 @@ +describe("View.FlavorsLightbox", function() { + var flb = new core.views.FlavorsLightbox(); + var flbEl; + var itemModel = new core.models.Item(tests.models.ItemWithNestedFlavors); + var rendered; + + flb.on("render", function(){ + rendered = true; + }); + flb.show(itemModel); + + it("test init", function() { + waitsFor(function(){ + return rendered; + }, "Waiting for Flavorbox to be rendered", 1000); + runs(function(){ + flbEl = flb.$el; + }); + }); + + describe("when clicking on 'Add to Cart'", function(){ + it("Should display an error if a mandatory choice was not provided", function(){ + var flavorsInvalidated; + flb.on("flavorsInvalid", function(){ + flavorsInvalidated = true; + }); + flbEl.find(".button").click(); //starts flavor selection + waitsFor(function(){ + return flavorsInvalidated; + }, "Waiting for flavors selection to be rejected as invalid", 1000); + + runs(function(){ + var invalidSections = flbEl.find(".invalidSection"); + expect(invalidSections.length).toBeGreaterThan(0); + }); + + }); + + it("Should return the models of flavors selected in the Lightbox", function(){ + var chosenFlavors; + var flavorsToCheck = [1131059, 1131062]; + + _.each(flavorsToCheck, function(flavorId){ + flbEl.find('[data-flavor-id]').filter(function(){ + return $(this).data('flavor-id')==flavorId; + }).attr("checked", "checked"); + }); + + flb.on("flavorsChosen", function(chosenFlavorsArg){ + chosenFlavors = chosenFlavorsArg; + }); + flbEl.find(".button").click(); //starts flavor selection + + waitsFor(function(){ + return chosenFlavors; + }, "waiting for the function to return the selected flavors", 1000); + + runs(function(){ + _.each(flavorsToCheck, function(flavorId){ + var flavorItem = chosenFlavors.selectedItemModel.getAllSubItems().find(function(el){ + return el.get('id')==''+flavorId; + }); + expect(flavorItem).toBeDefined(); + }); + + flb.hide(); //cleanup + }); + }); + }); + +}); \ No newline at end of file diff --git a/utilitybelt/core/tests/views/Lightbox/Lightbox.js b/utilitybelt/core/tests/views/Lightbox/Lightbox.js new file mode 100644 index 0000000..309ae30 --- /dev/null +++ b/utilitybelt/core/tests/views/Lightbox/Lightbox.js @@ -0,0 +1,121 @@ +describe("Lightbox", function() { + var view = null; + beforeEach(function() { + view = new core.views.Lightbox(); + }); + afterEach(function() { + if (view != null) { + view.hide(); + view = null; + } + }); + describe("initializing the lightbox", function() { + it("test init", function() { + runs(function() { + view.show(); + }) + waitsFor(function() { + return view.$el.is(":visible"); + }, "Lightbox should be shown", 1000); + }); + }); + + describe("testing that different templates work", function() { + it("test init", function() { + runs(function() { + view = new core.views.Lightbox({ + template : "Lightbox/small.html" + }); + view.show(); + }) + waitsFor(function() { + return view.$el.is(":visible"); + }, "Lightbox should be shown", 1000); + runs(function() { + expect(view.$el.hasClass("lb-small")).toBeTruthy(); + view.hide(); + }); + waitsFor(function() { + return !view.$el.is(":visible"); + }, "Lightbox should be hidden after a while", 1000); + runs(function() { + view = new core.views.Lightbox({ + template : "Lightbox/base.html" + }); + view.show(); + }); + waitsFor(function() { + return view.$el.is(":visible"); + }, "Lightbox should be shown again", 1000); + runs(function() { + expect(view.$el.hasClass("lb-big")).toBeTruthy(); + expect(view.$el.find(".closeLB").length).toBe(0); + }); + }); + }); + + describe("testing that different templates with close button work", function() { + it("different templates small and base", function() { + runs(function() { + view = new core.views.Lightbox({ + template : "Lightbox/small.html", + closeButton : true + }); + view.show(); + }) + waitsFor(function() { + return view.$el.is(":visible"); + }, "Lightbox should be shown", 1000); + runs(function() { + expect(view.$el.hasClass("lb-small")).toBeTruthy(); + expect(view.$el.find(".closeLB").length).toBe(1); + view.hide(); + }); + waitsFor(function() { + return !view.$el.is(":visible"); + }, "Lightbox should be hidden after a while", 1000); + runs(function() { + view = new core.views.Lightbox({ + template : "Lightbox/base.html", + closeButton : true + }); + view.show(); + }); + waitsFor(function() { + return view.$el.is(":visible"); + }, "Lightbox should be shown again", 1000); + runs(function() { + expect(view.$el.hasClass("lb-big")).toBeTruthy(); + expect(view.$el.find(".closeLB").length).toBe(1); + }); + }); + }); + + describe("testing close button", function() { + var spy = null; + it("closing via close button", function() { + runs(function() { + view = new core.views.Lightbox({ + closeButton : true + }); + view.show(); + }) + waitsFor(function() { + return view.$el.is(":visible"); + }, "Lightbox should be shown", 1000); + runs(function() { + expect(view.$el.find(".closeLB").length).toBe(1); + spyOn(view, "hide").andCallThrough(); + view.delegateEvents(); + view.$el.find(".closeLB").trigger("click"); + }); + waitsFor(function() { + return !view.$el.is(":visible"); + }, "Lightbox should be hidden after a while", 1000); + runs(function() { + expect(view.hide).wasCalled(); + }); + }); + }); + +}); diff --git a/utilitybelt/core/tests/views/Lightbox/MultipleLocation.js b/utilitybelt/core/tests/views/Lightbox/MultipleLocation.js new file mode 100644 index 0000000..ffd70f9 --- /dev/null +++ b/utilitybelt/core/tests/views/Lightbox/MultipleLocation.js @@ -0,0 +1,142 @@ +describe("MultipleLocation", function() { + beforeEach(function() { + this.locations = new core.collections.Locations([{ + "uri_search": "http://mockapi.lieferheld.de/restaurants/?city=berlin&lat=45.23234&long=37.77234", + "address": { + "city_slug": "berlin", + "city": "Berlin", + "street_number": "60", + "latitude": 45.232340000000001, + "country": "DE", + "street_name": "Mohrenstrasse", + "zipcode": "10117", + "longitude": 37.77234 + } + }, { + "uri_search": "http://mockapi.lieferheld.de/restaurants/?city=berlin&lat=46.2004&long=37.88774", + "address": { + "city_slug": "berlin", + "city": "Berlin", + "street_number": "59", + "latitude": 46.40000000000002, + "country": "DE", + "street_name": "Mohrenstrasse", + "zipcode": "10117", + "longitude": 37.887740000000001 + } + }, { + "uri_search": "http://mockapi.lieferheld.de/restaurants/?city=berlin&lat=46.2004&long=37.88774", + "address": { + "city_slug": "berlin", + "city": "Berlin", + "street_number": "59", + "latitude": 46.200400000000002, + "country": "DE", + "street_name": "Mohrenstrasse", + "zipcode": "10117", + "longitude": 37.987740000000001 + } + }, { + "uri_search": "http://mockapi.lieferheld.de/restaurants/?city=berlin&lat=46.2004&long=37.88774", + "address": { + "city_slug": "berlin", + "city": "Berlin", + "street_number": "59", + "latitude": 46.400400000000002, + "country": "DE", + "street_name": "Mohrenstrasse", + "zipcode": "10117", + "longitude": 37.987740000000001 + } + }]); + this.mockup = {}; + this.mockup.map = { + hide: function() { + }, + remove: function() { + } + }; + this.mockup.list = { + remove: function() { + } + }; + }); + afterEach(function(){ + if (this.view != null){ + if (this.view.map == null){ + this.view.map = this.mockup.map; + } + if (this.view.list == null){ + this.view.list = this.mockup.list; + } + this.view.hide(); + } + }); + + describe("When instantiated", function() { + beforeEach(function() { + this.view = new core.views.MultipleLocationLightbox(); + }); + it("Correct DOM Element", function() { + expect(this.view.el.nodeName).toEqual("DIV"); + }); + }); + + describe("check if shown at the correct position", function() { + beforeEach(function() { + this.view = new core.views.MultipleLocationLightbox({ locations: this.locations }); + this.view.show(); + this.view.map = this.mockup.map; + this.view.list = this.mockup.list; + }); + afterEach(function() { + this.view.hide(); + }); + it("check position", function() { + var lbWidth = this.view.$el.width(); + expect(lbWidth).toBeGreaterThan(0); + var lbHeight = this.view.$el.height(); + expect(lbHeight).toBeGreaterThan(0); + }); + }); + describe("locations handled correctly", function() { + beforeEach(function() { + this.view = new core.views.MultipleLocationLightbox({ + locations: this.locations + }); + }); + afterEach(function() { + this.view = null; + }); + it("check data is saved locally", function() { + expect(this.view).toBeDefined(); + expect(this.view.locations).toBeDefined(); + expect(this.view.locations.length).toBe(this.locations.length); + }); + }); + describe("window can be opened and also closed again without extra footprint", function() { + it("showing lightbox", function() { + this.view = new core.views.MultipleLocationLightbox( { locations: this.locations } ); + var mySpy = spyOn(this.view, "render").andCallThrough(); + this.view.show(); + expect(this.view.render).toHaveBeenCalled(); + expect(this.view.__shown).toBeTruthy(); + expect(this.view.$el).toBeDefined(); + expect(mySpy.callCount).toBe(1); + }); + it("closing lightbox", function() { + + this.view = new core.views.MultipleLocationLightbox( { locations: this.locations } ); + this.view.map = this.mockup.map; + this.view.list = this.mockup.list; + this.view.show(); + + var mySpy = spyOn(this.view.map, "hide"); + + this.view.hide(); + expect(this.view.map).toBeDefined(); + expect(this.view.__shown).toBeFalsy(); + expect(mySpy).toHaveBeenCalled(); + }); + }); +}); diff --git a/utilitybelt/core/tests/views/Maps/GoogleMaps.js b/utilitybelt/core/tests/views/Maps/GoogleMaps.js new file mode 100644 index 0000000..fbb8a55 --- /dev/null +++ b/utilitybelt/core/tests/views/Maps/GoogleMaps.js @@ -0,0 +1,98 @@ +describe("GoogleMaps", function() { + beforeEach(function() { + this.locations = new core.collections.Locations([{ + "uri_search": "http://mockapi.lieferheld.de/restaurants/?city=berlin&lat=45.23234&long=37.77234", + "address": { + "city_slug": "berlin", + "city": "Berlin", + "street_number": "60", + "latitude": 45.232340000000001, + "country": "DE", + "street_name": "Mohrenstrasse", + "zipcode": "10117", + "longitude": 37.77234 + } + }, { + "uri_search": "http://mockapi.lieferheld.de/restaurants/?city=berlin&lat=46.2004&long=37.88774", + "address": { + "city_slug": "berlin", + "city": "Berlin", + "street_number": "59", + "latitude": 46.40000000000002, + "country": "DE", + "street_name": "Mohrenstrasse", + "zipcode": "10117", + "longitude": 37.887740000000001 + } + }, { + "uri_search": "http://mockapi.lieferheld.de/restaurants/?city=berlin&lat=46.2004&long=37.88774", + "address": { + "city_slug": "berlin", + "city": "Berlin", + "street_number": "59", + "latitude": 46.200400000000002, + "country": "DE", + "street_name": "Mohrenstrasse", + "zipcode": "10117", + "longitude": 37.987740000000001 + } + }, { + "uri_search": "http://mockapi.lieferheld.de/restaurants/?city=berlin&lat=46.2004&long=37.88774", + "address": { + "city_slug": "berlin", + "city": "Berlin", + "street_number": "59", + "latitude": 46.400400000000002, + "country": "DE", + "street_name": "Mohrenstrasse", + "zipcode": "10117", + "longitude": 37.987740000000001 + } + }]); + + }); + + it("successfully loads Google Map in a 3 secs", function() { + + var mapsDomElement = $(document.createElement("DIV")); + mapsDomElement.height("300px"); + mapsDomElement.width("200px"); + mapsDomElement.css({ + "position": "relative", + "left": "0px", + "top": "0px" + }); + mapsDomElement.attr("class", "__maps"); + $('body').append(mapsDomElement); + var me = this; + + me.view = new core.views.Map({ + "locations": this.locations, + "domNode": mapsDomElement + }); + + for(var i = 0; i < 10; i++) { + var mde = mapsDomElement.clone(); + mde.css({ + "left": "0px" + }); + mde.attr("class", "__maps"); + $('body').append(mde); + var m = new core.views.Map({ + "locations": this.locations, + "domNode": mde + }); + + } + + waitsFor(function() { + return me.view.__loaded; + }, "still waiting and nothing happens", 3000); + + runs(function() { + expect(me.view.__loaded).toBeTruthy(); + $('.__maps').remove(); + }) + }); + +}); diff --git a/utilitybelt/core/tests/views/Throbber/Throbber.js b/utilitybelt/core/tests/views/Throbber/Throbber.js new file mode 100644 index 0000000..1b21854 --- /dev/null +++ b/utilitybelt/core/tests/views/Throbber/Throbber.js @@ -0,0 +1,68 @@ +describe("Throbber", function() { + + beforeEach(function() { + this.thb = new core.views.Throbber(); + }) + + afterEach(function() { + this.thb.hide(); + this.thb.remove(); + }) + + describe("base methods", function() { + it("should offer show and hide methods", function() { + expect(typeof this.thb.show).toEqual("function"); + expect(typeof this.thb.hide).toEqual("function"); + }); + }); + describe("testing timing settings", function() { + it("fade in time 200ms", function() { + runs(function() { + elem = this.thb.$el.find("img"); + this.thb.setDelay(0); + this.thb.setFadeInTime(200); + this.thb.show(); + elem = this.thb.$el.find("img"); + }); + waitsFor(function() { + return (elem.css("opacity") == 1); + }, "opacity has to be 1 after 200ms", 201); + runs(function() { + this.thb.hide(); + this.thb.setDelay(200); + this.thb.setFadeInTime(200); + this.thb.show(); + expect(elem.css("opacity") == 1).toBeTruthy(); + }); + waitsFor(function() { + return elem.css("opacity") != 0; + }, "opacity should change after 200ms", 201); + runs(function() { + this.thb.hide(); + expect(elem.css("opacity") == 1).toBeTruthy(); + }) + }) + }); + describe("testing timeout", function() { + it("testing the timeout function: should do a call to the callback function", function() { + var spy; + runs(function() { + spy = jasmine.createSpy(); + this.thb.show(1000, spy); + }); + waitsFor(function() { + return spy.callCount > 0; + }, 1100); + runs(function() { + expect(spy).toHaveBeenCalled(); + spy = jasmine.createSpy(); + this.thb.show(100, spy); + this.thb.hide(); + }); + waits(400); + runs(function() { + expect(spy).not.toHaveBeenCalled(); + }); + }); + }); +}); diff --git a/utilitybelt/core/tests/views/View/ActiveAddress.js b/utilitybelt/core/tests/views/View/ActiveAddress.js new file mode 100644 index 0000000..76b7e4b --- /dev/null +++ b/utilitybelt/core/tests/views/View/ActiveAddress.js @@ -0,0 +1,104 @@ +/* + * ActiveAddress Widget Tests + * + */ + +describe("core.views.ActiveAddress", function() { + + beforeEach(function() { + jasmine.getFixtures().fixturesPath = '../../core/tests/fixtures/'; + loadFixtures('ActiveAddress.html'); + spyOn(core.views.ActiveAddress.prototype, 'hideSearchWidget').andCallThrough(); + spyOn(core.views.ActiveAddress.prototype, 'initLocationSearchWidget').andCallThrough(); + spyOn(core.views.ActiveAddress.prototype, 'showSearchWidget').andCallThrough(); + jQuery.fx.off = true; + }); + + describe("If active_address cookie DOES exist", function() { + + describe("When instantiated", function() { + beforeEach(function() { + core.runtime.persist('active_address', {"suburb": "Barangaroo", "state": "NSW", "zipcode": "2000", "city": "Sydney"}, true); + this.activeAddress = new core.views.ActiveAddress({ + el: 'section.filter', + activeAddress: core.runtime.fetch('active_address', true) + }); + }); + + it("should call initLocationSearchWidget method", function() { + expect(this.activeAddress.initLocationSearchWidget).toHaveBeenCalled(); + }); + + it("should call hideSearchWidget method", function() { + expect(this.activeAddress.hideSearchWidget).toHaveBeenCalled(); + }); + + describe("Hide Location Search widget from UI", function() { + it("should NOT call showSearchWidget method", function() { + expect(this.activeAddress.showSearchWidget).not.toHaveBeenCalled(); + }); + + it("display:none CSS rule should exist", function() { + expect(this.activeAddress.locationSearchWidget.$el.css('display')).toEqual('none'); + }); + + it("a click event on the 'change' link should reveal the Location Search Widget", function() { + this.activeAddress.$('.change').click(); + expect(this.activeAddress.showSearchWidget).toHaveBeenCalled(); + expect(this.activeAddress.locationSearchWidget.$el.css('display')).not.toEqual('none'); + }); + + }); + + afterEach(function() { + core.runtime.destroy('active_address'); + }); + }); + + }); + + describe("If active_address cookie DOES NOT exist", function() { + describe("When instantiated", function() { + + beforeEach(function() { + core.runtime.destroy('active_address'); + this.activeAddress = new core.views.ActiveAddress({ + el: 'section.filter' + }); + }); + + it("should call initLocationSearchWidget method", function() { + expect(this.activeAddress.initLocationSearchWidget).toHaveBeenCalled(); + }); + + it("should call hideSearchWidget method", function() { + expect(this.activeAddress.hideSearchWidget).toHaveBeenCalled(); + }); + + it("should call showSearchWidget method", function() { + expect(this.activeAddress.showSearchWidget).toHaveBeenCalled(); + }); + + describe("Show Location Search widget in UI", function() { + + it("display:none CSS rule should NOT exist", function() { + expect(this.activeAddress.locationSearchWidget.$el.css('display')).not.toEqual('none'); + }); + + describe("triggering a click event on .close", function() { + it("hides the widget from the UI", function() { + expect(this.activeAddress.hideSearchWidget).toHaveBeenCalled(); + expect(this.activeAddress.locationSearchWidget.$el.css('display')).not.toEqual('none'); + }); + }); + + }); + + }); + + afterEach(function() { + core.runtime.destroy('active_address'); + }); + + }); +}); \ No newline at end of file diff --git a/utilitybelt/core/tests/views/View/Cart.js b/utilitybelt/core/tests/views/View/Cart.js new file mode 100644 index 0000000..6580702 --- /dev/null +++ b/utilitybelt/core/tests/views/View/Cart.js @@ -0,0 +1,135 @@ +describe("View.Cart", function() { + + beforeEach(function() { + this.mockEvent = { + currentTarget: $('
    • ').find('a') + }; + this.order = {"status": "created", "delivery_address": {"uri": "", "id": "", "address": {}}, "delivery_time": "", "coupon": {"code": ""}, "uri": "http://staging-api.deliveryhero.com.au:7001/users/3724003/orders/249044/", "user_location": {"latitude": 0.0, "longitude": 0.0}, "number": "", "validity": {"delivery_address": false, "errors": {"delivery_address": [1], "delivery_time": [2]}, "delivery_time": false, "coupon": true, "items": true, "payment_method": true}, "general": {"user_uri": "http://staging-api.deliveryhero.com.au:7001/users/3724003/", "user_id": "3724003", "coupon_fee": 0.0, "restaurant_uri": "http://staging-api.deliveryhero.com.au:7001/restaurants/30/", "min_order_fee": 0.0, "price": 0.0, "delivery_fee": 0.0, "restaurant_id": "30", "arriving_at": "", "created_at": "2012-07-04T17:11:36Z", "submitted_at": ""}, "estimated_minutes": "", "order_price_details": {"coupon_fee": 0.0, "price": 30.0, "delivery_fee": 0.0, "min_order_value": 0.0, "difference": 0.0, "subtotal": 30.0}, "id": "249044", "sections": [{"items": [{"flavors": {"items": [{"flavors": {"items": [{"description": "", "main_item": false, "name": "Garlic Bread", "comments": "", "sub_item": true, "id": "68702", "size": {"price": 0.0, "name": "normal"}}], "id": "0"}, "id": "13540"}, {"flavors": {"items": [{"description": "", "main_item": false, "name": "Napoli ", "comments": "", "sub_item": true, "id": "64917", "size": {"price": 0.0, "name": "normal"}}, {"description": "", "main_item": false, "name": "Texas ", "comments": "", "sub_item": true, "id": "64920", "size": {"price": 0.0, "name": "normal"}}], "id": "0"}, "id": "12836"}, {"flavors": {"items": [{"description": " ", "main_item": false, "name": "Pepsi Max", "comments": "", "sub_item": true, "id": "64138", "size": {"price": 0.0, "name": "normal"}}], "id": "0"}, "id": "12661"}], "id": "0"}, "name": "Deal 1", "description": "2 Large Traditional Pizzas, 1 x 1.25L Drink, 1 Garlic Bread (normal price $43.50)", "main_item": true, "comments": "", "sub_item": false, "size": {"price": 30.0, "name": "normal"}, "id": "103426", "quantity": 1}], "id": "249044"}], "payment": {"method": {"id": "0", "name": "cash"}, "gateway": {}}}; + }); + + describe("Event handling", function() { + beforeEach(function() { + var items = [ + { + "description": "inkl. 0,15\u20ac Pfand", + "sizes": [{"price": 5, "name": "L"}], + "pic": "", + "main_item": true, + "sub_item": false, + "id": "mp1", + "name": "Fanta*1,3,5,7 0,5L " + }, { + "description": "inkl. 0,15\u20ac Pfand", + "sizes": [{"price": 20, "name": "XL"}], + "pic": "", + "main_item": true, + "sub_item": false, + "id": "mp2", + "name": "Pain saucisse" + } + ]; + this.items = []; + var cart = new core.collections.Cart(); + cart.order = new core.models.Order(this.order); + for (var i=0, bound=items.length; i 0; + }, "element created", 1000); + runs(function() { + expect(this.view.__disabled).not.toBeTruthy(); + $(".big_input").submit(); + expect(this.view.__disabled).toBeTruthy(); + }); + waitsFor(function() { + return !this.view.__disabled; + }, "popup has to be removed after 2s", 2100); + runs(function() { + expect(this.view.__disabled).not.toBeTruthy(); + }) + }); + }); + describe("Testing Any Input goes To Collection.fetch", function() { + it("if the collection fetch method is called", function() { + waitsFor(function() { + return this.view != null; + }, "view not created!!!", 1000); + waitsFor(function() { + return $(".big_input").length > 0; + }, "element created", 1000); + runs(function() { + var inputValue = ""; + core.collections.Locations = function() { + this.setUrlParams = function(params) { + inputValue = params.searchLocation; + }; + this.fetch = function() { + } + }; + }); + }); + }); +}); diff --git a/utilitybelt/core/tests/views/View/MultipleLocationList.js b/utilitybelt/core/tests/views/View/MultipleLocationList.js new file mode 100644 index 0000000..6a44454 --- /dev/null +++ b/utilitybelt/core/tests/views/View/MultipleLocationList.js @@ -0,0 +1,99 @@ +describe("MultipleLocationList", function() { + beforeEach(function() { + this.locations = new core.collections.Locations([{ + "uri_search": "http://mockapi.lieferheld.de/restaurants/?city=berlin&lat=45.23234&long=37.77234", + "address": { + "city_slug": "berlin", + "city": "Berlin", + "street_number": "60", + "latitude": 45.232340000000001, + "country": "DE", + "street_name": "Mohrenstrasse", + "zipcode": "10117", + "longitude": 37.77234 + } + }, { + "uri_search": "http://mockapi.lieferheld.de/restaurants/?city=berlin&lat=46.2004&long=37.88774", + "address": { + "city_slug": "berlin", + "city": "Berlin", + "street_number": "59", + "latitude": 46.40000000000002, + "country": "DE", + "street_name": "Mohrenstrasse", + "zipcode": "10117", + "longitude": 37.887740000000001 + } + }, { + "uri_search": "http://mockapi.lieferheld.de/restaurants/?city=berlin&lat=46.2004&long=37.88774", + "address": { + "city_slug": "berlin", + "city": "Berlin", + "street_number": "59", + "latitude": 46.200400000000002, + "country": "DE", + "street_name": "Mohrenstrasse", + "zipcode": "10117", + "longitude": 37.987740000000001 + } + }, { + "uri_search": "http://mockapi.lieferheld.de/restaurants/?city=berlin&lat=46.2004&long=37.88774", + "address": { + "city_slug": "berlin", + "city": "Berlin", + "street_number": "59", + "latitude": 46.400400000000002, + "country": "DE", + "street_name": "Mohrenstrasse", + "zipcode": "10117", + "longitude": 37.987740000000001 + } + }]); + this.mockup = {}; + this.mockup.list = { + remove: function() { + } + }; + }); + afterEach(function() { + if(this.view != null) { + this.view.remove(); + this.view == null; + } + }); + describe("When instantiated", function() { + beforeEach(function() { + this.view = new core.views.MultipleLocationList({ locations: this.locations }); + }); + it("test dom structure", function() { + expect(this.view.el.nodeName).toBe("UL"); + expect(this.view.$el.hasClass("unstyled")).toBeTruthy(); + }); + }); + describe("locations handled correctly", function() { + beforeEach(function() { + this.view = new core.views.MultipleLocationList({ + locations: this.locations + }); + }); + it("showing the same amount of elements", function() { + expect(this.view.el.children.length).toBe(this.locations.length); + }); + }); + describe("show and hide also the list and no extra dom footprint should exist", function() { + beforeEach(function() { + this.view = new core.views.MultipleLocationList({ locations: this.locations }); + }); + it("check that list is shown correctly", function() { + this.view.render(); + expect(this.view.$el.parent().length).toBe(0); + }); + it("check removing from the dom", function() { + this.view.render(); + $('body').append(this.view.el); + expect(this.view.$el.parent().length).toBe(1); + this.view.remove(); + expect(this.view.$el.parent().length).toBe(0); + }); + }); +}); diff --git a/utilitybelt/core/tests/views/View/Payment.js b/utilitybelt/core/tests/views/View/Payment.js new file mode 100644 index 0000000..27f2480 --- /dev/null +++ b/utilitybelt/core/tests/views/View/Payment.js @@ -0,0 +1,139 @@ +describe("View.Payment", function() { + describe("Coupon validation", function() { + beforeEach(function() { + var me = this; + + this.order = new core.models.Order({ + general: new core.models.OrderGeneral({restaurant_id: '123', user_id: '1234'}), + coupon: new core.models.Coupon({code: ""}), + validity: { + coupon: true + }, + price_details: { + coupon_fee: 0, + delivery_fee: 0, + difference: 0, + min_order_value: 0, + price: 100, + subtotal: 100 + } + }); + $("body").append($("
      ").css("display", "none")); + + this.basePrice = 100; + this.view = new core.views.Payment({ + el: '.newpayment', + basePrice: this.basePrice, + payment_methods: [{"id": "0", "name": "cash"}, {"id": "1", "name": "paypal"}], + order: this.order + }); + + //state variables + me.couponValidated = false; + me.viewReady = false; + me.orderLoaded = false; //refactorable + + this.view.on("render", function(herp, derp, jerp){ + this.$("li").eq(1).click(); //preselect a payment method (paypal) + me.viewReady = true; + }); + + this.server = sinon.fakeServer.create(); + var fetchResponse = JSON.stringify({"pagination": {"total_items": 1, "limit": 10, "total_pages": 1, "page": 1, "offset": 0}, "data": [{"general": {"user_uri": "http://develop-api.lieferheld.de/users/46085571/", "user_id": "46085571", "coupon_fee": 0.0, "restaurant_uri": "http://develop-api.lieferheld.de/restaurants/129/", "min_order_fee": 0.0, "price": 0.0, "delivery_fee": 0.0, "restaurant_id": "129", "arriving_at": "", "created_at": "2012-06-13T16:06:24Z", "submitted_at": ""}, "uri": "http://develop-api.lieferheld.de/users/46085571/orders/7772430/", "id": "7772430"}]}); + this.server.respondWith("GET", /.*/, [200, {"Content-Type": "application/json"}, fetchResponse]); + + this.order.search({ + success: function(orderArg, response) { + me.orderLoaded = true; + } + }); + me.server.respond(); + }); + + afterEach(function() { + this.view.remove(); + this.view = null; + $(".newpayment").remove(); + this.server.restore(); + }); + + var testCases = [{ + res: { + validity: { + coupon: true + }, + order_price_details: { + subtotal: 1234, + coupon_fee: -10 //discount + }, + coupon: { + code: "testCode" + } + }, + fieldClass: "valid" + }, + { + res: { + validity: { + coupon: false + } + }, + fieldClass: "invalid" + }]; + _.each(testCases, function(testCase){ + it("Typing a " + testCase.fieldClass+ " coupon code updates the UI correctly", function() { + var me = this; + waitsFor(function(){ + return me.viewReady; + }, "UI to be rendered", 2000); + + waitsFor(function(){ + return me.orderLoaded; + }, "order to be loaded", 2000); + + runs(function(){ + var respJSON = JSON.stringify(testCase.res); + this.server.respondWith("PUT", me.order.url(), [200, {"Content-Type": "application/json"}, respJSON]); + + var couponField = me.view.$el.find(".order_form_coupon_code"); + couponField.val("testcoupon"); + + me.view.on("validateCoupon", function(couponValue){ + me.order.save(); + }); + + //simulate a keyup in the coupon field + var e = jQuery.Event("keypress"); + e.keyCode = 80; // "P" + couponField.trigger(e); + + setTimeout(function(){ + me.server.respond(); + },3500); //accounts for delayed save by QuietModel + + me.view.on("couponValidated", function(){ + me.couponValidated = true; + }); + }); + + waitsFor(function(){ + return me.couponValidated; + }, 5000); + + runs(function(){ + var couponField = me.view.$el.find(".order_form_coupon_code"); + var expectedAmount; + expect(couponField.hasClass(testCase.fieldClass)).toBeTruthy(); + if(testCase.res.validity.coupon){ + expectedAmount = core.utils.formatPrice(testCase.res.order_price_details.subtotal + testCase.res.order_price_details.coupon_fee); + }else{ + expectedAmount = core.utils.formatPrice(me.basePrice); + } + expect(_.str.trim(me.view.$el.find(".payment_total").text())).toEqual(expectedAmount); + }); + + + }); + }); + }); +}); diff --git a/utilitybelt/core/tests/views/View/UserAddressList.js b/utilitybelt/core/tests/views/View/UserAddressList.js new file mode 100644 index 0000000..175ac53 --- /dev/null +++ b/utilitybelt/core/tests/views/View/UserAddressList.js @@ -0,0 +1,124 @@ +/* + * UserAddressCollection Tests + * + */ + +describe("UserAddressListView", function() { + + describe("When instantiated", function() { + + beforeEach(function() { + this.view = new core.views.UserAddressList({user_id: 1}); + }); + + it("should create an element", function() { + expect(this.view.el.nodeName).toEqual("DIV"); + }); + + it("should have a correct class set", function() { + expect($(this.view.el).hasClass('UserAddressList')).toBeTruthy(); + }); + + }); + + describe("When instantiated with collection", function() { + + beforeEach(function() { + this.addr1 = new core.Model(tests.models.address1); + this.addr2 = new core.Model(tests.models.address2); + this.collection = new core.collections.Address([ + this.addr1, this.addr2 + ]); + this.view = new core.views.UserAddressList({ + collection: this.collection, + user_id: 1 + }); + this.collection.bindTo(this.view); + }); + + it("should create a view", function() { + expect(this.view).toBeDefined(); + }); + + it("should have collection set correctly", function() { + expect(this.view.collection.length).toEqual(this.collection.length); + }); + + it("re-renders view after the collection has been updated", function() { + spyOn(this.view, 'render'); + this.view.collection.reset(); + expect(this.view.render).toHaveBeenCalled(); + }); + + it("should display an empty message when collection is empty", function() { + this.view.collection.reset(); + expect(this.view.$el.html()!='').toBeTruthy(); + expect(this.view.$el.html() == jsGetText("no_addresses")).toBeTruthy(); + }); + + it('calls to editAddress method when click and it call to openEditWidget', function() { + var view = this.view; + + spyOn(view, 'editAddress'); + view.delegateEvents(); + + view.render(); + + $($(view.$el.find('a.edit-icon')[0])).trigger('click'); + + expect(this.view.editAddress).toHaveBeenCalled(); + }); + + it('calls to deleteAddressConfirm method when click on delete and it shows the confirm panel', function() { + var view = this.view; + + spyOn(view, 'deleteAddressConfirm'); + view.delegateEvents(); + + view.render(); + + $($(view.$el.find('a.delete-icon')[0])).trigger('click'); + + expect(this.view.deleteAddressConfirm).toHaveBeenCalled(); + }); + + it('after call to deleteAddressConfirm it shows the confirm panel', function() { + this.view.render(); + var target = $($(this.view.$el.find('a.delete-icon')[0])); + target.id = 1; + this.view.deleteAddressConfirm({ target: target }); + expect(this.view.$el.find('.message')).toExist(); + expect(this.view.$el.find('.yes')).toExist(); + expect(this.view.$el.find('.no')).toExist(); + }); + + describe("When address collection changes", function() { + + beforeEach(function() { + spyOn(this.view, 'render'); + this.view.collection.remove(this.addr1); + }); + + it("should update the collection accordingly", function() { + expect(this.view.collection.length).toEqual(1); + expect(this.view.collection.get(this.addr1.id)).toBeFalsy(); + }); + + it("should re-render (call render again)", function() { + this.view.collection.trigger('change'); + expect(this.view.render).toHaveBeenCalled(); + }); + + }); + + describe("When user click on edit icon", function() { + + beforeEach(function() { + this.view.render(); + }); + + }); + + }); + +}); \ No newline at end of file diff --git a/utilitybelt/core/utils/Class.js b/utilitybelt/core/utils/Class.js new file mode 100644 index 0000000..59a3a81 --- /dev/null +++ b/utilitybelt/core/utils/Class.js @@ -0,0 +1,63 @@ +/** + * Core class utils + */ +core.Class = { + define: function(className, definition) { + if (definition.extend) { + core.Class.defineSubClass(className, definition); + } else { + eval(className + ' = definition'); + } + }, + defineSubClass: function(className, definition) { + // ref to parent class + var parent = eval(definition.extend); + // get parent mixins to include in definition + var parentMixins = parent.prototype.mixins || []; + // prepare augmented definition + var fullDef = _.extend({}, definition); + _.each(fullDef.mixins, function(mixin) { + _.extend(fullDef, eval(mixin)); + }); + fullDef.mixins = parentMixins.concat(fullDef.mixins || []); + // set the className and namespace + fullDef.className = className.substring(className.lastIndexOf('.')+1); + fullDef.self = className; + // ref to parent prototype allowing easy parent members calling + fullDef.parent = parent.prototype; + // define the class and insert it in its namespace + var klass = parent.extend(fullDef); + eval(className + ' = klass'); + }, + instantiate: function(className, args) { + var klass = eval(className); + var obj = new klass(args); + return obj; + }, + getClass: function(className) { + eval ('var klass = ' + className); + return klass; + } + /*, + callParent: function(obj) { + + try { + eval('var parent = ' + obj.self + '.__super__'); // TODO: core.callParent(this) -> this.callParent(); + + var fn; + for (var field in obj) { + if (obj[field] == arguments.callee.caller) { + fn = field; + } + } + parent[fn].call(obj, Array.prototype.slice.call(arguments, 1)); + } + catch (ex) { + console.error('Cannot invoke super method', obj.self, ex); + } + }*/ +}; + +core.define = core.Class.define; +core.instantiate = core.Class.instantiate; +//core.callParent = core.Class.callParent; diff --git a/utilitybelt/core/utils/Dom.js b/utilitybelt/core/utils/Dom.js new file mode 100644 index 0000000..c0a7eac --- /dev/null +++ b/utilitybelt/core/utils/Dom.js @@ -0,0 +1,67 @@ +/** + * DOM utils + */ +core.utils.Dom = (function(){ + + var scrollingEl, scrollOffsetEl; + //the 'offset' for a scrolling page should be calculated on "body" in webkit, on "html" on other browsers (http://bit.ly/KmLb4F) + scrollingEl = $(document); + + if($.browser.msie){ + scrollingEl = $(window); // on IE, it should be "window" http://www.quirksmode.org/dom/events/scroll.html + } + if($.browser.webkit){ + scrollOffsetEl = $("body"); + }else{ + scrollOffsetEl = $("html"); + } + + return { + + scrollingEl: scrollingEl, //the element which should be used to compute the scrolling offsets + + /** + * Getter/setter for the top scroll offset. If an argument is passed, the method acts as a setter. + * @param selector A parameter for jQuery() i.e. a selector or DOM node. Scrolls the view port to the corresponding DOM node. + */ + scrollTop: function(selector) { + if(selector===undefined){ + //getter + return scrollOffsetEl.scrollTop(); + }else{ + //setter + var scrollNode = $(selector); + scrollOffsetEl.scrollTop(scrollNode.offset().top) + } + }, + + /** + * Getter/setter for the left scroll offset. If an argument is passed, the method acts as a setter. + * @param selector A parameter for jQuery() i.e. a selector or DOM node. Scrolls the view port to the corresponding DOM node. + */ + scrollLeft: function(selector) { + if(selector===undefined){ + //getter + return scrollOffsetEl.scrollLeft(); + }else{ + //setter + var scrollNode = $(selector); + scrollOffsetEl.scrollLeft(scrollNode.offset().left) + } + }, + + /** + * Returns the viewport's width in pixels (see http://api.jquery.com/width ) + */ + viewWidth : function(){ + return window.innerWidth || document.documentElement.clientWidth; + }, + + /** + * Returns the viewport's height in pixels (see http://api.jquery.com/height ) + */ + viewHeight : function(){ + return window.innerHeight; + } + } +})(); \ No newline at end of file diff --git a/utilitybelt/core/utils/LS.js b/utilitybelt/core/utils/LS.js new file mode 100644 index 0000000..049a6c6 --- /dev/null +++ b/utilitybelt/core/utils/LS.js @@ -0,0 +1,64 @@ + +/* + * LocalStorage simple database-alike layer + */ + +core.LSDB = function() { + + this.prefix = 'db-'; + + this.counter = function(table) { + return this.prefix + table + '-counter'; + }; + + this.incrementCounter = function(table) { + var cnt = parseInt(localStorage.getItem(this.counter(table))) || 0; + cnt++; + localStorage.setItem(this.counter(table), cnt); + } + + this.getCounter = function(table) { + this.incrementCounter(table); + return parseInt(localStorage.getItem(this.counter(table))); + } + + this.insert = function(table, record, id) { + if (typeof record != 'string') + record = JSON.stringify(record); + var id = id || this.getCounter(table); + localStorage.setItem(this.prefix + table + '-' + id, record); + } + + this.select = function(table, id, callback) { + var rec = localStorage.getItem(this.prefix + table + '-' + id); + if (rec) { + rec = JSON.parse(rec); + if (!callback || callback(rec) === true) + return rec; + else + return false; + } + else + return false; + } + + this.selectAll = function(table, callback) { + var recs = []; + if (!callback) + callback = function() { return true; }; + for (key in localStorage) { + var test_key = key.match(new RegExp((this.prefix + table + '-([a-zA-Z0-9]+)$'), "i")); + if (test_key && test_key[1] && test_key[1]!='counter') { + var rec = JSON.parse(localStorage[key]); + if (callback(rec) === true) + recs.push(rec); + } + } + return recs; + }; + + this.update = function(table, id, record) { + this.insert(table, record, id); + }; + +} diff --git a/utilitybelt/core/utils/Object.js b/utilitybelt/core/utils/Object.js new file mode 100644 index 0000000..fd7d942 --- /dev/null +++ b/utilitybelt/core/utils/Object.js @@ -0,0 +1,24 @@ +/** + * Object utils + */ +core.utils.object = { + + /** Allows get object property using string as a path: + * var foo = { bar: { boo: 'bam' } }; + * core.utils.object.getByPath( foo, 'bar.boo' ) -> 'bam' + */ + getByPath: function(o, s) { + s = s.replace(/\[(\w+)\]/g, '.$1'); // convert indexes to properties + s = s.replace(/^\./, ''); // strip a leading dot + var a = s.split('.'); + while (a.length) { + var n = a.shift(); + if (n in o) { + o = o[n]; + } else { + return; + } + } + return o; + } +} \ No newline at end of file diff --git a/utilitybelt/core/utils/Proxy.js b/utilitybelt/core/utils/Proxy.js new file mode 100644 index 0000000..7fa12ea --- /dev/null +++ b/utilitybelt/core/utils/Proxy.js @@ -0,0 +1,212 @@ + +/* + * Proxy + * @param {string} mode Proxy mode, can be 'local' or 'api' + * In 'local' mode requests to Backbone.sync are served through the LocalStorage + * In 'api' mode requests to Backbone.sync are served through the mock API engine, using the SinonJs fakeServer + */ + +core.Proxy = function(mode, debug) { + + this.modes = {}; + +/** +* Simple LocalStorage-based proxy. When enabled, all requests to Backbone.sync are served through the LocalStorage. +*/ + + this.modes.local = function() { + Backbone.serverSync = Backbone.sync; + + Backbone.sync = function(method, model, options) { + + var delay = 300; // to imitate back-end delay + var tbname = model.self, id = model.id, counter = localStorage.getItem(tbname + '-counter') || 0; + + if (model.model) { // collection + tbname = new model.model().self; + if (method == 'read') { + var recs = []; + for (var field in localStorage) { + var id = field.replace(tbname + '-', ''); + if (id.match(/^\d$/)) { + var rec = JSON.parse(localStorage.getItem(field)); + rec.id = id; + if (rec.address != null){ + if (rec.address == model.getUrlParams()['searchAddress']){ + recs.push(rec); + } + } else { + recs.push(rec) + } + + } + } + setTimeout(function() { options.success({ data: recs }) }, delay); + } + } + else { // single model + + if (method == 'create') { + id = ++counter; + model.id = id; + localStorage.setItem(tbname + '-counter', id); + } + + if (method == 'create' || method == 'update') { + localStorage.setItem(tbname + '-' + id, JSON.stringify(model.attributes)); + } + + var rec = JSON.parse(localStorage.getItem(tbname + '-' + id)); + rec.id = id; + + if (method == 'delete') { + localStorage.removeItem(tbname + '-' + id); + } + + setTimeout(function() { options.success(rec) }, delay); + + } + + }; + } + +/** +* Fake API mimicking the Lieferheld's REST API. When enabled, all ajax calls to the '/api/...' urls will be served through the Sinon fakeServer (see http://sinonjs.org/docs/#server). +* Uses LocalStorage as a persistent storage for the records. For now do not support the authorization. +*/ + + this.modes.api = function(debug) { + sinon.FakeXMLHttpRequest.useFilters = true; + sinon.FakeXMLHttpRequest.addFilter(function (method, url, async, username, password) { + if (url.indexOf('/api/') < 0) + return true; + else { + console.log('Sinon API call: ' + url, method); + } + }); + + var processRequest = function (xhr, url) { + var tmp = url.split(/\?(.*)$/), query = (tmp[1] || '').split('&'), record = JSON.parse(xhr.requestBody); + var url = tmp[0].split('/'), method = xhr.method, tbname = url[0], respond = {}; + var search_param = query[0].split('='), search_key = search_param[0], search_value = search_param[1]; + var ls = new core.LSDB(); + if (!url[1]) { // /users + if (method == 'POST') { + if (debug == 1) + console.log('create in ' + tbname); + ls.insert(tbname, record); + respond = record; + } + else if (method == 'GET') { + if (debug == 1) + console.log('select from ' + tbname); + var callback = (search_key!='' && search_value!='')? + function(rec) { + return rec[search_key] == search_value; + } + : function(rec) { + return true; + } + respond = ls.selectAll(tbname, callback); + } + } + else if (!url[2]) { // /users/u1 + var id = url[1]; + if (method == 'PUT') { + if (debug == 1) + console.log('update ' + tbname + ' with id ' + id); + ls.update(tbname, id, record); + respond = record; + } + else if (method == 'GET') { + if (debug == 1) + console.log('read ' + tbname + ' with id ' + id); + respond = ls.select(tbname, id); + } + } + else if (!url[3]) { // /users/u1/orders/ + var parent_id = url[1], child_tbname = url[2]; + if (method == 'POST') { + if (debug == 1) + console.log('create in ' + child_tbname + ' for ' + tbname + ' with id ' + parent_id); + record.parent_id = parent_id; + ls.insert(child_tbname, record); + respond = record; + } + else if (method == 'GET') { + if (debug == 1) + console.log('list ' + child_tbname + ' for ' + tbname + ' with id ' + parent_id); + respond = { data: ls.selectAll(child_tbname, function(rec) { return rec.parent_id == parent_id; }) }; + } + } + else { // /users/u1/orders/o1/ + var parent_id = url[1], child_tbname = url[2], child_id = url[3]; + if (method == 'PUT') { + if (debug == 1) + console.log('update ' + child_tbname + ' with id ' + child_id + ' for ' + tbname + ' with id ' + parent_id); + record.parent_id = parent_id; + ls.update(child_tbname, child_id, record); + respond = record; + } + else if (method == 'GET') { + if (debug == 1) + console.log('read ' + child_tbname + ' with id ' + child_id + ' for ' + tbname + ' with id ' + parent_id); + respond = ls.select(child_tbname, child_id, function(rec) { return rec.parent_id == parent_id; }); + } + } + + var headers = { "Content-Type": "application/json" }, responseCode; + + if (search_key) { + for (var i=0,ii=respond.length;i'); + } + }, options) + Backbone.old_sync(method, model, new_options); + }; + + } +} diff --git a/utilitybelt/core/utils/Router.js b/utilitybelt/core/utils/Router.js new file mode 100644 index 0000000..1edf2ac --- /dev/null +++ b/utilitybelt/core/utils/Router.js @@ -0,0 +1,64 @@ +/* + * Router + * Creates and returns the instance of the class according to the given token. + * Takes care of waiting for DOM-ready event. + * @param {String} className token (usually a class name) + * @param {Object} The data received from the CMS + * + * @handle authorize:grant Refresh user and related cookies + */ +core.Router = function(className, data) { + var env = pageEnv(); + + $(document).ready(function() { + var pageData = _.extend({}, data, env); + var page = core.instantiate(className, pageData); + }); + + function pageEnv() { + core.runtime.setupAuthHeader(core.runtime.apiKey, core.runtime.fetch('user_auth_token')); + // track the user accross the app + core.runtime.user = new core.models.User(); + + // if auth info, refresh the app-wide user instance + if (core.runtime.has('user_auth_token', 'user_id', 'user_email')) { + core.runtime.user.set({ + 'id': core.runtime.fetch('user_id'), + 'email': core.runtime.fetch('user_email') + }, {silent: true}); + core.runtime.user.token = core.runtime.fetch('user_auth_token'); + } + // if no cookie info, just set the available user id + // TODO create a new user to rely less on cookies + else { + core.runtime.user.set({ + 'id': core.runtime.fetch('user_id') + }, {silent: true}); + } + + // if the user authorization has expired, delete any related cookie + core.runtime.user.on('authorize:expire', function(user, token) { + core.runtime.destroy('user_auth_token', 'user_id', 'user_email', 'session', 'exit_poll'); + }); + + // logout callback: on successful logout, destroy cookies and redirect + core.runtime.user.on('authorize:revoke', function(user, token) { + core.runtime.destroy('user_auth_token', 'user_id', 'user_email', 'session', 'exit_poll'); + window.location.href = core.utils.formatURL('/'); + }); + + // login callback: on successful login, persist auth info and reload the page + core.runtime.user.on('authorize:grant authorize:anonym', function(user, token) { + core.runtime.user = user; + core.runtime.persist('user_auth_token', token); + core.runtime.persist('user_id', user.id); + core.runtime.persist('user_email', user.get('email')); + core.runtime.setupAuthHeader(core.runtime.apiKey, token); + }); + + return { + user: core.runtime.user, + active_address: core.runtime.fetch('active_address', true) + }; + } +}; diff --git a/utilitybelt/core/utils/buildUrl.js b/utilitybelt/core/utils/buildUrl.js new file mode 100644 index 0000000..2c876a7 --- /dev/null +++ b/utilitybelt/core/utils/buildUrl.js @@ -0,0 +1,41 @@ +/* + * Builds a URL from provided parts or from existing URL if none supplied + * + * @param {Object} object containing GET parameters. + * @param {String} URL origin (e.g. http://www.google.com) - defaults to window.location.orgin if none supplied. + * @param {String} URL path (e.g. /controller/action/1) - defaults to window.location.pathname if none supplied. + * @param {String} URL hash (e.g. #iamahash) - defaults to window.location.hash (via core.utils.getUrlHash wrapper) + * + * Example: + * + * var url = core.utils.buildUrl({categories: 'cat1, cat2, cat3', option1: 'on', option2: 'on}, 'http://www.google.com', '/controller/action/1', '#iamahash'); + * + */ + + core.utils.buildUrl = function(params, origin, pathname, hash) { + + var paramString, url, i; + + // Patch window.location.origin if it doesn't exist. + if (!window.location.origin) window.location.origin = window.location.protocol + "//" + window.location.host; + + origin = origin || window.location.origin; + pathname = pathname || window.location.pathname; + hash = hash || core.utils.getUrlHash(); + + paramString = ''; + i = 0; + + // TODO refactor without Underscore dependency + _.each(params, function(param, key) { + if(null != param && '' != param) { + paramString += (i > 0 ? '&' : '?') + key + "=" + param; + i++; + } + }); + + url = origin + pathname + paramString + hash; + + return url; + +} \ No newline at end of file diff --git a/utilitybelt/core/utils/formatAddress.js b/utilitybelt/core/utils/formatAddress.js new file mode 100644 index 0000000..ba4e411 --- /dev/null +++ b/utilitybelt/core/utils/formatAddress.js @@ -0,0 +1,16 @@ +core.utils.formatAddress = function(addressObject, mode) { + if (addressObject == null) { + return ""; + } + if (APP_LANGUAGE.toLowerCase() == "au") { + if (mode == 'full') { + return addressObject.suburb + ' ' + (addressObject.zipcode || '') + ', ' + addressObject.state; + } + else { + return addressObject.suburb; + } + } + else { + return addressObject.street_name + " " + addressObject.street_number; + } +} diff --git a/utilitybelt/core/utils/formatItem.js b/utilitybelt/core/utils/formatItem.js new file mode 100644 index 0000000..2ae4ce5 --- /dev/null +++ b/utilitybelt/core/utils/formatItem.js @@ -0,0 +1,8 @@ +/** + * returns the item name with superscript ingredients *1*2*3 + * @param {String} itemString + * @return text + */ +core.utils.formatItem = function (itemString){ + return itemString.replace(/(\*\d[*\d]+)/g, "$1"); +} diff --git a/utilitybelt/core/utils/formatPrice.js b/utilitybelt/core/utils/formatPrice.js new file mode 100644 index 0000000..b120b2d --- /dev/null +++ b/utilitybelt/core/utils/formatPrice.js @@ -0,0 +1,25 @@ + +core.utils._formatPrice = function(currency, onLeft, price) { + if (price) { + var string = price.toFixed(2); + if (onLeft) { + string = currency + string; + } else { + string += currency; + } + return string; + } + else + return ''; +}; + +core.utils.formatPrice = function(price) { + var locale = core.locales[APP_LANGUAGE]; + return core.utils._formatPrice(locale.currency.symbol, locale.currency.position == 'left', price); +} + +core.utils.formatPriceWithoutCurrency = function(price) { + return core.utils._formatPrice('', false, price); +} + +var jsFormatPrice = core.utils.formatPrice; diff --git a/utilitybelt/core/utils/formatURL.js b/utilitybelt/core/utils/formatURL.js new file mode 100644 index 0000000..ff222e4 --- /dev/null +++ b/utilitybelt/core/utils/formatURL.js @@ -0,0 +1,18 @@ +/** + * Formats a URL to the given endpoint + * @param {String} relative URL + * @return {String} + */ +core.utils.formatURL = function (url){ + var rootPath = '/happy'; + + var urlPrefix = window.location.protocol + "//" + window.location.hostname; + if (window.location.port !== '') { + urlPrefix += ":" + window.location.port; + } + + if (!lh_data.global_path_prefix) + rootPath = lh_data.global_path_prefix; + return urlPrefix + rootPath + url; + +} diff --git a/utilitybelt/core/utils/getIcon.js b/utilitybelt/core/utils/getIcon.js new file mode 100644 index 0000000..f7e8605 --- /dev/null +++ b/utilitybelt/core/utils/getIcon.js @@ -0,0 +1,45 @@ +/** + * Icon factory functions + * @private + */ +core.utils._iconFactory = { + + /** + * Returns marker for Google maps + * There are 10 markers for now, if number > 9, we'll rotate and start from beginning + * @param {Number} index Markers number + */ + mapMarker: function(index) { + + var markersPath = '/coredesign/images/google_maps/markers/'; + + var mapMarkers = [ + 'red_MarkerA.png', + 'blue_MarkerB.png', + 'green_MarkerC.png', + 'orange_MarkerD.png', + 'yellow_MarkerE.png', + 'purple_MarkerF.png', + 'red_MarkerG.png', + 'black_MarkerH.png', + 'brown_MarkerI.png', + 'white_MarkerJ.png', + 'grey_MarkerK.png' + ] + + var k = (index % mapMarkers.length); + return markersPath + mapMarkers[k || 0]; + + } +} + +/** + * Icon factory dispatcher + * @param {String} functionName Function name + */ +core.utils.getIcon = function(functionName) { + var fn = core.utils._iconFactory[functionName]; + if (fn) { + return fn.apply(fn, Array.prototype.slice.call(arguments, 1)); + } +} diff --git a/utilitybelt/core/utils/getLocale.js b/utilitybelt/core/utils/getLocale.js new file mode 100644 index 0000000..67c4389 --- /dev/null +++ b/utilitybelt/core/utils/getLocale.js @@ -0,0 +1,24 @@ +/* + * Retrieves locale-specific values + * @param {String} key The key for the localized value, can be nested ("currency.symbol", for example) + */ + +core.utils.getLocale = function(key) { + var value; + + try { + value = core.utils.object.getByPath(core.locales[APP_LANGUAGE], key); + } catch (ex) { + console.error('Cannot find the locale with key "' + key + '"'); + } + + if (typeof value == 'undefined') { + console.error('Cannot find the locale with key "' + key + '"'); + return key; + } else { + return value; + } +} + +var jsGetLocale = core.utils.getLocale; + diff --git a/utilitybelt/core/utils/getTemplate.js b/utilitybelt/core/utils/getTemplate.js new file mode 100644 index 0000000..1e7e2fa --- /dev/null +++ b/utilitybelt/core/utils/getTemplate.js @@ -0,0 +1,43 @@ +/** + * Loads template and apply the given callback. + * Supports asynchronous loading. + * @param {Array} names The template file names (relative to /js/core/templates) + * @param {Function} callback The callback to be run on the loaded templates + */ +core.utils.getTemplate = function(names, callback) { + var path = 'core/templates/', + toLoad = [], + loaded = []; + + if (typeof names == 'string') { + names = [names]; + } + _.each(names, function(name, i) { + if (name in core.templates) { + loaded[i] = core.templates[name]; + } else { + toLoad.push('template!' + path + name); + } + }); + + if (toLoad.length) { + require(toLoad, function() { + _.each(arguments, function(tpl, i) { + var name = tpl.name.replace(path, ''); + var j = _.indexOf(names, name); + loaded[j] = tpl.content; + }); + applyCallback(loaded); + }); + } else { + applyCallback(loaded); + } + + /** + * Applies the callback to all templates. + * @param {Array} templates The loaded templates + */ + function applyCallback (templates) { + callback.apply(this, templates); + } +}; diff --git a/utilitybelt/core/utils/getText.js b/utilitybelt/core/utils/getText.js new file mode 100644 index 0000000..2351a1d --- /dev/null +++ b/utilitybelt/core/utils/getText.js @@ -0,0 +1,35 @@ + +/** + * GetText utility for basic i18n implementation + * Supports sprintf format for messages + * @param {String} key message key + */ +core.utils.getText = function(key) { + var value, args = Array.prototype.slice.call(arguments, 1); + try { + value = core.messages[APP_LANGUAGE][key]; + } + catch (ex) { + value = key; + console.error('Cannot find the message with key "' + key + '"'); + } + + if (typeof value == 'string') { + try { + return _.str.sprintf.apply(_.str.sprintf, [value].concat(args)); + } + catch (ex) { + console.error(ex, '\nWrong message: ' + value); + return value; + } + } + else if (typeof value == 'undefined') { + console.error('Cannot find the message with key "' + key + '"'); + return key; + } + else { + return value.apply(value, args); + } +}; + +var jsGetText = core.utils.getText; diff --git a/utilitybelt/core/utils/getUrlHash.js b/utilitybelt/core/utils/getUrlHash.js new file mode 100644 index 0000000..6d37c3e --- /dev/null +++ b/utilitybelt/core/utils/getUrlHash.js @@ -0,0 +1,12 @@ +/** + * Get the current URL Hash + * @returns {String} String Representation of Hash + */ +core.utils.getUrlHash = function() { + var hash = window.location.hash; + // Normalisation fix for cross browser implementation discrepancies + if (hash.indexOf('#') === 0) { hash = hash.substring(0, hash.length); } + + return hash; + +}; \ No newline at end of file diff --git a/utilitybelt/core/utils/getUrlParams.js b/utilitybelt/core/utils/getUrlParams.js new file mode 100644 index 0000000..9732202 --- /dev/null +++ b/utilitybelt/core/utils/getUrlParams.js @@ -0,0 +1,26 @@ +/** + * Get parameters from URL + * @param {String} String representation of a URL (optional - will act on present URL if not supplied) + * @returns {Object} Javascript representation of URL params + */ +core.utils.getUrlParams = function(url) { + var regex, isArray, hash, parts, key, value; + var params = {}; + + url = url || window.location.href; + regex = /([^=&?]+)=([^&#]*)/g + + while((parts = regex.exec(url)) != null) { + key = parts[1], value = parts[2]; + isArray = /\[\]$/.test(key); + + if(isArray) { + params[key] = params[key] || []; + params[key].push(value); + } + else { + params[key] = value; + } + } + return params; +}; \ No newline at end of file diff --git a/utilitybelt/core/utils/redirectLocation.js b/utilitybelt/core/utils/redirectLocation.js new file mode 100644 index 0000000..b8b9f7b --- /dev/null +++ b/utilitybelt/core/utils/redirectLocation.js @@ -0,0 +1,18 @@ +core.utils.redirectLocation = function(options) { + var location = options.address || options.get('address'); + if (APP_LANGUAGE.toLowerCase() == "au") { + window.location.href = core.utils.formatURL("/" + core.utils.slugifyPart(location.city) + "/" + core.utils.slugifyPart(location.suburb) + "/"); + } else { + window.location.href = core.utils.formatURL("/search/" + location.latitude + "/" + location.longitude); + } +} + +core.utils.slugifyPart = function(part) { + return part.replace(/\s+/g, '-').toLowerCase() +} + +core.utils.redirectRestaurant = function(restaurantSlug, location){ + var address = location.address || location.get('address'); + core.runtime.persist('active_address', address, true); + window.location.href = core.utils.formatURL("/" + core.utils.slugifyPart(address.city) + "/" + restaurantSlug + "/"); +} diff --git a/utilitybelt/core/utils/trackingLogger.js b/utilitybelt/core/utils/trackingLogger.js new file mode 100644 index 0000000..074fe61 --- /dev/null +++ b/utilitybelt/core/utils/trackingLogger.js @@ -0,0 +1,25 @@ +/** +* Helper object for tracking events onto analytics services +*/ +core.utils.trackingLogger = (function(options) { + function logTrackingEvent(category, action, label, value) { + if (typeof _gaq != 'undefined') { + _gaq.push(['_trackEvent', category, action, label, value]); + } + if (typeof ClickTaleTag == "function") { + ClickTaleTag(category + "_" + action); + } + } + + return { + log: function(category, action, label, value) { + if (typeof console != 'undefined') { + console.log(action + ": " + label); + } + logTrackingEvent(category, action, label, value); + }, + logError: function(action, label, value) { + core.utils.trackingLogger.log("error", action, label, value); + } + }; +})(); diff --git a/utilitybelt/core/views/Box.js b/utilitybelt/core/views/Box.js new file mode 100644 index 0000000..8b08b94 --- /dev/null +++ b/utilitybelt/core/views/Box.js @@ -0,0 +1,197 @@ +/** + * Basic Box with the configurable header, footer, and content + * + * Example: + * + * var bb = new core.views.Box({ title: 'Some awesome title', content: 'Awesomest content', el: $('#box-container') }); + * bb.show(); + * + * @title Basic Box + * @param {String} title Title + * @param {String} content Content + * @param {String} template Path to template + * @param {Object or Boolean} If truthy, the box will be fixed to te top of the page when the users scrolls past it. + * If it is an object, additional params can specify the fixable behavior. See initFixablePosition(). + */ +core.define('core.views.Box', { + + extend: 'core.View', + + className: "Box", + + template: 'Box/base.html', + + /** + * Initialize the widget + * @constructor + */ + initialize: function() { + + var template = this.options.template || this.template; + + var me = this; + core.utils.getTemplate(template, function(tpl) { + me.template = tpl; + me.render(tpl); + if (me.options.fixable) { + me.initFixablePosition(me.options.fixable); + } + }); + }, + + render: function(tpl) { + tpl = tpl || this.template; + this.el = _.template(tpl)(this.options.renderData); + this.appendElement(this.el); + + var title = this.title || this.options.title || this.$el.attr('header'); + if (title /*&& $('.title-container', this.$el).length < 1*/) { + this.setTitle(title); + } + + var content = this.options.content; + if (content) { + this.setContent(content); + } + + core.views.Box.__super__.render.call(this); + }, + + /** + * Method for appending the final mark-up to DOM. By default, adds it to the this.el + * Should be overriden for Lightbox-alike widgets (which render themself to the ). + */ + appendElement: function(el) { + this.$el.empty().append(el); + }, + + /** + * Set title for the Lightbox + * @param {String} title Lightbox Title + */ + setTitle: function(title) { + this.$('.title-container').html(title); + }, + + /** + * Set content for the Lightbox + * @param {String} content Lightbox Content + */ + setContent: function(content) { + this.getBody().html(content); + }, + + /* + * Returns body element + * @returns jQuery + */ + getBody: function() { + var body = this.$('.body-box'); + if (body.children("div").length > 0){ + return this.$('.body-box').children().first(); + } else { + return body; + } + }, + + /* + * Adds link to title container and return its element + * @returns jQuery + */ + addTitleLink: function(html) { + var $title_link = this.$('.top-box .title-link'); + return $title_link.append(html); + }, + + /** + * Initialize a part of the view which can change CSS positioning to "fixed" if the user scrolls past its top offset + * @param {Object} opts The options for the fixable behavior: + * hideEl {String} [optional] Selector for an element which will be hidden when the box goes to fixed position + * marginTop {Integer} [optional] The margin-top the box should have when in fixed position + */ + initFixablePosition: function(opts) { + var me = this; + var $fixable = this.$el; + + if (!$fixable || $fixable.length <= 0) + return; + + var hideableSel = opts.hideEl; + var hideable; + if (hideableSel) { + hideable = $(hideableSel); + } + + this.initialOffsetTop = $fixable.offset().top; + this.offsetTop = $fixable.offset().top; + this.initialMarginTop = $fixable.css('margin-top'); + + if ($fixable[0].style.width) { + //if there's inline style defined width, remember it so we can restore it + $fixable.data('origWidth', $fixable[0].style.width); + } + var $placeholder = $("
      "); //takes the place of $fixable when scrolling so the layout is unaltered + $placeholder.insertAfter($fixable); + + core.utils.Dom.scrollingEl.on("scroll", function(evt) { + if (_.has(opts, "hooks") && _.has(opts.hooks, "scrollingStart")) { + if (_.isFunction(opts.hooks.scrollingStart)) { + opts.hooks.scrollingStart.call(me); + } + } + + if ( !me.isFixable && !me.options.fixable ) return; + + var scrollTop = core.utils.Dom.scrollTop(); + + //if the user scrolled past the fixable side, add "fixed-top" class, otherwise remove it. + if (scrollTop > me.offsetTop) { + var fixableWidth = $fixable.width(); + var fixableHeight = $fixable.height(); + $placeholder.css({ + width: fixableWidth, + height: fixableHeight, + display: 'block' + }); + $fixable.css("width", fixableWidth); + $fixable.addClass("fixed-top"); + opts.marginTop && $fixable.css('margin-top', opts.marginTop); + if(hideable){ + hideable.css({ + visibility: "hidden" + }); + } + } else { + if ($fixable.data('origWidth')) { + $fixable.css("width", $fixable.data('origWidth')); + } else { + $fixable.css("width", ""); + } + $fixable.removeClass("fixed-top"); + opts.marginTop && $fixable.css('margin-top', me.initialMarginTop); + $placeholder.css({ + display: 'none' + }); + if(hideable){ + hideable.css({ + visibility: "visible" + }); + } + if (_.has(opts, "hooks") && _.has(opts.hooks, "scrollPositionDocked")) { + if (_.isFunction(opts.hooks.scrollPositionDocked)) { + opts.hooks.scrollPositionDocked.call(me); + } + } + } + }); + core.utils.Dom.scrollingEl.trigger("scroll"); //init the layout: restores $fixable's position if page is refreshed after scrolling + }, + + demo: function() { + var bb = new core.views.Box({ + title: 'Box title' + }); + return bb; + } +}); + \ No newline at end of file diff --git a/utilitybelt/core/views/Box/Cart.js b/utilitybelt/core/views/Box/Cart.js new file mode 100644 index 0000000..e179e8b --- /dev/null +++ b/utilitybelt/core/views/Box/Cart.js @@ -0,0 +1,83 @@ +/** + * Shopping Cart container, for inline display. + */ +core.define('core.views.CartBox', { + extend: 'core.views.Box', + className: "CartBox", + + initialize: function() { + core.views.CartBox.__super__.initialize.call(this); + var me = this; + this.on('render', function() { + me.addClearAllButton(); + me.addPaymentIcons(this.options.paymentIcons); + me.addItem(core.views.Cart, this.options.cart || {}); + }); + }, + + /* + * Adds 'clear cart' button + * @returns jQuery + */ + addClearAllButton: function(el) { + var html = '
      '; + var $btn = $(html); + var $title = $('.top-box', this.$el); + $btn.on('click', _.bind(this.onClearBtnClick, this)); + return $btn.appendTo($title); + }, + + /* + * Adds payment icons + */ + addPaymentIcons: function(icons) { + var $footer = this.$('section > div:last'); + _.each(icons, function(icon) { + $('
      ').addClass(icon.css).attr('tooltip', icon.tooltip).appendTo($footer); + }); + }, + + /* + * Handler for the 'clear cart' button + */ + onClearBtnClick: function() { + this.getItem().collection.empty(); + }, + + demo: function() { + var items = [ + { + "description": "inkl. 0,15\u20ac Pfand", + "sizes": [{"price": 5, "name": "L"}], + "pic": "", + "main_item": true, + "sub_item": false, + "id": "mp1", + "name": "Fanta*1,3,5,7 0,5L " + }, { + "description": "inkl. 0,15\u20ac Pfand", + "sizes": [{"price": 20, "name": "XL"}], + "pic": "", + "main_item": true, + "sub_item": false, + "id": "mp2", + "name": "Pain saucisse" + } + ]; + var cart = new core.collections.Cart(); + for (var i=0, bound=items.length; i 0) + input.parent().children('.error_message').text(error.msg); + + if (input.animate) { + animateField(input, 3, function(field) { + if (input.data('placeholder')) + field.attr('placeholder', input.data('placeholder')); + if (input.data('bg')) + field.css('background-color', input.data('bg')); + }); + } + }); + return false; + }, + + /* + * Mark invalid via simple label color change + * @param {ArrayOfObjects} errors Array of errors + */ + markInvalidSimple: function(errors) { + var form = this.$el; + this.clearInvalidSimple(); + for (var i=0,ii=errors.length; i 0) + changedInput.parent().children('.error_message').text(''); + }, + + /* + * Send form data via jQuery's Ajax call + * Use inline spinner as a loading indicator + * @param {Object} data Form data + * @param {Object} options Ajax call options ('url', 'type' and 'success' for success callback) + */ + send: function(data, options) { + var me = this; + this.showSpinner(); + $.ajax({ + url: options.url, + type: options.type || 'post', + dataType: 'json', + success: _.bind(options.success, this), + error: function() { me.hideSpinner(); console.log('error', arguments); }, + data: data + }); + }, + + /* + * Submit form handler + * Serialize form data and validate it. If validation succeeds, send it, if failed -- use markInvalid to mark errors + * @param {Object} data Form data + * @param {Object} options Ajax call options ('url', 'type' and 'success' for success callback) + */ + submit: function() { + var options = this.options, validations = this.validations, me = this; + var form = this.options.form || this.el; + var data = this.getValues(); + var errors = this.validate(data, validations); + + if (!errors.length) { + this.send(data, options); + } + else { + this.markInvalid(errors); + } + + return false; + }, + + /* + * Serialize and returns Form data as a JSON + * @param {String} extraElementsSelector If provided, the elements described by this jQuery selector will also be serialized (useful e.g. to include hidden elements) + */ + getValues: function(extraElementsSelector) { + var data = {}, formData = this.$(':input:visible,input:checked,select').add(this.$(extraElementsSelector)).serializeArray(); + _.each(formData, function(el) { + if (el.value) { + data[el.name] = el.value; + } + }); + return data; + }, + + /* + * Clear validation errors + * + */ + clearErrors: function() { + + this.errors = []; + + $('input, select, textarea, label', this.el).removeClass('invalid'); + $('.error_messages', this.el).remove(); + + }, + + + /* + * Map validations to form inputs and display validation errors + * + */ + showErrors: function(model,errors) { + var self = this; + errors = errors || this.errors; + + core.utils.getTemplate(self.validationErrorsTemplate, function(tpl) { + var _tpl = _.template(tpl, {'errors': errors}); + self.$('.error_messages').remove(); + self.$el.append(_tpl); + + }); + + _.each(errors, function (error) { + if(_.isArray(error.attr)) { + _.each(error.attr, function(attr) { + self.$('input[name=' + attr + '], label[for=' + attr + '], label.' + attr, this.el).addClass('invalid'); + }); + } else if(error.attr != '') { + self.$('input[name=' + error.attr + '], label[for=' + error.attr + '], label.' + error.attr, this.el).addClass('invalid'); + } + }); + }, + + + /* + * Validate the form data and save the Model if validated successfully, mark errors otherwise + */ + formValidate: function() { + var data = this.getValues(); + this.model.set(data, { silent: true }); + if (this.model.isValid()) { + this.model.save( {}, { success: this.options.onSuccess } ); + } else { + var errors = this.model.validationErrors; + this.markInvalidSimple(errors); + } + }, + + /** + * Enable Form Submit Button + */ + enableSubmit: function() { + this.$el.find('input:submit').prop('disabled', false).removeClass('disabled'); + }, + + /** + * Disable Form Submit Button + */ + disableSubmit: function() { + this.$el.find('input:submit').prop('disabled', true).addClass('disabled'); + }, + + demo: function() { + return $(''); + } +}); diff --git a/utilitybelt/core/views/Form/Address.js b/utilitybelt/core/views/Form/Address.js new file mode 100644 index 0000000..f065cb0 --- /dev/null +++ b/utilitybelt/core/views/Form/Address.js @@ -0,0 +1,121 @@ + +/** +* Address Form, appears on the Home page +* @param {String} template Template code +*/ + +lh.widgets.Address = core.views.Form.extend({ + className: "Address", + type: 'post', + +/* Render widget */ + + render: function() { + this.el.html(_.template(this.template)); + lh.widgets.Address.__super__.render.call(this); + }, + +/* @constructor */ + + initialize: function() { + this.el = $(this.el); + this.model = this.options.model || new lh.models.Address(); + this.defaults = this.options.defaults || this.model.defaults; + + /* Select first field when rendered */ + + this.bind('show', this.selectFirstField); + + this.render(); + + /* Bind tooltip to the question link */ + + $('.question_address', this.el).click( + function(event) { + var tooltip = new lh.core.widgets.Tooltip({ el: $('.tooltip_address'), click_event: event, anchor: $(this) }); + } + ); + + /* Create field labels from Model with error message containers */ + + if (this.model) { + var fields = this.model.fields; + for (var i=0, ii=fields.length; i
      '); + } + } + + this.options.success = this.success; + lh.widgets.Address.__super__.initialize.call(this); + }, + +/* Widget template */ + + template: '
      ' + + '
      ' + + '
      ' + + '' + + '' + + '
      {{ jsGetText("question_address_tooltip") }}
      ' + + '' + + '
      ' + + '
      ' + + '
      ' + + '' + + '' + + '
      ' + + '' + + '' + + '
      ' + + '
      ', + +/* Success callback */ + + success: function(data) { + this.hideSpinner(); + if (data.results) { + if (data.results.length > 1) { + + /* Open Google map widget when multiple addresses */ + + var suggestion_window = new lh.widgets.MultipleChoiceMap({ + el: $('#multiple_choice_address_lightbox'), + addresses: data.results }); + suggestion_window.show(); + } + else { + + /* Redirect to the result page when single result */ + + this.showSpinner(); + lh.utils.redirectByAddress(data.results[0]); + } + } + else { + + /* Show error window if there is no results */ + + /* TODO: create widget for this kind of LB */ + var error_window = new lh.core.widgets.Lightbox({ + el: $('#address_not_found_lightbox'), + events: { + 'click .lightboxSubmit': 'hide' + } + }); + error_window.show(); + + /* Focus and select first field after lightbox is closed */ + + $(error_window.el).bind('hide', _.bind(function() { + this.selectFirstField(); + }, this)); + } + } +}); diff --git a/utilitybelt/core/views/Form/Checkout.js b/utilitybelt/core/views/Form/Checkout.js new file mode 100644 index 0000000..43be4d0 --- /dev/null +++ b/utilitybelt/core/views/Form/Checkout.js @@ -0,0 +1,198 @@ +/** + * Checkout Form View Controller + */ + +core.define('core.views.CheckoutForm', { + + extend: 'core.views.Form', + + events: { + 'submit' : 'submit', + 'keypress' : 'handleKeypress', + 'change select[name=preorder_day]' : 'renderPreOrderTime', + 'click input[name=delivery_time]': 'toggleOrderTime' + }, + + validationErrorsTemplate: 'Form/ValidationErrors.html', + + hiddenFields: "input[name=state]", + + initialize: function () { + var self = this; + + this.delegateEvents(); + + this.on('addressReady', this.addressReady, this); + + // this.$('input[name=delivery_time]').on('click', function(e) { + + // }); + + if (!this.options.restaurant.general.open) { + this.setPreorderTimesOnly(); + } + + if (this.options.activeAddress) { + this.prefillFromActiveAddress(); + } + + this.renderPreOrderTime(); + + core.views.CheckoutForm.__super__.initialize.call(this); + + }, + + setPreorderTimesOnly: function() { + this.$('.preorder input[value=now]').next('label').remove(); + this.$('.preorder input[value=now]').remove(); + this.$('.preorder input[value=preorder]').prop('checked', true); + }, + + prefillFromActiveAddress: function() { + this.setValues(this.options.activeAddress); + }, + + error: function (model, e) { + var me = this; + var trackingErrorsMap = { + invalid_zipcode: 'checkout_error_deliveryarea', + name_required: 'checkout_error_field_first_name', + lastname_required: 'checkout_error_field_last_name', + phone_required: 'checkout_error_field_phone', + invalid_pre_order_time: 'checkout_error_field_preorder', + street_number_required: 'checkout_error_field_building_number', + street_name_required: 'checkout_error_street', + street_name_not_specified: 'checkout_error_street', + email_required: 'checkout_error_field_email', + suburb_required: 'checkout_error_suburb', + restaurant_still_closed : 'checkout_error_field_preorder' + } + _.each(this.order.validationErrors, function(error){ + var attr = error.attr, + msg = error.msg; + if ( trackingErrorsMap.hasOwnProperty(msg) ) { + core.utils.trackingLogger.logError(trackingErrorsMap[msg], error.value || ''); + } + + }); + if (model.validationErrors.length > 0) { + this.errors = model.validationErrors || []; + this.showErrors(); + this.enableSubmit(); + } + }, + + setOrder: function(order) { + + this.order = order; + this.order.on('error:checkout', this.error, this); + + if (order.get('status') === 'created') { + return true; + } else { + this.errors = [{ + 'attr': '', + 'msg': 'this_order_has_already_been_submitted' + }]; + this.showErrors(); + return false; + } + }, + + /** + * Order time toggle handler + * @param {Object} e The event object + */ + + toggleOrderTime: function (e) { + var targetVal; + targetVal = e.target.value; + + this.renderPreOrderTime(); + + if ('now' === targetVal) { + this.order.set('delivery_time','',{silent:true}); + this.$('.preorder select').prop('disabled', true).addClass('disabled'); + + } else if ('preorder' === targetVal) { + this.order.set('delivery_time','preorder',{silent:true}); + this.$('.preorder select').prop('disabled', false).removeClass('disabled'); + } + + this.$('.preorder select').trigger('change'); + }, + + /** + * Render pre-order time dropdowns, conditionally + * @param {Object} e An event object + */ + renderPreOrderTime: function (e) { + var day = this.$('select[name=preorder_day]').val(); + var openHours = {}; + var $preorderTimeSelect = this.$('select[name=preorder_time]'); + var $preorderDaySelect = this.$('select[name=preorder_day]'); + + if (_.isEmpty(this.options.today_times)) { + $preorderDaySelect.find("option[value=today]").remove(); + } + + if (_.isEmpty(this.options.tomorrow_times)) { + $preorderDaySelect.find("option[value=tomorrow]").remove(); + } + + if (day === 'today') { + openHours = this.options.today_times; + } else if (day === 'tomorrow') { + openHours = this.options.tomorrow_times; + } + + $preorderTimeSelect.empty(); + + // openHours maps a restaurant's local pre-order time to the same time in UTC (with date): + // {'22:00': '2012-08-30T12:00:00Z', ...} + var sortedLocalTimes = _.keys(openHours).sort(); + var utcTime; + _.each(sortedLocalTimes, function(localTime) { + utcTime = openHours[localTime]; + $preorderTimeSelect.append(''); + }); + $preorderTimeSelect.trigger('select:update'); + }, + + /** + * Form submit event listener callback + * @param {Object} e The event object + * @param {Boolean} validateOnly If true, the form is just validated and not submitted. + */ + submit: function (e, validateOnly) { + var formData, self = this, order, address, deliveryAddress, deliveryTime, payment, validationErrors; + + if(!validateOnly){ + this.disableSubmit(); + } + this.clearErrors(); + e.preventDefault(); + formData = this.getValues(this.hiddenFields); + + this.order.checkout(formData, validateOnly); + }, + + /** + * Updates the order model from the form. + */ + updateOrder: function(){ + var formData = this.getValues(this.hiddenFields); + this.order.setFormData(formData); + }, + + /** + * Handles the keypress event. If the user presses enter, validate the form but don't submit it. + */ + handleKeypress: function(evt){ + if(evt.which == 13){ + this.submit(evt, true); + evt.preventDefault(); + } + } + +}); diff --git a/utilitybelt/core/views/Form/ExitPoll.js b/utilitybelt/core/views/Form/ExitPoll.js new file mode 100644 index 0000000..e5fb82f --- /dev/null +++ b/utilitybelt/core/views/Form/ExitPoll.js @@ -0,0 +1,84 @@ +/** + * Form for Exit Poll + * + * Example: + * + * var form = new core.views.ExitPollForm({ model: theExitPoll }); + * form.renderTo($('#my_container')); + * + * @param {Object} model An instance of an Exit Poll model + */ +core.define('core.views.ExitPollForm', { + extend: 'core.views.Form', + className: 'ExitPollForm', + options: { + template: 'ExitPoll/ExitPollForm.html' + }, + + events: { + "click .submit": "submit", + }, + + plugins: { + 'form' : 'jqTransform' + }, + + initialize: function(options) { + var me = this, tpl = this.options.template; + this.collection = this.options.collection || new core.collections.ExitPoll(); + + var exit_poll_obj = this.collection.getShuffledOptions(); + core.utils.getTemplate(this.options.template, function(tpl) { + me.template = _.template(tpl, {exit_poll: exit_poll_obj}); + me.render(); + }); + }, + + /** + * Renders the widget using the given template. + * @param {String} tpl The html template used to render the form + */ + render: function(tpl) { + this.$el.html(this.template); + this.form = this.$('form'); + this.logFirstTimeDisplay(); + + core.views.ExitPollForm.__super__.render.call(this); + }, + + submit: function() { + var data = this.getValues(); + var correct = this.collection.validate(data); + + if (correct) { + this.send(data); + } else { + this.markInvalid(); + } + + return false; + }, + + markInvalid: function(){ + this.$('.error').text(jsGetText('poll_error')); + }, + + send: function(data, options){ + this.logSendDisplay(); + //set a cookie to prevent more than 1 instance of the poll + core.runtime.persist('exit_poll', 'yes'); + this.options.hide(); //hide() method passed from parent lightbox + }, + + logFirstTimeDisplay: function(){ + var howManyOptions = this.$('input[type="radio"]').length; + var locationData = this.options.deliveryAddress.get('suburb') + ',' + this.options.deliveryAddress.get('city'); + core.utils.trackingLogger.log('polls', 'exit_poll_first_find_display', locationData, howManyOptions); + }, + + logSendDisplay: function(){ + var howManyOptions = this.$('input[type="radio"]').length; + var selectedOption = this.$('input[type=radio]:checked').val(); + core.utils.trackingLogger.log('polls', 'exit_poll_first_find_submit', selectedOption, howManyOptions); + } +}); diff --git a/utilitybelt/core/views/Form/Filter.js b/utilitybelt/core/views/Form/Filter.js new file mode 100644 index 0000000..858e5e8 --- /dev/null +++ b/utilitybelt/core/views/Form/Filter.js @@ -0,0 +1,283 @@ +/** + * Restaurant List Filter Form + * + */ +core.define('core.views.FilterForm', { + + extend: 'core.views.Box', + className: 'FilterForm', + + options: { + cookieName: 'showFilter', + implicit_categories: '', + template: 'Filter/FilterForm.html' + }, + + plugins: { + 'form' : 'jqTransform' + }, + + events: { + 'click form .button' : 'submit', + 'click input[type=checkbox]' : 'toggleCheckbox', + 'click .hideshow' : 'toggleVisibility' + }, + + /** + * Constructor. + */ + initialize: function() { + var me = this; + var tpl = this.options.template; + + this.optionsCollection = this.options.options || new core.collections.Option(); + this.categoriesCollection = this.options.categories || new core.collections.Category(); + + core.utils.getTemplate(tpl, function(tpl) { + me.render(tpl); + }); + + core.views.FilterForm.__super__.initialize.call(this); + }, + + /** + * Get selected (checked) input[type=checkbox] + * @returns {Object} Selected items + */ + getSelected: function() { + var selected = [], categories = [], options = []; + + selected = this.$("input[type=checkbox]:checked"); + + _(selected).each(function(filter,i) { + if ($(filter).hasClass('category')) { + categories.push(filter.name); + } else if (!$(filter).hasClass('filter-category-select-all')) { + options.push(filter.name); + } + }); + + return {categories: categories, options: options}; + + }, + + /** + * Builds a new filter URL + * @returns {String} A string representation of URL + */ + buildUrl: function() { + var selected, params = {}; + + selected = this.getSelected(); + if (selected.categories) { + params['categories'] = selected.categories; + } + + _.each(selected.options, function(option) { + params[option] = "true"; + }); + + pathname = window.location.pathname; + if(this.options.implicit_categories) { + parts = pathname.split('/'); + parts_len = parts.length + + for(var i = 0; i < this.options.implicit_categories.length; i++) { + category_part = _.lastIndexOf(parts, this.options.implicit_categories[i]); + if(category_part != -1) { + parts.splice(category_part, 1); + } + } + + /* If we haven't popped anything then there is some other category + * indicator than needs to be removed + */ + if(parts_len == parts.length) { + if(parts[-1] == '') { + parts.splice(-2, 1) + } else { + parts.pop() + } + } + pathname = parts.join('/'); + } + + + url = core.utils.buildUrl(params, null, pathname); + + return url; + }, + + /* + * Toggles select all / option checkbox(es) + * + */ + toggleCheckbox: function(e) { + if($(e.target).hasClass('filter-category-select-all')) { + $("input.category").prop("checked", false).trigger('change'); + } else if($(e.target).hasClass('category')){ + $('.filter-category-select-all').prop("checked", false).trigger('change'); + } + }, + + /** + * Form Submit Event Handler + * + */ + submit: function() { + var url = this.buildUrl(); + window.location.href = url; + }, + + /** + * Renders the view/template and injects in to the dom + * @param {String} tpl An HTML template + */ + render: function(tpl) { + var me = this; + + me.template = _.template(tpl,{ + categories: me.categoriesCollection.toJSON(), + options: me.optionsCollection.toJSON() + }); + + me.$el.html(this.template); + this.prePopulateFromUrlParams(); + this.setVisibilityBasedOnCookie(); + + core.views.Form.__super__.render.call(this); + + }, + + prePopulateFromUrlParams: function() { + var params, + categories, + me = this; + + params = core.utils.getUrlParams(); + // Grab the categories + categories = (params.categories || '').split(','); + + if(this.options.implicit_categories) { + if(_.intersection(categories, this.options.implicit_categories).length <= 0) { + if(categories[0] == '') { categories.pop(); } + categories = _.union(categories, this.options.implicit_categories); + } + } + + // Delete categories from params hash + delete params.categories; + + // Check selected categories + if(categories.length === 1 && (categories[0] === 'all' || categories[0] === '')) { + me.$(".filter-category-select-all").prop('checked', true); + } else { + _.each(categories, function(category) { + me.$("input[name=" + category + "]").prop("checked", true); + }); + } + + // Anything left is assumed to be an option + _.each(params, function(value, option) { + me.$("input[name=" + option + "]").prop("checked", true); + }); + + }, + + /** + * Toggle Filter Visibility based on value of cookie + */ + setVisibilityBasedOnCookie: function() { + if ($.cookie(this.options.cookieName) === 'false') { + this.hide(true); + } else { + this.show(true); + } + }, + + /** + * Toggle Filter Visibility + */ + toggleVisibility: function() { + if (this.$el.hasClass('hidden')) { + this.show(); + } else { + this.hide(); + } + }, + + /** + * Show Filter Widget + * @param {Bool} noFx Suppress/Skip FX/Animation + * TODO: Move to core.View - implement noFx as property? + */ + show: function(noFx) { + var me = this; + me.isFixable = true; + + if (!noFx) { + me.$('.body-filter').slideDown(function() { + me.$el.removeClass('hidden'); + me.$('.label').text(jsGetText('hide_filter')); + $.cookie(me.options.cookieName, 'true'); + }); + } else { + me.$el.removeClass('hidden'); + me.$('body-filter').show(); + me.$('.label').text(jsGetText('hide_filter')); + } + }, + + /** + * Hide Filter Widget + * @param {Bool} noFx Suppress/Skip FX/Animation + * TODO: Move to core.View - implement noFx as property? + */ + hide: function(noFx) { + var me = this; + me.isFixable = false; + + if (!noFx) { + me.$('.body-filter').slideUp(function() { + me.$el.addClass('hidden'); + me.$('.label').text(jsGetText('show_filter')); + $.cookie(me.options.cookieName, 'false'); + }); + } else { + me.$el.addClass('hidden'); + me.$('.body-filter').hide(); + me.$('.label').text(jsGetText('show_filter')); + } + $(document).scroll(); //udpate view and hide filter + }, + + /** + * Demo code + */ + demo: function() { + + var categories = new core.collections.Category([ + {name: 'pizza'}, + {name: 'fast-food'}, + {name: 'asian'}, + {name: 'sushi'}, + {name: 'indian'}, + {name: 'mediterran'}, + {name: 'oriental'}, + {name: 'gourmet'}, + {name: 'international'} + ]); + + var options = new core.collections.Option([ + {name: 'online_payment'}, + {name: 'box'} + ]); + + var form = new core.views.FilterForm({ + categories: categories, + options: options + }); + + return form; + } +}); \ No newline at end of file diff --git a/utilitybelt/core/views/Form/Login.js b/utilitybelt/core/views/Form/Login.js new file mode 100644 index 0000000..80e6ae9 --- /dev/null +++ b/utilitybelt/core/views/Form/Login.js @@ -0,0 +1,129 @@ +/** + * Form for User login + * + * Example: + * + * var form = new core.views.LoginForm({ model: theUser }); + * form.renderTo($('#my_container')); + * + * @param {Object} model An instance of a user model + */ +core.define('core.views.LoginForm', { + extend: 'core.views.Form', + className: 'LoginForm', + options: { + template: 'Login/LoginForm.html' + }, + + events: { + 'keypress': 'submitOnEnter', + 'submit form': 'authorize', + 'click .submit': 'authorize' + }, + + /** + * Constructor. + * Instantiates an empty User model if none provided. + * Renders the form with the provided template (defaults to Login/LoginForm.html). + * Sets up authorize callbacks. + * @param {Object} options A hash containing the view options + */ + initialize: function(options) { + var me = this, + tpl = this.options.template; + this.model = this.options.model || new core.models.User(); + + this.model.on('authorize:grant', _.bind(this.authorizeSuccess, this)); + this.model.on('authorize:deny', _.bind(this.authorizeDeny, this)); + + core.utils.getTemplate(this.options.template, function(tpl) { + me.template = _.template(tpl); + me.render(); + }); + + if (this.options.show) { + this.render(); + } + }, + + /** + * Renders the widget using the given template. + * @param {String} tpl The html template used to render the form + */ + render: function(tpl) { + this.$el.html(this.template()); + this.form = this.$('form'); + this.clearInvalidSimple(); + core.views.LoginForm.__super__.render.call(this); + }, + + refresh: function(submittable, errors) { + this.$('p.message').empty(); + if (errors) { + this.markInvalid(errors); + this.$('p.message').html('' + jsGetText('login_error_credentials') + ''); + } + if (submittable) { + this.$('.submit').removeClass('disabled'); + } + else { + this.$('.submit').addClass('disabled'); + } + }, + + /** + * Tells if the form is submittable or not + * @return {Boolean} True if the form can be submitted, false otherwise + */ + isSubmittable: function() { + return !this.$('.submit').is('.disabled'); + }, + + /** + * Validates login inputs and forward authorization query to the User model. + */ + authorize: function() { + if (!this.isSubmittable()) { + return; + } + var data = this.getValues(); + this.model.set(data); + if (this.model.isValid()) { + this.refresh(false); + this.model.authorize(); + } else { + this.refresh(true, this.model.validationErrors); + } + }, + + submitOnEnter: function(e) { + if (e.which == 13) { + this.authorize(); + } + }, + + /** + * Authorization success handler. + * @param {core.models.User} user The freshly authorized user + */ + authorizeSuccess: function(user, token) { + var bits = ['Hi,', user.get('name'), user.get('last_name'), '!']; + this.$el.text(bits.join(' ')); + }, + + /** + * Authorization error handler. + * @param {core.models.User} user The unauthorized user + */ + authorizeDeny: function(user, errors) { + this.refresh(true, [{attr: 'email'}, {attr: 'pwd'}]); + }, + + /** + * Demo code + */ + demo: function() { + var form = new core.views.LoginForm(); + return form; + } +}); diff --git a/utilitybelt/core/views/Form/Password.js b/utilitybelt/core/views/Form/Password.js new file mode 100644 index 0000000..89900e4 --- /dev/null +++ b/utilitybelt/core/views/Form/Password.js @@ -0,0 +1,146 @@ +/** + * Form for account settings (password change) + * + * Example: + * + * var form = new core.views.PasswordForm(); + * form.renderTo($('#my_container')); + * + * @param {Object} user An instance of a user model + */ +core.define('core.views.PasswordForm', { + extend: 'core.views.Form', + className: 'PasswordForm', + options: { + template: 'Password/PasswordForm.html' + }, + + events: { + 'keypress': 'handleKeypress', + 'click .button.submit': 'updateDetails' + }, + + plugins: { + 'h4': 'jqTransform' + }, + + /** + * Constructor. + * Renders the form with the provided template (defaults to Password/PasswordForm.html). + * @param {Object} options A hash containing the view options + */ + initialize: function(options) { + var me = this, + tpl = this.options.template; + + this.model = this.options.user; + this.originalModel = this.options.user; + + core.utils.getTemplate(this.options.template, function(tpl) { + me.template = _.template(tpl , { + email: me.model.get('email') + }); + me.render(); + }); + + if (this.options.show) { + this.render(); + } + }, + + /** + * Renders the widget using the given template. + * @param {String} tpl The html template used to render the form + */ + render: function(tpl) { + this.$el.html(this.template); + this.form = this.$('form'); + this.message = this.$('h4'); + this.message.hide(); + core.views.UserAddressForm.__super__.render.call(this); + }, + + /** + * Updates the view after form validation + * @param {Array} errors The validation error + * @param {String} errors.message An error message to be displayed + */ + refresh: function(errors) { + var errorMsgElt = this.$('.inputError'); + errorMsgElt.empty(); + if (errors) { + this.markInvalid(errors); + errorMsgElt.html( errors.message ); + } + }, + + /* + * Validate the form to update the user's details (currently, only the password is supported) + * If the validation is successful, make a request to the server and display the result message ("ok" or "error") + * Otherwise, highlight invalid fields + */ + updateDetails: function(){ + var formData = this.getValues(); + var me = this; + this.model.set({ + email: formData.email, + pwd: formData.pwd + },{ + silent: true + }); //explicitly set both properties: if the pwd field is empty, it is not in formData and the previous value of the property remains in the model + this.model.validate(); + + if (formData["pwd"] != formData["repeat_pwd"]) { + //manually set an invalid status in the model + this.model.validationErrors.push({attr:'pwd'}); + this.model.validationErrors.push({attr:'repeat_pwd'}); + this.model.validationErrors.message = jsGetText('passwords_donot_match'); + } + + if (this.model.isValid()) { + if (!this.model.has('pwd')) this.model.unset('pwd', {silent: true}); //don't send an empty password to the API + + this.model.unset("email", {silent: true}); //FIXME the API won't allow a user to change their email address (unlike lieferheld.de). This line is ecessary for now + this.model.save( {}, { + success: _.bind(this.accountUpdated, this), + error: function(){ + var errors = []; + errors.message = jsGetText('settings_not_saved'); + me.refresh(errors); + }, + silent: true + }); + } else { + this.refresh(this.model.validationErrors); + } + }, + + /** + * Called when the user's details have been successfully updated on the server + */ + accountUpdated: function(model, response){ + this.form.hide(); + this.message.show(); + var me = this; + _.delay( function(){ + me.trigger( "hide" ); + }, 3000 ); + }, + + /** + * Handles the keypress event and updates the details if the user presses enter + */ + handleKeypress: function(evt){ + if(evt.which==13){ + this.updateDetails(); + } + }, + + /** + * Demo code + */ + demo: function() { + var form = new core.views.PasswordForm(); + return form; + } +}); diff --git a/utilitybelt/core/views/Form/UserAddress.js b/utilitybelt/core/views/Form/UserAddress.js new file mode 100644 index 0000000..dd8e71b --- /dev/null +++ b/utilitybelt/core/views/Form/UserAddress.js @@ -0,0 +1,91 @@ +/* + * Form for editing User Address + * + * Example: + * + * var form = new core.views.UserAddressForm({ id: 'a2', user_id: 'a1' }); + * form.renderTo($('#my_container')); + * + * @param {String} user_id User ID + * @param {String} id Address ID + */ + +core.define('core.views.UserAddressForm', { + extend: 'core.views.Form', + className: 'UserAddressForm', + template: '', + user_id: null, + model: null, + + events: { + "submit form": "formValidate", + "click .submit": "formValidate", + "click a.back": "onBackLinkClick" + }, + + plugins: { + 'form': 'jqTransform' + }, + + /** + * Constructor + */ + initialize: function(){ + var me = this; + this.user_id = this.options.user_id; + this.id = this.options.id; + + this.record = this.options.record; + + this.model = this.options.model || new core.models.Address({ user_id: this.user_id, id: this.id }); + + core.utils.getTemplate('UserAddress/UserAddressForm.html', function(tpl) { + me.render(tpl); + }); + }, + + /* + * Render the widget using the template + */ + render: function(tpl){ + var me = this; + me.template = _.template(tpl); + me.$el.append(me.template); + me.form = me.$el.find('form'); + me.clearInvalidSimple(); + me.loadAddressData(); + core.views.UserAddressForm.__super__.render.call(this); + }, + + /* + * Populate with the address data + */ + loadAddressData: function(){ + var me = this; + if (this.record) { + me.model = this.record; + me.setValues(this.record); + } + else if (this.options.id) { + this.model.fetch({ success: function(mdl) { + me.setValues(mdl); + } }); + } + }, + + /* + * Runs when user clicks on a "Back" link + */ + onBackLinkClick: function() { + if (this.options && this.options.onBackLinkClick) + this.options.onBackLinkClick(); + }, + + /* + * Demo code + */ + demo: function() { + var form = new core.views.UserAddressForm({ id: 1, user_id: 1 }); + return form; + } +}); diff --git a/utilitybelt/core/views/Lightbox.js b/utilitybelt/core/views/Lightbox.js new file mode 100644 index 0000000..2d58d5b --- /dev/null +++ b/utilitybelt/core/views/Lightbox.js @@ -0,0 +1,297 @@ +/** + * Basic Lightbox with the title, close icon, and content + * + * Example: + * + * var lb = new core.views.Lightbox({ title: 'Some awesome title', content: 'Awesomest content' }); + * lb.show(); + * + * @title Basic Lightbox + * @param {String} title Title + * @param {String} content Content + * @param {Number} x X coordinate on the screen + * @param {Number} y Y coordinate on the screen + * @param {String} tempate Path to template + */ +core.define('core.views.Lightbox', { + + extend: 'core.views.Box', + + className: "Lightbox", + + template: 'Lightbox/base.html', + + templateCloseButton : 'Lightbox/close.html', + + __closeButton : null, + + events: { + "click .top-box .close-icon" : "hide", + "click .closeLB.button" : "hide" + }, + + options: { + closeButton: false, + modal: false, + position: "absolute", + blanketId: "dark_blanket", + blanketClickToClose: false, + fx: true + }, + + /** + * Init blanket layer which cover the page to prevent interaction when Lightbox is modal. + * Click on blanket close the Lightbox. + */ + initBlanket: function() { + var doc = $(document); + if (!this.blanket) { + this.blanket = $(document.createElement("div")).attr({ + id : this.options.blanketId + }).hide(); + if (this.options.blanketClickToClose){ + this.blanket.bind('click', _.bind(this.hide, this)); + } + + this.blanket.appendTo(document.body); + } + }, + + /** + * Hide Lightbox, destroy its DOM structure, hide blanket + */ + hide: function() { + this.trigger('hide'); + if (this.options.fx) { + var me = this; + this.$el.stop().fadeOut('fast', function() { + me.remove(); + }); + if (this.isModal() && this.blanket) { + this.blanket.stop().fadeOut('fast', function() { + $(this).remove(); + me.blanket = null; + }); + } + } + else { + // remove from DOM, to prevent flooding + this.remove(); + if (this.isModal() && this.blanket) { + this.blanket.remove(); + this.blanket = null; + } + } + }, + + /** + * Init Lightbox position and layout properties + */ + initPosition: function() { + this.$el.css({ + position : this.options.position || 'absolute', + 'margin': '0px' + }); + + this.body = this.getBody(); + + if(this.options.height) { + this.body.height(this.options.height); + this.body.css({ + 'overflow-x': 'hidden', + 'overflow-y': 'auto' + }); + } + + var pos; + + if (this.options.x && this.options.y) { + pos = { + left : this.options.x, + top : this.options.y + }; + } else { + //center the lightbox + pos = this._getCenteredPosition(); + } + + pos.top += core.utils.Dom.scrollTop(); //Fit the lightbox vertically into the viewport if it has been scrolled + + this.setPosition(pos); + }, + + /** + * Initialize the widget + * @constructor + */ + initialize: function() { + this.template = this.options.template || this.template; + var me = this; + this.on('render', function() { + me.clearError(); + me.initPosition(); + if (me.options.closeButton){ + me.showCloseButton(); + } else { + me.hideCloseButton(); + } + // show view + if (me.options.fx) { + me.$el.fadeIn('fast'); + } + else { + me.$el.show(); + } + // show blanket + if (me.isModal()) { + me.initBlanket(); + if (me.options.fx) + me.blanket.fadeIn('fast'); + else + me.blanket.show(); + } + if (me.options.errorMessage) { + me.showError(me.options.errorMessage); + } + }); + core.utils.getTemplate(this.template, function(tpl) { + me.template = tpl; + }); + core.utils.getTemplate(this.templateCloseButton, function(tpl) { + me.templateCloseButton = _.template(tpl)(); + }); + }, + + /** + * shows the close button + */ + showCloseButton : function() { + if (this.__closeButton == null) { + var body = this.$el.find('.body-box'); + this.__closeButton = $(this.templateCloseButton).clone(); + body.append(this.__closeButton); + } + this.options.closeButton = true; + }, + + + /** + * hides the close button + */ + hideCloseButton: function(){ + if (this.__closeButton != null) { + this.__closeButton.remove(); + this.__closeButton = null; + } + this.options.closeButton = false; + }, + + appendElement: function(el) { + this.$el = $(el).appendTo($('body')).hide(); + }, + + /** + * Set title for the Lightbox + * @param {String} title Lightbox Title + */ + setTitle: function(title) { + this.$('.title-container').html(title); + }, + + /** + * Set content for the Lightbox + * @param {String} content Lightbox Content + */ + setContent: function(content) { + this.getBody().html(content); + }, + + /* + * Check if Lightbox is modal + * @returns Boolean + */ + isModal: function() { + return this.options.modal != false; + }, + + /* + * Show inline error via .message element + * @method showError + */ + showError: function(text) { + this.options.errorMessage = text; + this.$('.message').html(text); + }, + + /* + * Clear error from the .message element + */ + clearError: function() { + this.$('.message').html(''); + }, + + /** + * Sets the position of the lightbox + * @param {Object} position Object with two properties: + * {Number} top: The top offset in px + * {Number} left: The left offset in px + */ + setPosition: function(position){ + this.$el.css({ + top: position.top, + left: position.left + }); + }, + + /** + * Returns true if the lightbox's height is bigger than the viewport's, false otherwise. + */ + isOverflowing: function(){ + return this.$el.height() > core.utils.Dom.viewHeight(); + }, + + /** + * Returns the coordinates of the lightbox, centered inside the viewport + */ + _getCenteredPosition: function(){ + return { + top: 130, + left: ((core.utils.Dom.viewWidth() - this.$el.outerWidth()) / 2) + core.utils.Dom.scrollLeft() + }; + }, + + /* + * Center Lightbox + */ + center: function() { + var centeredPos = this._getCenteredPosition(); + this.setPosition(centeredPos); + }, + + isVisible: function() { + return this.$el.is(':visible') || this.$el.is(':animated'); + }, + + /* + * Show Lightbox + */ + show: function() { + if (this.isVisible()) { + return; + } + this.__closeButton = null; + var me = this; + me.render(); + }, + + demo: function() { + var $result = $('Click Me').click(function() { + var lb = new core.views.Lightbox({ + title: 'Some awesome title', + content: 'Awesomest content' + }); + lb.show(); + }); + return $result; + } +}); diff --git a/utilitybelt/core/views/Lightbox/ChooseBox.js b/utilitybelt/core/views/Lightbox/ChooseBox.js new file mode 100644 index 0000000..bd5e821 --- /dev/null +++ b/utilitybelt/core/views/Lightbox/ChooseBox.js @@ -0,0 +1,58 @@ +/** + * Lightbox for choosing + * a) to keep the current active address + * or + * b) to search again + * + * Examples: + * + * var cb = new core.views.ChooseLightbox(); + * cb.show(); + * + * @extends core.views.Lightbox + */ + +core.define('core.views.ChooseLightbox', { + + extend : 'core.views.Lightbox', + + template: 'PLZ/choose_lightbox.html', + + events : { + "click .proceed" : "proceed", + "click .back" : "back" + }, + + options: { + blanketId: "light_blanket", + blanketClickToClose: true + }, + + className : 'ChooseLightbox', + + proceed: function(){ + core.utils.redirectLocation(this.options.activeAddress); + }, + + back: function(){ + this.trigger("show:search"); + }, + + setRestaurantId: function(restaurantId){ + this.options.restaurantId = restaurantId; + }, + + setAddress: function(address){ + this.options.activeAddress = address; + }, + + show: function(restaurantName){ + this.options.renderData = { + restaurantName: restaurantName, + activeAddress : (this.options.activeAddress.get('address').zipcode + " " + this.options.activeAddress.get('address').city) + }; + core.views.ChooseLightbox.__super__.show.call(this); + //this.setTitle(jsGetText("location_box_title", restaurantName)); + } + +}); diff --git a/utilitybelt/core/views/Lightbox/Error.js b/utilitybelt/core/views/Lightbox/Error.js new file mode 100644 index 0000000..65cd2c2 --- /dev/null +++ b/utilitybelt/core/views/Lightbox/Error.js @@ -0,0 +1,28 @@ +core.define('core.views.ErrorLightbox', { + extend : 'core.views.Lightbox', + + template: 'Lightbox/small.html', + + events : { + "click .top-box .close-icon" : "hide", + "click .reload": "reload" + }, + + initialize: function() { + core.views.ErrorLightbox.__super__.initialize.call(this); + this.on('render', _.bind(this.update, this)); + }, + + update: function() { + this.setTitle(jsGetText('global_error_sorry')); + this.setContent([ + $('

      ').text(jsGetText('global_ajax_error')).html(), + $('

      ').html($('').attr('href', window.location.href).text(jsGetText('page_reload'))).html() + ].join(' ')); + }, + + reload: function() { + window.location.reload(); + return false; + } +}); diff --git a/utilitybelt/core/views/Lightbox/ExitPoll.js b/utilitybelt/core/views/Lightbox/ExitPoll.js new file mode 100644 index 0000000..911994a --- /dev/null +++ b/utilitybelt/core/views/Lightbox/ExitPoll.js @@ -0,0 +1,23 @@ +/** + * Lightbox for Exit Poll + * + * var cb = new core.views.ExitPollLightbox(); + * cb.show(); + * + * @extends core.views.Lightbox + */ + +core.define('core.views.ExitPollLightbox', { + + extend : 'core.views.Lightbox', + template: 'Lightbox/small.html', + + initialize: function(options) { + this.on('render', function() { + this.options['hide'] = _.bind(this.hide, this); + this.addItem(core.views.ExitPollForm, this.options); + this.setTitle(jsGetText('poll_title')); + }); + core.views.ExitPollLightbox.__super__.initialize.call(this); + } +}); diff --git a/utilitybelt/core/views/Lightbox/Flavors.js b/utilitybelt/core/views/Lightbox/Flavors.js new file mode 100644 index 0000000..eebfd2d --- /dev/null +++ b/utilitybelt/core/views/Lightbox/Flavors.js @@ -0,0 +1,300 @@ +/** + * Lightbox which displays a list of flavors for a menu item + * + * Examples: + * + * var lb = new core.views.FlavorsLightbox(); + * lb.show(item); + * + * @extends core.views.Lightbox + */ + +core.define('core.views.FlavorsLightbox', { + + extend: 'core.views.Lightbox', + + templateFlavor: "Flavors/flavors.html", + + events: { + "click .top-box .close-icon": "hide", + "click .button": "chooseFlavors", + "click input[data-flavor-id]": 'recalculatePrice' + }, + + className: 'FlavorsLightbox', + + plugins: { + 'section': 'jqTransform' + }, + + /** + * Constructor + * @constructor + * @param {Object} options Options + */ + initialize: function(options) { + var me = this; + core.utils.getTemplate(me.templateFlavor, function(tpl) { + me.templateFlavor = tpl; + }); + core.views.FlavorsLightbox.__super__.initialize.call(this, options); + }, + + /** + * Display the flavors lightbox for a menu item + * @param {core.models.Item} item The backbone model (with flavors information) for the menu item + */ + show: function(item) { + this.item = item; + this.itemJSON = item.viewJSON(); + this.basePrice = this.itemJSON.sizes[0].price; + + //add all flavors to a view-wide collection so they can easily be retrieved by ID + this.allFlavors = item.getAllSubItems(); + + + this.title = this.itemJSON.name; + this.options.content = _.template(this.templateFlavor)( _.extend(this.itemJSON, { + getType: function(flavorContainer){ + if (flavorContainer.flavors.structure == null){ + throw new Error("Invalid flavor structure"); + } + var type = "radio"; + if (flavorContainer.flavors.structure == "0" || flavorContainer.flavors.structure == "2"){ + type = "checkbox"; + } + return type; + } + })); + core.views.FlavorsLightbox.__super__.show.call(this); + + this.setDisplayedPrice(core.utils.formatPriceWithoutCurrency(this.basePrice)); + }, + + /** + * Validates the flavors chosen for the item. If the validation is successful, the item is added to the cart. + * Otherwise, the invalid sections are highlighted. + */ + chooseFlavors: function() { + var clonedItem = {}; + jQuery.extend(true, clonedItem, this.itemJSON); + var itemModel = this.createItemWithSelectedFlavors(this.getSelectedFlavors(), clonedItem); + + var errorDiv = this.$(".flavors_error_message") + //VALIDATION + var errors = itemModel.validate(); + var me = this; + if(errors) { + errorDiv.html(""); + this.$(".flavors-section-header").removeClass("invalidSection"); + var scrolled = false; + _.each(errors, function(formId) { + var form = this.$("#" + formId); + if(form.length) { + if(!scrolled && me.isOverflowing()){ + core.utils.Dom.scrollTop(form[0]); //scroll to the *first* invalid form + scrolled = true; + } + form.find(".flavors-section-header").addClass("invalidSection"); + } + }, me); + errorDiv.html(jsGetText("flavors_message_choose")); + this.trigger("flavorsInvalid"); + } else { + this.trigger("flavorsChosen", { + selectedItemModel: itemModel + }); + this.hide(); + } + }, + + /** + * Creates a core.models.Item model for an item object. The resulting model contains only the flavors specified in flavorsArray. + * @param {Array} flavorsArray An array of ID strings of flavors to be included in the returned Item model + * @param {Object} selectedItem The object data structure for the menu item + */ + createItemWithSelectedFlavors: function(flavorsArray, selectedItem) { + + //TODO support arbitrary depth flavor nesting !! ditto for flavors.js + //FIXME selectedItem is not a backbone model anymore + for(var i = 0, subItems = selectedItem.flavors.items; i < subItems.length; i++) { + var subItem = subItems[i]; + for(var j = 0, subSubItems = subItem.flavors.items; j < subSubItems.length; j++) { + var subSubItem = subSubItems[j]; + var subSubId = subSubItem.id; + if(!_.include(flavorsArray, subSubId)) { + subItem.flavors.items = _.difference(subItem.flavors.items, [subSubItem]) + } + } + } + //all unselected flavors have been removed from the selectedItem. + var selectedItemModel = new core.models.Item(selectedItem); + return selectedItemModel; + }, + + /** + * Schedule this.calculateTotalPrice() for asynchronous execution, allowing the UI to update state + * @param {Object} evt The event object + */ + recalculatePrice: function(evt) { + var me = this; + setTimeout(function() { + me.calculateTotalPrice(); + }, 1); + + }, + + /** + * Returns the ID's of flavors that have been selected + * @return {Array} An array of stirng, one for each ID + */ + getSelectedFlavors: function() { + var checkedInputs = this.$el.find("form input:checked"); + var flavorIds = _.map(checkedInputs, function(input) { + return $(input).data('flavor-id') + ''; + }); + return flavorIds; + }, + + /** + * Updates the total price displayed in the view. + */ + calculateTotalPrice: function() { + var selectedFlavors = this.$el.find("form input:checked"); + var me = this; + var newPrice = this.basePrice; + + + _.each(selectedFlavors, function(elem) { + var id = $(elem).data('flavor-id') + ''; + var selectedFlavor = this.allFlavors.find(function(el){ + return el.get('id') == id; + }); + if(selectedFlavor && selectedFlavor.get('sizes')) { + var flavorPrice = selectedFlavor.get('sizes').at(0).get('price'); + newPrice += flavorPrice; + } + }, me); + this.setDisplayedPrice(core.utils.formatPriceWithoutCurrency(newPrice)); + }, + + /** + * Sets the displayed price to a specific value + * @param {String} price The price to be set + */ + setDisplayedPrice: function(price) { + this.$el.find(".total_price h3.right").text(price); + }, + + demo: function() { + var demoItem = { + "flavors": { + "items": [{ + "flavors": { + "items": [{ + "description": "", + "sizes": [{ + "price": 0.60, + "name": "normal" + }], + "pic": "", + "main_item": false, + "sub_item": true, + "id": "1131061", + "name": "Balsamico" + }, { + "description": "", + "sizes": [{ + "price": 0.60, + "name": "normal" + }], + "pic": "", + "main_item": false, + "sub_item": true, + "id": "1131062", + "name": "Caesar*1" + }], + "id": "76666", + "structure": "0" + }, + "description": "", + "sizes": [], + "pic": "", + "main_item": false, + "sub_item": false, + "id": "76666", + "name": "Extra Dressing" + }, { + "flavors": { + "items": [{ + "description": "", + "sizes": [{ + "price": 0.00, + "name": "normal" + }], + "pic": "", + "main_item": false, + "sub_item": true, + "id": "1131059", + "name": "Balsamico" + }, { + "description": "", + "sizes": [{ + "price": 0.00, + "name": "normal" + }], + "pic": "", + "main_item": false, + "sub_item": true, + "id": "1131060", + "name": "Caesar*1 " + }, { + "description": "", + "sizes": [{ + "price": 0.00, + "name": "normal" + }], + "pic": "", + "main_item": false, + "sub_item": true, + "id": "1131057", + "name": "ohne Dressing" + }], + "id": "76665", + "structure": "1" + }, + "description": "", + "sizes": [], + "pic": "", + "main_item": false, + "sub_item": false, + "id": "76665", + "name": "Dressing" + }], + "id": "1633809", + "structure": "-1" + }, + "description": "und Parmesan (inkl. 1 Dressing) ", + "sizes": [{ + "price": 4.50, + "name": "normal" + }], + "pic": "", + "main_item": true, + "sub_item": false, + "id": "1633809", + "name": "Gemischter Salat mit H\u00e4hnchenbrustfilet " + } + + var demoItemModel = new core.models.Item(demoItem); + + var flb = new core.views.FlavorsLightbox(); + var $result = $('Have a Gemischter Salat!').click( function() { + flb.show(demoItemModel); + flb.on("flavorsChosen", function(flavorsObj) { + //me.cartCollection.add(flavorsObj.selectedItemModel); //just a demo! + }); + }); + return $result; + } +}); \ No newline at end of file diff --git a/utilitybelt/core/views/Lightbox/Login.js b/utilitybelt/core/views/Lightbox/Login.js new file mode 100644 index 0000000..dcc0c54 --- /dev/null +++ b/utilitybelt/core/views/Lightbox/Login.js @@ -0,0 +1,43 @@ +/** + * Lightbox with the Login Form inside + * + * Examples: + * + * var lb = new core.views.LoginLightbox({ title: jsGetText('login') }); + * lb.show(); + * + * @extends core.views.Lightbox + */ +core.define('core.views.LoginLightbox', { + extend: 'core.views.Lightbox', + template: 'Lightbox/small.html', + + /** + * Constructor. + * @method initialize + * @constructor + * @param {Object} options Options passed to the login form view + */ + initialize: function(options) { + this.model = options.user || new core.models.User(); + this.options.model = this.model; + this.model.on('authorize:grant', _.bind(this.hide, this)); + this.on('render', function() { + var lb = this; + this.addItem(core.views.LoginForm, this.options); + }); + core.views.LoginLightbox.__super__.initialize.call(this); + }, + + /** + * Demo code. + */ + demo: function() { + var $result = $('Click Me') + .click(function() { + var lb = new core.views.LoginLightbox({ title: jsGetText('login_title') }); + lb.show(); + }); + return $result; + } +}); diff --git a/utilitybelt/core/views/Lightbox/MultipleLocation.js b/utilitybelt/core/views/Lightbox/MultipleLocation.js new file mode 100644 index 0000000..2914e19 --- /dev/null +++ b/utilitybelt/core/views/Lightbox/MultipleLocation.js @@ -0,0 +1,181 @@ +/** + * Lightbox with the User Location List inside + * + * Examples: + * + * var lb = new core.views.MultipleLocationLightbox(); + * lb.show(); + * + * @extends core.views.Lightbox + */ + +core.define('core.views.MultipleLocationLightbox', { + + extend : 'core.views.Lightbox', + + template : ["MultipleLocation/MultipleLocation.html"], + + events : { + "click .top-box .close-icon" : "hide", + "click .closeLB.button" : "hide" + }, + + options: { + closeButton: true, + blanketId: 'dark_blanket' + }, + + __shown : false, + + className : 'MultipleLocationLightbox', + + /** + * Constructor + * @constructor + * @param {Object} options Options + */ + initialize : function(options) { + this.locations = this.options.locations; + var me = this; + this.title = jsGetText("multiple_location_found"); + + this.searchTerm = this.options.searchTerm; + this.on('render', function() { + //adding the map and the list + me.map = me.addItem(core.views.Map, { + locations : me.locations + }, ".maps"); + me.map.on("markerClick", me.locationSelectHandler, me); + + var listOptions = { + locations : me.locations, + getBackgroundCSS : me.getBackgroundCSS + }; + me.map.render(); + me.list = me.addItem(core.views.MultipleLocationList, me.options, ".streets"); + me.list.on("location:selected", me.locationSelectHandler, me); + me.list.render(); + + core.utils.trackingLogger.log('search', 'search_event_popup_suggestions_display', this.searchTerm, me.locations.length); + }); + core.views.MultipleLocationLightbox.__super__.initialize.call(this); + }, + addItem : function(type, config, place) { + if (place != null) { + var positionElement = $('.body-box', this.$el).find(place); + var new_el = new type(config); + $(positionElement).html(new_el.$el); + return new_el; + } else if (this.getBody) { + core.views.MultipleLocationLightbox.__super__.addItem.call(this, type, config); + } + }, + /** + * setLocationList + * @param {Object} list + */ + setLocationList : function(list) { + this.locationList = list; + }, + /** + * + */ + getLocationList : function() { + return this.locationList; + }, + /** + * locationSelectHandler + * @param {Object} elem + */ + locationSelectHandler : function(elem) { + core.utils.trackingLogger.log('search', 'search_event_popup_suggestions_select', this.searchTerm, this.locations.length); + this.trigger("location:selected", elem); + }, + hide : function() { + //TODO as soon as we have a container object which keeps children we can add them here + if (this.map) { + this.map.hide(); + } + if (this.list) { + this.list.remove(); + } + this.__shown = false; + core.views.MultipleLocationLightbox.__super__.hide.call(this); + }, + show : function() { + this.__shown = true; + core.views.MultipleLocationLightbox.__super__.show.call(this); + }, + isShown : function() { + return this.__shown; + }, + demo : function() { + + var locations = { + "pagination" : { + "total_items" : 2, + "limit" : 10, + "total_pages" : 1, + "page" : 1, + "offset" : 0 + }, + "data" : [{ + "uri_search" : "http://mockapi.lieferheld.de/restaurants/?city=berlin&lat=45.23234&long=37.77234", + "address" : { + "city_slug" : "berlin", + "city" : "Berlin", + "street_number" : "60", + "latitude" : 45.232340000000001, + "country" : "DE", + "street_name" : "Mohrenstrasse", + "zipcode" : "10117", + "longitude" : 37.77234 + } + }, { + "uri_search" : "http://mockapi.lieferheld.de/restaurants/?city=berlin&lat=46.2004&long=37.88774", + "address" : { + "city_slug" : "berlin", + "city" : "Berlin", + "street_number" : "59", + "latitude" : 46.40000000000002, + "country" : "DE", + "street_name" : "Mohrenstrasse", + "zipcode" : "10117", + "longitude" : 37.887740000000001 + } + }, { + "uri_search" : "http://mockapi.lieferheld.de/restaurants/?city=berlin&lat=46.2004&long=37.88774", + "address" : { + "city_slug" : "berlin", + "city" : "Berlin", + "street_number" : "59", + "latitude" : 46.200400000000002, + "country" : "DE", + "street_name" : "Mohrenstrasse", + "zipcode" : "10117", + "longitude" : 37.987740000000001 + } + }, { + "uri_search" : "http://mockapi.lieferheld.de/restaurants/?city=berlin&lat=46.2004&long=37.88774", + "address" : { + "city_slug" : "berlin", + "city" : "Berlin", + "street_number" : "59", + "latitude" : 46.400400000000002, + "country" : "DE", + "street_name" : "Mohrenstrasse", + "zipcode" : "10117", + "longitude" : 37.987740000000001 + } + }] + }; + + var $result = $('Click Me').click(function() { + var lb = new core.views.MultipleLocationLightbox({ + locations : locations.data + }); + lb.show(); + }); + return $result; + } +}); diff --git a/utilitybelt/core/views/Lightbox/Password.js b/utilitybelt/core/views/Lightbox/Password.js new file mode 100644 index 0000000..2f10416 --- /dev/null +++ b/utilitybelt/core/views/Lightbox/Password.js @@ -0,0 +1,46 @@ +/** + * Lightbox with the "password forgotten" form + * + * Examples: + * + * var lb = new core.views.PasswordLightbox(); + * lb.show(); + * + * @extends core.views.Lightbox + */ +core.define('core.views.PasswordLightbox', { + extend: 'core.views.Lightbox', + template: 'Lightbox/base.html', + + + /** + * Constructor. + * @method initialize + * @constructor + * @param {Object} options Options passed to the login form view + */ + initialize: function(options) { + this.model = options.user; + var lb = this; + this.on('render', function() { + this.addItem(core.views.PasswordForm, this.options); + this.formView = this.getItem(); + this.formView.on("hide",function(){ + lb.hide(); + }); + }); + core.views.LoginLightbox.__super__.initialize.call(this); + }, + + /** + * Demo code. + */ + demo: function() { + var $result = $('Click Me') + .click(function() { + var lb = new core.views.PasswordLightbox({ title: jsGetText('change_details') }); + lb.show(); + }); + return $result; + } +}); diff --git a/utilitybelt/core/views/Lightbox/PasswordForgotten.js b/utilitybelt/core/views/Lightbox/PasswordForgotten.js new file mode 100644 index 0000000..336e37e --- /dev/null +++ b/utilitybelt/core/views/Lightbox/PasswordForgotten.js @@ -0,0 +1,61 @@ +core.define('core.views.PasswordForgotten', { + + extend: 'core.views.Lightbox', + + template: ["User/PasswordForgotten.html"], + + events: { + 'click .button': 'resetPassword', + 'submit .passwordForgottenForm': 'resetPassword', + 'click .top-box .close-icon': 'hide' + }, + + hide: function(){ + delete this.options.errorMessage; + core.views.PasswordForgotten.__super__.hide.call(this); + }, + + resetPassword: function(event){ + event.preventDefault(); + var val = this.$("input.input-default").val(); + this.hide(); + this.reset(val); + }, + + /** + * resets the password via backbone model by the given email address + * @param {Object} email + */ + reset: function(email){ + var me = this; + var model = this.options.Authorization || new core.models.Authorization(); + model.save({ + "email": email, "op": "reset" + },{ + success: function(model, response){ + if (response != null && response.errors != null){ + _.each(response.errors, function(e){ + if (e.error_code != null){ + var errorCode = parseInt(e.error_code); + switch(errorCode){ + case 617: + case 616: + me.show(); + me.options.errorMessage = jsGetText("password_forgotten_not_found"); + break; + default: + me.show(); + me.options.errorMessage = jsGetText("error_happened_try_again"); + } + return; + } + }); + } + //TODO show the login box, was not in the master right now + }, error: function(){ + me.show(); + } + } + ); + } +}); diff --git a/utilitybelt/core/views/Lightbox/Throbber.js b/utilitybelt/core/views/Lightbox/Throbber.js new file mode 100644 index 0000000..a290eea --- /dev/null +++ b/utilitybelt/core/views/Lightbox/Throbber.js @@ -0,0 +1,166 @@ +/** + * Lightbox for showing the Loading Animation with or without blanket + * + * Examples: + * + * @extends core.views.Lightbox + */ + +core.define('core.views.Throbber', { + + extend: 'core.views.Lightbox', + + template: ["Throbber/Throbber.html"], + + events: { + }, + /** + * fadeInTime: fading in time[ms] for the animated image + * fadeOutTime: fading out time[ms] for the animated image + * delay: the delay time[ms] for starting the fading in of the animated image + * the blanket is not affected by this settng, it will be shown immediately + */ + options: { + fadeInTime: 250, + fadeOutTime: 150, + fadeDelay: 2000, + blanketId: "dark_blanket", + auto: true, + position: "fixed" + }, + + // timer for user callback + __timerCallback: null, + + initialize: function() { + var me = this, + timeout = this.options.fadeDelay; + this.timer = false; + if (this.options.auto) { + $(document).ajaxStop(function(event, xhr, error) { + clearTimeout(me.timer); + me.timer = false; + _.defer(function() { + me.hide(); + }); + }).ajaxStart(function(event, xhr, error) { + me.timer = _.delay(_.bind(me.show, me), timeout); + }); + } + core.views.Throbber.__super__.initialize.call(this); + }, + + /** + * handles the event when the window is resized + * calls center + * @param {Object} event includes the me object + */ + onResize: function(event) { + event.data.me.center.call(event.data.me); + }, + + initPosition: function() { + this.center(); + }, + /** + * calculates the center of the screen for the given image + */ + center: function() { + //center the animation picture + var image = this.getAnimImg(); + image.css({ + left: Math.round(($(window).innerWidth() - image.width() ) / 2) + "px", + top: Math.round(($(window).innerHeight() - image.height() ) / 2) + "px" + }); + }, + /** + * removes the blanket and the animated image + */ + hide: function() { + var me = this; + if (me.__timerCallback) { + clearTimeout(me.__timerCallback); + } + if (!this.isVisible()) { + return; + } + var img = me.getAnimImg(); + img.fadeOut(me.options.fadeOutTime, function() { + core.views.Throbber.__super__.hide.call(me); + $(window).off("resize", me.onResize); + }); + }, + /** + * fetches the img node inside the template + */ + getAnimImg: function() { + return this.$(".throbber"); + }, + /** + * setting the fading in duration + */ + setFadeInTime: function(value) { + if ( typeof value == 'number' && value >= 0) { + this.options.fadeInTime = value; + } + }, + /** + * setting the fading out duration + */ + setFadeOutTime: function(value) { + if ( typeof value == 'number' && value >= 0) { + this.options.fadeOutTime = value; + } + }, + /** + * setting the delay time to start fading in + */ + setDelay: function(value) { + if ( typeof value == 'number' && value >= 0) { + this.options.delay = value; + } + }, + /** + * setting the url of the image + */ + setImage: function(url) { + var image = this.getAnimImg(); + image.attr("src", url); + }, + /** + * setting the blanket id + * currently supported: + * - dark_blanket + * - light_blanket + */ + setBlanketId: function(blanketId) { + if (blanketId) { + this.options.blanketId = blanketId; + } + }, + + /** + * shows the blanket and starts the timeout using options.delay + * @see options.delay + */ + show: function(time, callback) { + var me = this; + if (this.isVisible()) { + return; + } + core.views.Throbber.__super__.show.call(me); + var img = me.getAnimImg(); + $(window).on("resize", { + "me": me + }, this.onResize); + img.fadeIn(me.options.fadeInTime); + me.center(); + //calling the callback function after a while + if (_.isFunction(callback) && _.isNumber(time) && time > 0) { + this.__timerCallback = setTimeout(function() { + callback(); + me.hide(); + }, time); + } + } +}); diff --git a/utilitybelt/core/views/Lightbox/UserAddress.js b/utilitybelt/core/views/Lightbox/UserAddress.js new file mode 100644 index 0000000..722c23c --- /dev/null +++ b/utilitybelt/core/views/Lightbox/UserAddress.js @@ -0,0 +1,61 @@ + /** + * Lightbox with the User Address Form inside + * + * Examples: + * + * var lb = new core.views.UserAddressLightbox({ id: 1, title: jsGetText('save_address') }); // fetch the record from the back-end + * lb.show(); + * + * var lb2 = new core.views.UserAddressLightbox({ record: { ... } }); // use the predefined record + * + * var lb3 = new core.views.UserAddressLightbox({ title: jsGetText('new_address') }); // open empty form + * lb3.on('render', function(lightbox) { // populate with data + * lightbox.getForm().populate( { ... } ) + * }); + * + * @extends core.views.Lightbox + */ + +core.define('core.views.UserAddressLightbox', { + + extend: 'core.views.Lightbox', + +/** + * Initialize + * @method initialize + * @constructor + * @param {Object} options Options + */ + + initialize: function(options){ + + var options = this.options; + + this.on('render', function() { + var lb = this; + this.options.onSuccess = function() { + lb.hide(); + new core.views.UserAddressListLightbox(options).show(); + }; + this.options.onBackLinkClick = function() { + lb.hide(); + new core.views.UserAddressListLightbox(options).show(); + }; + this.addItem(core.views.UserAddressForm, this.options); + }); + + core.views.UserAddressLightbox.__super__.initialize.call(this); + + }, + + demo: function() { + var $result = $('Click Me') + .click( function() { + var lb = new core.views.UserAddressLightbox({ id: 1, title: jsGetText('save_address') }); + lb.show(); + }); + return $result; + } + + +}); \ No newline at end of file diff --git a/utilitybelt/core/views/Lightbox/UserAddressList.js b/utilitybelt/core/views/Lightbox/UserAddressList.js new file mode 100644 index 0000000..3f7586a --- /dev/null +++ b/utilitybelt/core/views/Lightbox/UserAddressList.js @@ -0,0 +1,71 @@ +/** +* Lightbox with the User Addresses List inside +* +* Examples: +* +* var lb = new core.views.UserAddressListLightbox(); +* lb.show(); +* +* @extends core.views.Lightbox + */ + +core.define('core.views.UserAddressListLightbox', { + + extend: 'core.views.Lightbox', + + className: 'UserAddressListLightbox', + +/** + * Initialize + * @method initialize + * @constructor + * @param {Object} options Options + */ + + initialize: function(options){ + + this.title = jsGetText('address_list'); + + var user_id = this.options.user_id; + + this.on('render', function() { + var lb = this; + lb.addTitleLink(jsGetText('new_address')).addClass('new-address').click(function() { + var form = new core.views.UserAddressLightbox({ title: jsGetText('new_address'), user_id: user_id }); + form.show(); + lb.hide(); + }); + this.options.openEditWidget = function(options) { + options.user_id = user_id; + var form = new core.views.UserAddressLightbox(options); + form.show(); + lb.hide(); + }; + + var list = this.addItem(core.views.UserAddressList, this.options); + this.setAddressList(list); + + }); + + core.views.UserAddressListLightbox.__super__.initialize.call(this); + + }, + + setAddressList: function(list) { + this.addressList = list; + }, + + getAddressList: function() { + return this.addressList; + }, + + demo: function() { + var $result = $('Click Me') + .click( function() { + var lb = new core.views.UserAddressListLightbox({ user_id: 1 }); + lb.show(); + }); + return $result; + } + +}); \ No newline at end of file diff --git a/utilitybelt/core/views/Lightbox/ZipCodeBox.js b/utilitybelt/core/views/Lightbox/ZipCodeBox.js new file mode 100644 index 0000000..d5cb1af --- /dev/null +++ b/utilitybelt/core/views/Lightbox/ZipCodeBox.js @@ -0,0 +1,60 @@ +/** + * Lightbox showing the search location input + * + * Examples: + * + * var zb = new core.views.ZipCodeLightbox(); + * zb.show(); + * + * @extends core.views.Lightbox + */ + +core.define('core.views.ZipCodeLightbox', { + + extend : 'core.views.Lightbox', + + template: 'PLZ/zip_code_lightbox.html', + + plugins: { + 'form *': 'placeholder', + '.zip-input input': [ 'autocomplete', { + 'source': 'suggestLocation', + 'select': 'onSelectSuggestedLocation', + 'minLength': 2, + 'position': { offset: '0 -3' }, + 'delay': 300 + } ] + }, + + events : { + "submit .zip-input": "suggestOnEnter", + "click .button.big": "suggestOnEnter" + }, + + mixins: ['core.mixins.AutosuggestLocations'], + + options: { + blanketId: "light_blanket", + blanketClickToClose: true + }, + + __disabled: false, + + className : 'ZipCodeLightbox', + + getSearchInputField: function() { + if (!this.searchInputField) + this.searchInputField = $('.zip-input input'); + return this.searchInputField; + }, + + show: function(restaurantName){ + this.options.renderData = { + "restaurantName": restaurantName + }; + + core.views.ZipCodeLightbox.__super__.show.call(this); + //this.setTitle(jsGetText("location_box_title", restaurantName)); + } + +}); diff --git a/utilitybelt/core/views/MapInterface.js b/utilitybelt/core/views/MapInterface.js new file mode 100644 index 0000000..482eb67 --- /dev/null +++ b/utilitybelt/core/views/MapInterface.js @@ -0,0 +1,56 @@ +/* + * Map Interface + */ +core.define('core.views.MapInterface', { + + extend: 'core.View', + + template: '', + + className: "MapInterface", + + /* + * Constructor + * @returns core.views.Form + * @constructor + */ + initialize: function(options) { + //do the require here... + //throw new Error("this is just an interface and has to be implemented"); + }, + + /** + * + * @param {Object} markers + */ + addMarkers: function(markers) { + // throw new Error("this is just an interface and has to be implemented"); + }, + + /** + * + */ + removeAllMarkers: function() { + // throw new Error("this is just an interface and has to be implemented"); + }, + /** + * + */ + removeMarkers: function() { + // throw new Error("this is just an interface and has to be implemented"); + }, + /** + * @returns + */ + getServiceName: function() { + + }, + + addListener: function(){ + + }, + + demo: function() { + + } +}); diff --git a/utilitybelt/core/views/Maps/GoogleMaps.js b/utilitybelt/core/views/Maps/GoogleMaps.js new file mode 100644 index 0000000..01d7de6 --- /dev/null +++ b/utilitybelt/core/views/Maps/GoogleMaps.js @@ -0,0 +1,151 @@ +/* + * Google map wrapper + */ +core.define('core.views.Map', { + + extend: 'core.views.MapInterface', + + className: "Map", + __loaded: false, + + markers: [], + + /* + * Constructor + * @returns core.views.Form + * @constructor + */ + initialize: function(options) { + var me = this; + this.options = options; + this.__loaded = (window.google != null && window.google.maps != null && window.google.maps.Map != null); + this.on("render", function() { + if(me.__loaded) { + me.showMap(); + } + }); + //do the require here + if(!this.__loaded) { + if(window.mapLoadedObserver == null) { + window.mapLoadedObserver = { + mapLoaded: function() { + $(window.mapLoadedObserver).trigger("loaded"); + delete (window.mapLoadedObserver); + }, + addMapLoadedHandler: function(func, context){ + $(window.mapLoadedObserver).bind("loaded", _.bind(func, context)); + } + }; + var url = 'http://maps.google.com/maps/api/js?sensor=false&callback=window.mapLoadedObserver.mapLoaded'; + if(core.MapConfig != null && core.MapConfig.GOOGLE_MAP_KEY != null && core.MapConfig.GOOGLE_MAP_KEY.length > 0) { + url += '&key=' + core.MapConfig.GOOGLE_MAP_KEY; + } + require([url], function() { + }); + + } + // window.mapCallback = _.bind(function() { + // this.mapLoaded(); + // delete (window.mapCallback); + // }, this); + window.mapLoadedObserver.addMapLoadedHandler(me.mapLoaded, me); + }; + + }, + /** + * needed as a callback function to listen to + */ + mapLoaded: function() { + this.__loaded = true; + this.showMap(); + }, + /** + * shows the map in the dom, because of the internal asynhronous loading of google maps + */ + showMap: function() { + var mapDiv = null; + if(this.options.domNode != null) { + mapDiv = $(this.options.domNode); + } else { + mapDiv = $('.maps'); + } + + if(mapDiv.length != null && mapDiv.length > 0) { + mapDiv = mapDiv[0]; + } + else { + mapDiv = $('

      ')[0]; + } + + // create the map + this.map = new google.maps.Map(mapDiv, { + // center: new google.maps.LatLng(0, 0), + zoom: 13, + mapTypeId: google.maps.MapTypeId.ROADMAP, + navigationControl: true, + navigationControlOptions: { + style: google.maps.NavigationControlStyle.SMALL + } + }); + //adding the markers on the map + this.markers = []; + this.latLngBounds = new google.maps.LatLngBounds(); + var me = this; + this.options.locations.each( function(elem, index) { + var singleMarker = this.createMapMarker(elem.get('address'), index); + google.maps.event.addListener(singleMarker, 'click', function() { + me.handleMarkerClick.call(me, elem); + }); + this.markers.push(singleMarker); + this.latLngBounds.extend(singleMarker.position); + }, this); + //setting the center and the correct zoom level + this.map.setCenter(this.latLngBounds.getCenter()); + this.map.fitBounds(this.latLngBounds); + }, + /** + * handler for the marker clicking on the map + * @param {google.maps.Marker} elem which has been clicked on the map + */ + handleMarkerClick: function(elem) { + this.trigger("markerClick", elem); + }, + /** + * + * @param {Object} address the address object with street, latitude, longitude, etc... + * @param {Number} the index number for knowing which element has been clicked later on + */ + createMapMarker: function(addressObject, index) { + return new google.maps.Marker({ + map: this.map, + title: addressObject.street, + icon: core.utils.getIcon('mapMarker', index), + addressObject: addressObject, + index: index, + position: new google.maps.LatLng(addressObject.latitude, addressObject.longitude) + }); + }, + /** + * removes all markers from the map + */ + removeAllMarkers: function() { + _.each(this.markers, function(marker) { + // for (var marker in this.markers){ + marker.setMap(null); + }); + this.markers = []; + }, + /** + * @return {String} which service is being used + */ + getServiceName: function() { + return "Google Maps"; + }, + /** + * removes all markers and also the element itself from the DOM + */ + hide: function() { + this.removeAllMarkers(); + this.remove(); // TODO: make sure we're remove all Google Map's events as well + } +}); diff --git a/utilitybelt/core/views/Page.js b/utilitybelt/core/views/Page.js new file mode 100644 index 0000000..dc5f815 --- /dev/null +++ b/utilitybelt/core/views/Page.js @@ -0,0 +1,65 @@ +/** + * Basic Page class, every app page should extend it. + */ +core.define('core.Page', { + extend: 'core.View', + + /** + * Constructor. + * Sets up login or user actions for all pages. + */ + initialize: function() { + var user = this.options.user; + + // take over the whole page + this.setElement($('body')); + + // always provide a login lightbox, in case of error 401 + var authorizeTool = new core.views.LoginLightbox({ + user: user, + title: jsGetText('login_title') + }); + + this.on('render', function() { + // set up user action trigger (either login or actions) + var uat = { + model: user, + el: $('.button.login').parent(), + authorizeTool: user.isAuthorized() ? false : authorizeTool + }; + var userAccountTrigger = new core.views.UserAccountTrigger(uat); + + var knownAtLoading = user.isAuthorized(); + user.on('authorize:expire', function(user) { + if (knownAtLoading) { + authorizeTool.show(); + } + else { + user.makeAnonym(); + } + }); + + // set up a lightbox to display for ajax request errors + var errorBox = new core.views.ErrorLightbox(); + + this.$el.ajaxError(function(event, xhr, settings, error) { + if (_.indexOf([500, 502], xhr.status) > -1 && !settings.silent) { + errorBox.show(); + } + else if (401 === xhr.status) { + user.authorizeExpire(); + } + }); + + if (_.has(this.options, 'throbberDelay')) { + var throbber = new core.views.Throbber({ + fadeDelay: this.options.throbberDelay + }); + } + + }); + this.render(); + + core.Page.__super__.initialize.call(this); + } +}); diff --git a/utilitybelt/core/views/Tooltip.js b/utilitybelt/core/views/Tooltip.js new file mode 100644 index 0000000..0aa379a --- /dev/null +++ b/utilitybelt/core/views/Tooltip.js @@ -0,0 +1,62 @@ +/** + * Tooltip + * @requires core.View + */ +core.define('core.views.Tooltip', { + + extend: 'core.View', + + className: "Tooltip", + + /** + * Define handlers for mouse interactions (click, mouseenter, mouseleave) + */ + setHandlers: function() { + var out; + this.options.anchor.mouseleave( function() { + out = setTimeout( function() { + $el.fadeOut(600); + }, 300); + }); + + $(document).click( function() { + $el.fadeOut(600); + }); + + $el.mouseenter( function(event) { + clearTimeout(out); + event.stopPropagation(); + $el.mouseleave( function(event) { + $el.fadeOut(600); + }); + }); + + }, + + /* + * Adjust Tooltip position according to the anchor + * @param {Object} anchor Anchor element (for example, link which should be clicked to display the tooltip) + */ + adjustPosition: function(anchor) { + var pos = anchor.position(), // offset doesn't calculate position correctly + height = anchor.height(), + width = anchor.width(); + $el.css({ "left": (pos.left) - (width/2 - 50) + "px", "top": pos.top + height - 5 + "px", position: 'absolute', 'z-index': 999999 }); + }, + + /* + * @constructor + */ + initialize: function() { + $el = this.el; + + this.adjustPosition(this.options.anchor); + + this.setHandlers(); + + $el.fadeToggle(600); + + if (this.options.click_event) + this.options.click_event.stopPropagation(); + } +}); diff --git a/utilitybelt/core/views/View.js b/utilitybelt/core/views/View.js new file mode 100644 index 0000000..59b3914 --- /dev/null +++ b/utilitybelt/core/views/View.js @@ -0,0 +1,123 @@ +/** + * Basic core View, all views (widgets) should extend it + * @param {String} template Template code + */ +core.define('core.View', { + + extend: 'Backbone.View', + + /* + * Plugins to be bound to the view after rendering. + * Describe plugins like this: + * selector: [plugin name, opt1, opt2, ...] + * Options will be passed to the plugin function with apply(). + */ + plugins: {}, + children: [], + + /** + * Runs on widget's render, triggers 'render' event + */ + render: function() { + + this.delegateEvents(); + + if (this.options && this.options.events) { + this.delegateEvents(_.extend(this.events, this.options.events)); + } + + var me = this; + me.rendered = true; + setTimeout( function() { + me.trigger('render'); + me.bindPlugins(); + }, 1); + return this; + }, + + /* + * Renders widget to specified cotainer + * @param {jQuery} ct Jquery container to render to + */ + renderTo: function(ct) { + var me = this; + this.on('render', function() { + ct.empty().append(me.$el); + }); + }, + + + /** + * Binds the plugins to the view using the plugins property. + * The property has to describe which element, which plugin and how to attach it: + * selector1: [plugin1, opt1, opt2, ...], + * selector2: [plugin2, opt3, opt4, ...] + * + * Option can be both primitive types and callbacks: + * + * 'input': [ 'autocomplete', { + * 'source': 'suggestLocation', // will be bind to suggestLocation method of the View + * 'minLength': 2 + * } ] + * + * With no options, the array can be omitted: + * selector1: plugin1 + * + * The plugin is first looked up in the view itself, then in $.fn, + * enabling overwritting for complex config. + */ + bindPlugins: function() { + var me = this; + for (var s in me.plugins) { + var plugin = me.plugins[s], + opts = []; + if (typeof plugin == 'object') { + opts = plugin.slice(1); + _.each(opts, function(opt) { + for (var key in opt) { + var fn = opt[key]; + if (me[fn]) + opt[key] = _.bind(me[fn], me); + }; + }) + plugin = plugin[0]; + } + var fn = plugin in me ? me[plugin] : $.fn[plugin]; + fn.apply(me.$el.find(s), opts); + } + }, + + /** + * Adds an instance of the "type" widget into the body (container returned by the getBody() method) + * Keeps a reference to the added item in the children property. + * @param {Class} type Class name which should be added + * @param {Object} config Class config options + * @returns added element + */ + addItem: function(type, config) { + if (this.getBody) { + var $body = this.getBody(); + var el = 'cid' in config ? config : new type(config); + this.children.push(el); + $body.append(el.$el); + return el; + } + }, + + /** + * Returns the last of the view children. + * TODO handle multiple children. + * + * FIXME somehow, in the UB_docs, a view is added twice for a box. + * Apparently, the good reference (which has its markup in the DOM) is the last one. + * + * @return {Object} The last added child + */ + getItem: function() { + return this.children[this.children.length - 1]; + }, + + demo: function() { + return $(''); + } +}); diff --git a/utilitybelt/core/views/View/ActiveAddress.js b/utilitybelt/core/views/View/ActiveAddress.js new file mode 100644 index 0000000..9117e02 --- /dev/null +++ b/utilitybelt/core/views/View/ActiveAddress.js @@ -0,0 +1,134 @@ +/** + * Example: + */ +core.define('core.views.ActiveAddress', { + extend: 'core.View', + + events: { + 'click a.change' : 'showSearchWidget' + }, + + options: { + el: 'section.filter', + searchPlaceholder: '.search_placeholder', + marginTarget: '.restaurant-count, .landing_page', + marginClass: 'margin-top-filter', + changeSelector: 'a.change', + cookieName: 'active_address', + closeable: true + }, + + /** + * Constructor + */ + initialize: function() { + var me = this; + this.initLocationSearchWidget(); + this.hideSearchWidget(true); + if (!this.options.activeAddress) { + me.showSearchWidget(); + } + }, + + /** + * Initialise Location Search widget and register event listeners + */ + initLocationSearchWidget: function() { + var me = this, + ls = { + renderToEl : $(me.options.searchPlaceholder), + showCloseButton: this.options.closeable + }; + me.locationSearchWidget = new core.views.LocationSearch(ls); + me.locationSearchWidget.on("locationFound", this.locationFound); + me.locationSearchWidget.on("locationNotFound", function(params){ + core.utils.trackingLogger.logError('search_error_nomatch_rlist', params.searchValue); + me.showError.call(me); + }); + me.locationSearchWidget.on("locationFetchError", function(){ + me.showError.call(me); + }); + + me.locationSearchWidget.on('toggle:hide', function(searchWidget) { + searchWidget.$el.fadeOut(300, function() { + me.trigger('toggle:hide:complete'); + $(me.options.marginTarget).removeClass(me.options.marginClass, 300); + me.$(me.options.changeSelector).fadeIn(); + }); + }); + + me.locationSearchWidget.on('toggle:show', function(searchWidget) { + $(me.options.marginTarget).addClass(me.options.marginClass, 300, function() { + searchWidget.$el.fadeIn(300, function() { + me.trigger('toggle:show:complete'); + me.$(me.options.changeSelector).fadeOut(); + }); + }); + }); + }, + + /** + * Location Found event handler + */ + locationFound: function(ev) { + var me = this; + if (ev && ev.collection) { + if (ev.collection.length > 1) { + if (me.multiLocationLB && me.multiLocationLB.isShown()) { + return; + } + me.multiLocationLB = new core.views.MultipleLocationLightbox({ + locations : ev.collection, + position : "absolute", + searchTerm: ev.searchTerm + }); + me.multiLocationLB.on('location:selected', function(item) { + me.multiLocationLB.hide(); + core.utils.redirectLocation(item); + }); + me.multiLocationLB.show(); + } else if (ev.collection.length == 1) { + core.utils.redirectLocation(ev.collection.first()); + } + } + }, + + /** + * Show Location Search Widget Wrapper + */ + showSearchWidget: function(ev) { + if (ev) { + ev.preventDefault(); + } + this.locationSearchWidget.show(); + }, + + /** + * Hide Location Search Widget Wrapper + */ + hideSearchWidget: function(bypass) { + if (bypass) { + this.locationSearchWidget.$el.hide(); + this.trigger('toggle:hide:complete'); + } else { + this.locationSearchWidget.hide(); + } + }, + + /** + * Show errors + */ + showError : function() { + var messageLightbox = new core.views.Lightbox({ + title : jsGetText("search_not_found_title"), + content : jsGetText("search_not_found_message"), + template : "Lightbox/small.html" + }); + var me = this; + messageLightbox.on("hide", function(){ + me.locationSearchWidget.__disabled = false; + }); + this.locationSearchWidget.__disabled = true; + messageLightbox.show(); + } +}); \ No newline at end of file diff --git a/utilitybelt/core/views/View/Cart.js b/utilitybelt/core/views/View/Cart.js new file mode 100644 index 0000000..f40eefd --- /dev/null +++ b/utilitybelt/core/views/View/Cart.js @@ -0,0 +1,229 @@ +/** + * View for displaying a cart + * + * Example: + * + * var cart = new core.views.Cart(); + * cart.renderTo($('#some_element')); + * + * @param {Cart} cart The linked Cart item + */ +core.define('core.views.Cart', { + extend: 'core.View', + className: 'Cart', + events: {}, + options: { + readOnly: false, + preorder: false, + collection: false, + mainTpl: 'Cart/Cart.html', + itemTpl: 'Cart/Item.html', + detailsTpl: 'Cart/Details.html' + }, + + /** + * Constructor, sets collection and templates. + * Builds partials to provide shortcuts for modifying item quantities. + */ + initialize: function() { + // partials for incrementing and decrementing quantities + this.quantityInc = _.bind(this.quantityUpdate, this, 1); + this.quantityDec = _.bind(this.quantityUpdate, this, -1); + this.collection = this.options.collection || new core.collections.Cart(); + var me = this; + // copied from parent class to avoid unbinding relevant events + this.collection.on('reset change', function() { + me.render(); + }, this.collection); + // let's bind and trigger stuff when template is finally loaded + core.utils.getTemplate([me.options.mainTpl, me.options.itemTpl, me.options.detailsTpl], function(main, item, details) { + me.mainRender = _.template(main); + me.itemRender = _.template(item); + me.detailsRender = _.template(details); + me.collection.on('change sync reset', _.bind(me.onItemsChange, me)); + me.collection.order.on('order:changeAddress', _.bind(me.onItemsChange, me)); + // (re)render item row for each addition or quantity modification + me.collection.on('quantity:increase quantity:decrease item:add', function(item, delta) { + me.unRenderItem('.empty'); + me._renderDetails(); + me.renderItem(item.toJSON()); + }); + // remove cart row for item removals + me.collection.on('item:remove', function(item, delta) { + if (this.length) { + me.unRenderItem(item.get('id')); + me._renderDetails(); + } + }); + // display empty cart if emptied + me.collection.on('reset', function() { me.renderEmpty(); }); + if (!me.options.readOnly) { + _.extend(me.events, { + 'click .cart-plus': 'quantityInc', + 'click .cart-minus': 'quantityDec', + 'click a.checkout': '_checkout' + }); + } + // trigger change and rendering + me.collection.trigger('change'); + }); + }, + + /** + * Runs when cart items change (add, remove, empty, save state) + */ + onItemsChange: function() { + this.collection.order.get('price_details').updatePriceDetails(); + this.render(); + this.toggleCheckoutButton(this.collection.isReadyForSync()); + }, + + /** + * Toggles checkout button state. + * @param {Boolean} enabled Boolean tellin wether the button must be enabled or not + */ + toggleCheckoutButton: function(enabled) { + var btn = this.$('.checkout'), cls = 'disabled'; + if (enabled) + btn.removeClass(cls); + else + btn.addClass(cls); + }, + + /** + * Renders widget using the collection and the templates + * @return {core.views.Cart} this + */ + render: function() { + var me = this, + collection = me.collection; + var records = collection.toJSON(); + me.$el.empty().append(me.mainRender({cart: this.options})); + if (collection.length) { + this._renderDetails(); + _.each(records, function(item) { + me.renderItem(item); + }); + } else { + me.renderEmpty(); + } + return core.views.Cart.__super__.render.call(this); + }, + + /** + * Refreshes or adds a new item to the cart. + * @param {Object} item A hash (produced by CartItem.toJSON()) + * @return {core.views.Cart} this + */ + renderItem: function(item) { + item.price = core.utils.formatPrice(item.price); + var markup = this.itemRender({ + cartItem: item, + cart: { + readOnly: this.options.readOnly + } + }); + var current = this.$('.' + item.id); + if (current.length) { + current.replaceWith($(markup)); + } else { + this.$('ul .meta:first').before(markup); + } + return this; + }, + + /** + * Refreshes or adds a detail rows to the cart. + * @return {core.views.Cart} this + */ + _renderDetails: function() { + var priceDetails = this.collection.order.get('price_details').viewJSON(); + var markup = this.detailsRender(priceDetails); + this.$('ul .meta').remove(); + this.$('ul').append(markup); + return this; + }, + + unRenderItem: function(id) { + this.$('ul li.' + id).remove(); + if (!this.collection.length) { + this.renderEmpty(); + } + }, + + /** + * Runs when cart is empty. + * Note that fees should still be displayed: + */ + renderEmpty: function() { + var cartItems = this.$('ul').empty(); + $('
    • ').addClass('empty span-12').text(jsGetText("cart_empty")).prependTo(cartItems); + if (this.collection.hasFee()) { + this._renderDetails(); + } + }, + + /** + * Cart item spinner controls handler. + * Its only business is to identify the modified element + * and ask the collection to update accordingly. + * Everything else is done by the handlers bound to the collection. + * @param {int} inc The increment (positive or negative) to add to the current quantity + * @param {Object} e A jQuery event with a current target having a "name" attribute + */ + quantityUpdate: function(inc, e) { + var input = $(e.currentTarget).parents('li').children(':input'), + itemId = input.attr('name').split('|').pop(), + q = parseInt(input.val()) + inc; + input.val(q); + this.collection.update(itemId, q); + }, + + _checkout: function(e) { + if (this.options.checkout && this.collection.isReadyForSync()) { + var me = this; + me.toggleCheckoutButton(false); // disable button to prevent double submit + this.collection.save({ + success: function() { + me.toggleCheckoutButton(false); // disable button to prevent double submit + me.undelegateEvents(); // so cart content can't be changed + me.options.checkout(); + } + }); + } + }, + + /** + * This demo code uses some fixtures to render a cart containing some + * items. + * @return {core.views.Cart} The view used to render a cart + */ + demo: function() { + var items = [ + { + "description": "inkl. 0,15\u20ac Pfand", + "sizes": [{"price": 5, "name": "L"}], + "pic": "", + "main_item": true, + "sub_item": false, + "id": "mp1", + "name": "Fanta*1,3,5,7 0,5L " + }, { + "description": "inkl. 0,15\u20ac Pfand", + "sizes": [{"price": 20, "name": "XL"}], + "pic": "", + "main_item": true, + "sub_item": false, + "id": "mp2", + "name": "Pain saucisse" + } + ]; + var cart = new core.collections.Cart(); + for (var i=0, bound=items.length; idiv").addClass("hide"); + this.$(">."+visibleClass).removeClass("hide"); + } +}); diff --git a/utilitybelt/core/views/View/LocationSearch.js b/utilitybelt/core/views/View/LocationSearch.js new file mode 100644 index 0000000..30850fd --- /dev/null +++ b/utilitybelt/core/views/View/LocationSearch.js @@ -0,0 +1,154 @@ +/** + * Example: + */ +core.define('core.views.LocationSearch', { + extend: 'core.View', + className: 'LocationSearch', + template: ["Search/LocationSearchSingle.html"], + events: { + "submit .big_input": "suggestOnEnter", + 'keypress input[name="location"]': 'enableAutosuggest' + }, + plugins: { + 'form *': 'placeholder', + 'input[name="location"]': [ 'autocomplete', { + 'source': 'suggestLocation', + 'select': 'onSelectSuggestedLocation', + 'minLength': 2, + 'position': { offset: '0 -3' }, + 'delay': 300 + } ] + }, + + mixins: ['core.mixins.AutosuggestLocations'], + + options: { + wide: false, + placeholder: false, + bgBlinkColor : '#FFDCA8', + show: false + }, + + /** + * used for disabling the search when there is already an error popup shown + */ + __disabled: false, + + /** + * fetching the template for search box + * can be extended later on for two search boxes (e.g. in germany) + */ + initialize: function() { + //should be able to fetch the already rendered elements from the page + var me = this; + if (!this.options.placeholder) { + this.options.placeholder = jsGetText("search_input_placeholder"); + } + core.utils.getTemplate(me.template, function(tpl) { + //render the template and put in the correct wording + me.template = _.template(tpl)(me.options); + me.$el = $(me.template); + if (me.options.renderToEl) { + me.renderTo(me.options.renderToEl); + } + me.render(); + + if (!me.options.showCloseButton) { + me.$('.close').remove(); + } else { + me.$('.close').click(function() { + me.hide(); + }); + } + + me.on("render", function() { + me.searchInputField = me.$('input[name="location"]'); + me.searchInputField.attr("placeholder", me.options.placeholder); + + if (me.options.show) { + me.$el.show(); + } + }); + }); + }, + + getSearchInputField: function() { + if (!this.searchInputField) + this.searchInputField = $('input[name="location"]'); + return this.searchInputField; + }, + + /** + * making the element change it background color + * @param {Object} elem the jquery element which is affected by the animation + * @param {Number} maxRun how often should it run + * @param {Object} startingBackgroundColor the starting background color which it will have it again at the end + * @param {Number} currentRun current running index + * @param {String} placeHolderText the placeholder text which is shown inside the input element + */ + makeBling: function(elem, maxRun, startingBackgroundColor, currentRun) { + if ( typeof (currentRun) == "undefined") { + currentRun = 0; + } + if (!currentRun) { + elem.attr("placeholder", ""); + } + var me = this; + $(elem).animate({ + "background-color": me.options.bgClinkColor + }, 200).animate({ + "background-color": startingBackgroundColor + }, 200, function() { + if (currentRun < maxRun - 1) { + me.makeBling(elem, maxRun, startingBackgroundColor, ++currentRun); + } else { + //TODO here it could fire an event for ending the whole animation sequence, then it could be more generic + elem.attr("placeholder", me.options.placeholder); + } + }); + }, + + onEmptyLocation: function() { + var me = this; + this.__disabled = true; + //TODO currently using the lightbox but will be replaced as soon as we have a real tooltip + this.tooltip = new core.views.Lightbox({ + modal: false, + template: "Lightbox/small.html" + }); + this.tooltip.show(); + this.tooltip.setContent(jsGetText("search_location_hint")); + this.tooltip.setTitle(jsGetText("search_location_hint_title")); + setTimeout(function() { + me.tooltip.hide(); + me.__disabled = false; + }, 2000); + var backgroundColor = this.getSearchInputField().css("background-color"); + this.makeBling(this.getSearchInputField(), 3, backgroundColor); + }, + + /** + * Show Location Search Widget + * + */ + show: function() { + this.trigger('toggle:show', this); + }, + + /** + * Hide Location Search Widget + * + */ + hide: function() { + this.trigger('toggle:hide', this); + }, + + demo: function() { + var $placeHolder = $('
      '); + $placeHolder.appendTo($element); + var search = new core.views.LocationSearch({ + renderToEl: $('#search_placeholder') + }); + return $placeHolder; + } +}); diff --git a/utilitybelt/core/views/View/MultipleLocationList.js b/utilitybelt/core/views/View/MultipleLocationList.js new file mode 100644 index 0000000..ddcf365 --- /dev/null +++ b/utilitybelt/core/views/View/MultipleLocationList.js @@ -0,0 +1,83 @@ +/** + * View for Mulitple Location List + * + * Examples: + * + * @extends core.View + */ + +core.define('core.views.MultipleLocationList', { + + extend : 'core.View', + + template : ['MultipleLocation/MultipleLocationList.html'], + + events : { + "click li" : "locationSelectHandler" + }, + + className : 'MultipleLocationList', + + /** + * Initialize + * @method initialize + * @constructor + * @param {Object} options Options + */ + initialize : function(options) { + this.locations = this.options.locations; + var me = this; + var tmpData = { + locations : this.locations, + getBackgroundCSS : function(index) { + return 'background:url("' + core.utils.getIcon('mapMarker', index) + '") 0px 0px no-repeat; padding-left:30px'; + } + }; + core.utils.getTemplate(this.template, function(tpl) { + me.template = _.template(tpl)(tmpData); + me.render(); + }); + }, + + render : function(tpl) { + this.rendered = true; + this.$el = $(this.template); + this.el = this.$el[0]; + this.delegateEvents(); + }, + /** + * + * @param {Object} list + */ + setLocationList : function(list) { + this.locationList = list; + }, + /** + * + */ + getLocationList : function() { + return this.locationList; + }, + /** + * + * @param {Object} elem + */ + locationSelectHandler : function(elem) { + //TODO need to trigger an event + var key = elem.currentTarget.getAttribute("key"); + this.trigger("location:selected", this.locations.models[key]); + }, + /** + * + */ + demo : function() { + //TODO must be implemented for later on + // var $result = $('Click Me').click(function() { + // var lb = new core.views.UserLocationListLightbox({ + // user_id : 1 + // }); + // lb.show(); + // }); + // return $result; + } +}); diff --git a/utilitybelt/core/views/View/Payment.js b/utilitybelt/core/views/View/Payment.js new file mode 100644 index 0000000..077be87 --- /dev/null +++ b/utilitybelt/core/views/View/Payment.js @@ -0,0 +1,217 @@ +/** + * View for displaying the Payment box on the checkout page + * + * Example: + * + * var paymentView = new core.views.Payment(); + * + * @param {Array} payment_methods An array of valid payment methods. See http://mockapi.lieferheld.de/doc/reference/data/payment_method.html + */ +core.define('core.views.Payment', { + extend: 'core.View', + className: 'Payment', + events: { + "click li.paymentBox" : "selectPaymentMethod", + "keypress .order_form_coupon_code" : "scheduleCouponValidation", + "paste .order_form_coupon_code" : "scheduleCouponValidation" + }, + plugins:{ + //'.orderform': 'jqTransform', //disabling to remove flickering when re-rendering view + }, + options: { + template: "Payment/payment.html" + }, + + /** + * @params See Top of the file + */ + initialize: function(options) { + var me = this; + this.order = options.order; + + core.utils.getTemplate(me.options.template, function(tpl) { + me.tpl = tpl; + me.render(); + me.order.on("sync", function(view, response){ + var poppedCoupon = me.loadingCoupons.pop(); + if( poppedCoupon == me.getEnteredCoupon() && !me.loadingCoupons.length){ + me.render(); + if ( me.order.get('validity').coupon == false ){ + core.utils.trackingLogger.logError("tracking:checkout_error_field_coupon", poppedCoupon); + me.focusCoupon(); + } + } + me.trigger("couponValidated"); + }); + }); + + this.loadingCoupons = []; + this.validateCouponDebounced = _.debounce(_.bind(me.validateCoupon, me), 2000); + + this.order.on("payWithPaypal", _.bind(this.payWithPaypal, this)); + core.views.Payment.__super__.initialize.call(this); + }, + + getEnteredCoupon: function(){ + return this.$(".order_form_coupon_code").val(); + }, + + + /** + * Focus the coupon field + */ + focusCoupon: function(){ + var couponField = this.$('.order_form_coupon_code'); + couponField.focus(); + var oldVal = couponField.val(); + couponField.val(''); + couponField.val(oldVal); //moves the caret to the end of the input + }, + + render: function(){ + var me = this; + core.views.Payment.__super__.render.call(this); //is this necessary? + + //temporary fix for DHFE-725 until LH-4007 is implemented + var paypalOpt = _.find(this.options.payment_methods, function(opt){ + return opt.name == "paypal"; + }); + + if ( paypalOpt ) { + this.options.payment_methods = _.filter(this.options.payment_methods, function(opt){ + return opt.name != "credit"; + }); + this.options.payment_methods.push({ + id: paypalOpt.id, + name: "credit" + }); + } + + this.renderedTpl = _.template(this.tpl, { + payment_methods: this.options.payment_methods, + selected_payment: this.paymentMethod, + price_details: this.order.get('price_details').viewJSON(), + validity: this.order.get('validity'), + coupon: this.order.get('coupon').viewJSON(), + enteredCoupon: this.enteredCoupon || "", + canDisplayCoupon: function(){ + return me.paymentMethod.name!="cash"; + } + }); + + this.el.innerHTML = this.renderedTpl; + this.$el.children().jqTransform(); //doing this here and not in 'plugins' to avoid flickering caused by async rendering + this.statusDiv = this.$(".order_form_coupon_status"); + this.trigger("render"); + }, + + /** + * Schedule coupon validation + * @param {Object} evt The event object passed by Backbone + */ + scheduleCouponValidation: function(evt){ + if(evt.which == 13 || evt.charCode == 13){ + evt.preventDefault(); //disable form submission when pressing enter + }else{ + if(evt.charCode===0) return; //firefox triggers keypresses for special characters with charCode==0 + } + + this.setStatusLoading(); + var me = this; + setTimeout(function(){ + //giving time to keypress evt to udpate input value + me.validateCouponDebounced(evt.target.value); + }); + }, + + /* + * Validate a coupon code against the backend + * @param {String} couponValue The coupon value + */ + validateCoupon: function(couponValue){ + this.enteredCoupon = couponValue; + this.trigger("validateCoupon", couponValue); + this.order.set("coupon", { + code: couponValue + },{ + silent: true + }); + this.order.save(); + + this.loadingCoupons.push(couponValue); + }, + + /** + * Update the UI to reflect an "loading" state. + */ + setStatusLoading: function(){ + this.statusDiv.children().hide(); + this.statusDiv.children(".loading").show(); + }, + + /** + * Update the UI to select a payment method when the user clicks a tab + * @param {Object} evt The event object passed by Backbone + */ + selectPaymentMethod: function(evt){ + var $li = $(evt.currentTarget); + var paymentMethodId = $li.data("paymentMethodId"); + var paymentMethodName = $li.data("paymentMethod"); + + this.order.set("payment", { + method: { + id: paymentMethodId, + name: paymentMethodName + } + }); + + if ( !this.paymentMethod || this.paymentMethod.name != paymentMethodName ) { + if(paymentMethodName=="cash"){ + this.order.resetCoupon(); + this.enteredCoupon = ""; + } + this.paymentMethod = { + id: paymentMethodId, + name: paymentMethodName + }; + this.render(); + } + }, + + /** + * Initiates the paypal payment workflow. + * @param {Object} payment The payment information data + */ + payWithPaypal: function(payment){ + var paypal_params = payment.gateway.params[0]; + var return_url = core.utils.formatURL("/order_confirmation/" + this.order.get('id')); //TODO do this better + var cancel_return_url = window.location.href+"?error=paypal"; + + var allParams = _.extend({ + 'button_subtype': 'products', + 'return': return_url, + 'cancel_return': cancel_return_url, + 'notify_url': payment.gateway.notify_url, + 'bn': 'PP-BuyNowBF:btn_buynowCC_LG.gif:NonHosted' + }, paypal_params); + + var paypalForm = $("
      ").attr('action', payment.gateway.url).attr('method', 'post'); + _.each(allParams, function(value, key, list){ + var hiddenInput = $("").attr('name', key).attr('value', value); + paypalForm.append(hiddenInput); + }); + + this.$el.append(paypalForm); + paypalForm[0].submit(); + }, + + /** + * @return {core.views.Payment} The view used to render a Payment + */ + demo: function() { + var paymentView = new core.views.Payment({ + "payment_methods": [{"id": "0", "name": "cash"}] + }); + return paymentView; + } +}); diff --git a/utilitybelt/core/views/View/UserAccountTrigger.js b/utilitybelt/core/views/View/UserAccountTrigger.js new file mode 100644 index 0000000..e8d6052 --- /dev/null +++ b/utilitybelt/core/views/View/UserAccountTrigger.js @@ -0,0 +1,73 @@ +/** + * View for login / my account trigger + * + * @param {core.models.User} model The linked User + */ +core.define('core.views.UserAccountTrigger', { + extend: 'core.View', + className: 'UserAccountTrigger', + events: { + 'click .login': 'showActions', + 'mouseleave': 'hideActions', + 'click li:nth-child(1) a': 'showPasswordLightbox', + 'click li:last-child a': 'logout' + }, + options: {}, + + initialize: function() { + core.views.UserAccountTrigger.__super__.initialize.apply(this, arguments); + this.options.model.on('authorize', _.bind(this.refresh, this)); + + this.passwordLightbox = new core.views.PasswordLightbox({ + title: jsGetText('change_details'), + user: this.options.model + }); + + _.defer(_.bind(this.refresh, this)); + }, + + /** + * Refreshes the view using the user authorization state. + */ + refresh: function() { + if (this.options.model.isAuthorized()) { + this.$('>a').text(jsGetText('header_my_account_link')); + this.$('>ul').hide(); + } + else { + this.$('>a').text(jsGetText('header_login_link')); + } + }, + + render: function() { + return core.views.Cart.__super__.render.call(this); + }, + + showActions: function(event) { + var opts = this.options; + if (opts.authorizeTool && !opts.model.isAuthorized()) { + opts.authorizeTool.show(); + } + else { + var actions = this.$('>ul'); + if (!actions.is(':visible')) { + actions.slideDown('fast'); + } + } + }, + + hideActions: function(event) { + var actions = this.$('>ul'); + if (actions.is(':visible')) { + actions.slideUp('fast'); + } + }, + + showPasswordLightbox: function(){ + this.passwordLightbox.show(); + }, + + logout: function() { + this.options.model.revoke(); + } +}); diff --git a/utilitybelt/core/views/View/UserAddressList.js b/utilitybelt/core/views/View/UserAddressList.js new file mode 100644 index 0000000..749d59b --- /dev/null +++ b/utilitybelt/core/views/View/UserAddressList.js @@ -0,0 +1,135 @@ +/** + * View with the list of User Addresses + * + * Example: + * + * var list = new core.views.UserAddressList(); + * list.renderTo($('#some_element')); + * + * @param {Collection} collection Collection of core.models.Address + * @param {Function} openEditWidget Handler to open the widget for model editing + */ +core.define('core.views.UserAddressList', { + + extend: 'core.View', + className: 'UserAddressList', + template: '', +// user_id: null, + + events: { + 'click .edit-icon': 'editAddress', + 'click .delete-icon': 'deleteAddressConfirm', + 'click .user-address li': 'addressEventPropagate' + }, + + /** + * Constructor, set collection and template + */ + initialize: function() { + + if (!this.options || !this.options.user_id) { + throw('User_id must be set'); + return; + } + + this.collection = this.options.collection || new core.collections.Address(); + this.collection.bindTo(this); + var me = this; + core.utils.getTemplate(['UserAddress/UserAddresses.html'], function(tpl) { + me.template = tpl; + me.collection.setUrlParams({ user_id: me.options.user_id }); + me.collection.fetch(); // should change url first... + }); + + }, + + /** + * Render widget using the collection and the template + */ + render: function() { + var me = this, + collection = me.collection; + + if (!collection.length) { + me.onUserListEmpty(); + } + else { + var col = collection.toJSON(), + tpl = _.template(me.template), + data = { addresses: col }; + + data.get_full_address = function(address) { // TODO: create set of global renderers and process them in generic way + return address.city + ' ' + address.street_name + ' ' + address.street_number; + }; + me.$el.empty().append(tpl(data)); + } + core.views.UserAddressList.__super__.render.call(this); + }, + + /** + * Run when addresses collection is empty + */ + onUserListEmpty: function() { + this.$el.empty().append(jsGetText("no_addresses")); + }, + + /** + * Run when click on "edit user" icon + */ + editAddress: function(e) { + var id = e.target.id; + var rec = this.collection.get(id); + if (rec && rec.id && this.options.openEditWidget) { + this.options.openEditWidget({ record: rec, title: jsGetText('edit_address') }); + } + }, + + /** + * Remove the address from collection and re-render the widget + */ + deleteAddress: function(id) { + var rec = this.collection.get(id); + var me = this; + rec.destroy({ success: function() { + me.collection.remove(); + me.render(); + } }); + }, + + /** + * Run when click on "delete user" icon, show small inline "confirmation" dialog + */ + deleteAddressConfirm: function(e) { + var id = e.target.id, me = this; + core.utils.getTemplate(['UserAddress/DeleteConfirm.html'], function(tpl) { + var html = _.template(tpl); + var ct = $(e.target).parents('li'); + var confirm_message = ct.hide().after(html).next(); + confirm_message.height(ct.height()); + $('.yes', confirm_message).click(function() { + ct.show(); + confirm_message.hide(); + me.deleteAddress(id); + }); + $('.no', confirm_message).click(function() { + ct.show(); + confirm_message.hide(); + }); + }); + }, + + /** + * Delegates an event on a address DOM element to its inner checkox. + * @param {Object} e The event + */ + addressEventPropagate: function(e) { + var eventType = e.handleObj.type; + if (!$(e.target).is('input,a')) + $(e.currentTarget).find('input[type=checkbox]').trigger(eventType); + }, + + demo: function() { + var list = new core.views.UserAddressList(); + return list; + } +}); diff --git a/utilitybelt/core/views/c.js b/utilitybelt/core/views/c.js new file mode 100644 index 0000000..e506082 --- /dev/null +++ b/utilitybelt/core/views/c.js @@ -0,0 +1,14 @@ +// See http://www.jshint.com/docs/ for more options +{ + "curly": true, + "maxlen": 99, + "maxcomplexity": 1, + "latedef": false, + "unused": true, + "undef": true, + "plusplus": false, + "devel": false, + "jquery": true, + "browser": true, + "predef": [ "core", "_" ] +} \ No newline at end of file