/*global Class Timer Application Client History*/ var Application = Class.extend({ initialize: function() { this.clientCallbackHandlers = []; this.views = {}; this.params = {}; this.cache = {}; this.client = {}; this.features = {}; this.currentView = null; this.wrapper = $('#view_wrapper'); this.buffer = $('#tmp'); this.footer = $('#footer'); this.parseClientParams(); // Get hash params if (window.location.hash) { this.params = $.deserialize(location.hash.slice(1)); } this.params = $.extend(window.CONFIG, this.params); this.features.supportsClose = this.getConfigValue('clientSupportsClose', false); this.features.supportsAlert = this.getConfigValue('clientSupportsAlert', false); this.features.supportsNudgeNav = this.getConfigValue('clientSupportsNudgeNav', true); this.features.hasFooter = this.getConfigValue('hasFooter', !!document.getElementById('footer')); if (!this.features.supportsClose) { this.app_is_initializing = true; } // Start timer for game updates // TODO only run this on game views this.timer = new Timer($.proxy(this._fireTimerEvent, this), this.params.defaultTimer); this.timer.start(); }, getConfigValue: function(value, defaultValue) { return this.params.hasOwnProperty(value) ? this.params[value] : defaultValue; }, parseClientParams: function() { var q = $.deserialize( location.search.slice(1) ); $.assert(q.client, '`client` param is required.'); $.assert(q.version, '`version` param is required.'); this.client[q.client] = true; this.client.version = parseFloat(q.version); $('html').addClass('client_'+q.client); }, // Fire a timer event _fireTimerEvent: function() { $(document.body).trigger('timerReachInterval'); }, // Call the view if the method exists _onTimerReachedInterval: function() { var view = this.currentView; if (view && view._onTimerReachedInterval) { view._onTimerReachedInterval.call(view); } }, // Register a view constructor under a specific name. If a link in the application has // a parameter named "view" that points to a registered view name, then the `Application` // will load that view. Optionally, you can pass in parameters, and these will be merged // with any other parameters in the link that triggers view loading (parameters in the // link override parameters in the registration binding). This allows you to bind the // same view under different names with different parameters in order to parameterize // its behavior. // // @param {Object} name // @param {Object} constructor // @param {Object} params (optional) registerView: function(name, constructor, params) { this.views[name] = { constructor: constructor, params: (params || {}) }; }, // Stash a view, i.e. change the url to something meaningful, but do not show the // view based on this URL change. This is needed in cases where the device goes to // sleep, and then reloads upon awakening. stashView: function(params) { $.assert(!this.features.supportsClose, "App should not be using Application.stashView method"); // TODO kill this method in all apps. Need client support. if (!this.features.supportsClose) { console.log('** Application.stashView'); var local = {}; local.view = params.view; if (params.filter) { local.filter = params.filter; } if (params.key) { local.key = params.key; } local.stash = true; var url = "#" + $.serialize(local); document.location = url; } }, // Show a view based on the supplied parameters. The only required parameter is "view" // which must reference a valid view name. // // @param {Object} params showView: function(params) { console.log('$$ Application.showView'); $.assert(params.view, 'No view specified (use /' + this.params.serverPath + '/#view=)'); var binding = this.views[params.view]; $.assert(binding, params.view + ' is not a registered view name. Did you add it to views.js?'); // Merge binding parameters (from views.js) into any parameters that were supplied in the URL for (var prop in binding.params) { if (typeof params[prop] === 'undefined') { params[prop] = binding.params[prop]; } } var view = new binding.constructor(params, this); $.assert(view.create, params.view + ' does not have a create method and is not a valid view'); view.create(params); }, // When the view is done initializing, it must call this method. appendView: function(newView, cached) { console.log('$$ Application.appendView'); // Save the scroll state of the old view if (this.currentView) { this.currentView.params.pageYOffset = window.pageYOffset; } // Re-assign this.currentView to the new view. // Add it to the Application.cache. this.currentView = this.cache[History.currentHash()] = newView; // Mark the view as cached if (cached) { this.currentView.params.cached = true; // NBA3 TODO use the param $(newView.element).addClass('cached'); } this.wrapper.empty().append(newView.element); this.currentView._evaluateDataAttribute(); // Set scroll position. window.scrollTo(0, (this.currentView.params.pageYOffset || 0)); // Reset metrics flag this.currentView.params.hasSentMetrics = false; // Allow the view to add behavior. this.currentView.onViewChanged(this.currentView.params, this); // Send Metrics if (!this.currentView.params.skip_metrics === true) { this.sendMetrics(this.currentView); } // Let plugins know about the view being appended $(document.body).trigger('Application:viewAppended', [this.currentView.params]); // Show the footer if (this.footer && this.features.hasFooter) { this.footer.show(); } // Hide the loader $.hideLoader(); }, sendMetrics: function(view) { Client.notify({ 'action': view.getAction(), 'buttons': view.getButtons(), 'filter': view.getFilter(), 'metric': view.getMetric(), 'refreshAd': view.shouldRefreshAd(), 'section': view.getSection(), 'title': view.getTitle() }); view.params.hasSentMetrics = true; }, storage: function() { // Feature detect localStorage + local reference var storage, fail, uid; try { uid = new Date(); (storage = window.localStorage).setItem(uid, uid); fail = storage.getItem(uid) !== uid; storage.removeItem(uid); fail && (storage = false); } catch(e) {} if (storage) { console.log('=> localStorage is available'); return storage; } else { console.log('=> localStorage is NOT available'); return null; } }, // The web application is receiving a callback from the client to set a value or execute some // behavior. If the view has an `onClientCallback` method, this will be executed first. It can // cancel any further event handling by returning `true`. Otherwise, the application sends the // event to all of the `clientCallbackHandlers` that are registered with the application, in the // order they were registered. // // @param {String} name // @param {Object} value onClientCallback: function(name, value) { var handled = false; var view = this.currentView; if (view && view.onClientCallback) { handled = view.onClientCallback.call(view, name, value); } if (!handled) { this.clientCallbackHandlers.forEach(function(handler) { handler.call(this, name, value); }, this); } }, // load a view from cache or the server when we get a hashchange event _onHashChange: function(event, memo) { if (!this.features.supportsClose) { // If the app is not initializing, and the view is stashed, do not do anything if (!this.app_is_initializing && memo.stash) { console.log('=> App is not initializing & a view is stashed. RETURN'); return; } } $.assert(memo.view, 'The requested URL could not be found. { href: ' + location.href + ' }'); var view = this.cache[History.currentHash()]; if (view) { console.log('@@ Getting view from cache.'); view.params.cached = true; this.appendView(view, true); } else { console.log('@@ Creating view from scratch.'); this.showView(memo); } if (!this.features.supportsClose) { this.app_is_initializing = false; } }, // Initializes application functionality on page ready, and kicks off loading the view. // // @private _onReady: function() { $(document.body) .on('hashchange', $.proxy(Application._onHashChange, this)) .on('timerReachInterval', $.proxy(Application._onTimerReachedInterval, this)) .list_hl('tr[href]', function(e, el) { History.add( $.deserialize( el.attr('href').slice(1) ) ); }); } }); // Singleton. console.log('@@@@ Starting Application'); Application = new Application(); Zepto(function($) { Application._onReady(); });