PageRenderTime 63ms CodeModel.GetById 26ms RepoModel.GetById 0ms app.codeStats 0ms

/client/default/js/app.js

https://bitbucket.org/garmur/salesforce-example
JavaScript | 1145 lines | 851 code | 227 blank | 67 comment | 80 complexity | db6c5701e29589a0d320b69d419d8aef MD5 | raw file
Possible License(s): MIT, BSD-3-Clause, Apache-2.0
  1. // This is a function which allow us to demonstrate the app, which listens
  2. // exclusively to tap events (in order to avoid the slight delays in click
  3. // events mobile browser engines introduce), by firing those tap events when we
  4. // detect a genuine click, while also allowing us to scroll etc...
  5. $('body').on('mousedown', function(event) {
  6. if (!(/mobile/i.test(navigator.userAgent)) && !('ontouchstart' in window)) {
  7. $('body')
  8. .one('mousemove', function() {
  9. $('body').off('mouseup');
  10. })
  11. .one('mouseup', function() {
  12. $(event.srcElement).trigger('tap');
  13. });
  14. }
  15. });
  16. // Allows us to check whether our SalesForce API session should still be valid
  17. // or timed out, with a 5 minute 'insurance' against making bad calls.
  18. function isSessionTimedOut() {
  19. var timeString = localStorage.getItem('timestamp'),
  20. time;
  21. if (timeString) {
  22. time = new Date(timeString);
  23. // Check if it's at least 115 minutes old...
  24. if ((new Date()) - time >= 6900000) {
  25. return true;
  26. }
  27. }
  28. return false;
  29. }
  30. // There's a couple of things we'll always want to do, and provide as arguments,
  31. // when making act request in this app, so we wrap them up and make it DRY.
  32. function doAct(act, req, onSuccess, onFailure) {
  33. var authData = localStorage.getItem('authData');
  34. // TODO: Figure out why this doesn't seem to be kicking in on first load.
  35. window.app.viewport.loadMask(true);
  36. function wrappedSuccess(res) {
  37. window.app.viewport.loadMask(false);
  38. localStorage.setItem('timestamp', (new Date()).toJSON());
  39. onSuccess(res);
  40. }
  41. function wrappedFailure(msg, err) {
  42. window.app.viewport.loadMask(false);
  43. onFailure(msg, err);
  44. }
  45. $fh.act({
  46. act: act,
  47. req: req ? req : authData ? JSON.parse(authData) : undefined
  48. }, wrappedSuccess, onFailure);
  49. }
  50. // Custom Backbone.sync which currently only allows us to fetch all items from
  51. // a collection.
  52. Backbone.sync = function(method, collection, options) {
  53. var act = collection.act;
  54. if (method !== 'read') {
  55. return options.error('Only read ops supported at the moment!');
  56. }
  57. return doAct(act, null,
  58. function(res) {
  59. var resp = res.records;
  60. options.success(resp);
  61. }, options.error);
  62. };
  63. app = (function() {
  64. // We don't need any custom logic for our models or collections, so we can use
  65. // the generic Backbone base classes for everything.
  66. var SalesforceItem = Backbone.Model.extend();
  67. var SalesforceCollection = Backbone.Collection.extend({
  68. model:SalesforceItem
  69. });
  70. // We've decided to implement a custom sorting function for the cases, so we
  71. // have a custom object for this.
  72. var Cases = SalesforceCollection.extend({
  73. act: 'listCases',
  74. comparator: function comparator(aCase) {
  75. return aCase.get('IsClosed') ? 2 : 1;
  76. }
  77. });
  78. // These are the items we're dealing with in this simple example app.
  79. window.collections = {
  80. accounts: new (SalesforceCollection.extend({act:'listAccounts'})),
  81. cases: new Cases(),
  82. campaigns: new (SalesforceCollection.extend({act:'listCampaigns'})),
  83. opportunities: new (SalesforceCollection.extend({act:'listOpps'}))
  84. };
  85. // The main application viewport and visual hub of the app. Because there
  86. // should be only one of these for the app, we render to the page on init.
  87. var AppViewport = Backbone.View.extend({
  88. tagName: 'div',
  89. id: 'app-viewport',
  90. viewport: null,
  91. scrollers: [],
  92. currentView: null,
  93. events: {
  94. 'tap #menu-button': 'toggleMenu',
  95. // TODO: Make these global, providing opt-out based on class.
  96. 'click a': 'stopLink',
  97. 'tap a': 'followLink'
  98. },
  99. // Provide this hardcoded in order to take it out of users HTML file.
  100. // TODO: Provide dynamic menu rendering (_.template() ?)
  101. template: [
  102. '<div id="app-menu">',
  103. '<ul id="app-menu-list">',
  104. '<li><a class="ss-users" href="/#accounts">Accounts</a></li>',
  105. '<li><a class="ss-notebook" href="/#cases">Cases</a></li>',
  106. '<li><a class="ss-stopwatch" href="/#campaigns">Campaigns</a></li>',
  107. '<li><a class="ss-target" href="/#opportunities">Opportunities</a></li>',
  108. '<li><a class="ss-reply" href="/#logout">Logout</a></li>',
  109. '</ul>',
  110. '</div>',
  111. '<div id="viewport-wrapper">',
  112. '<header id="titlebar">',
  113. '<button id="menu-button" class="ss-icon">list</button>',
  114. '<h1>SalesHenry</h1>',
  115. '<button id="context-button" class="ss-icon"><span class="active">sync</span></button>',
  116. '</header>',
  117. '<section id="main-viewport">',
  118. '</section>',
  119. '</div>'
  120. ].join('\n'),
  121. initialize: function() {
  122. _.bindAll(this);
  123. this.$el.html(this.template);
  124. $('body').append(this.$el);
  125. // Store the viewport element, we'll be using it a lot...
  126. this.viewport = this.$('#main-viewport');
  127. if (this.$('#app-menu-list').height() > $(window).height()) {
  128. this.scrollers.push(new iScroll('app-menu'));
  129. }
  130. // Setup phonegap event listener for Android menu button.
  131. document.addEventListener("menubutton", this.toggleMenu, false);
  132. this.on('pagechange', this.closeMenu);
  133. },
  134. setView: function(NewView, attributesToAdd) {
  135. var hash = window.location.hash,
  136. newView,
  137. menuListItems = $('#app-menu-list li'),
  138. menuListLength = menuListItems.length,
  139. i, curItem, titlebar;
  140. // Adjust menu item styling first to make seem extra responsive.
  141. for (i = 0; i < menuListLength; i++) {
  142. curItem = $(menuListItems[i]).find('a');
  143. if (curItem.attr('href').search(hash.split('/')[0]) === 1) {
  144. curItem.addClass('selected');
  145. } else {
  146. curItem.removeClass('selected');
  147. }
  148. }
  149. // Page views should destroy all iScrolls etc. before removal. We don't
  150. // need to do explicit empty() call, because later html() does this.
  151. if (this.currentView && this.currentView.destroy) this.currentView.destroy();
  152. if (attributesToAdd) {
  153. newView = new NewView(attributesToAdd);
  154. } else {
  155. newView = new NewView();
  156. }
  157. this.currentView = newView;
  158. titlebar = (newView.titlebar || newView.options.titlebar);
  159. if (titlebar) {
  160. this.$('#titlebar h1').text(titlebar.title);
  161. this.$('#titlebar').removeClass('hidden');
  162. // This gives us option of adjusting page padding/margin etc. to adjust
  163. // for lack of titlebar.
  164. this.viewport.addClass('with-titlebar');
  165. } else {
  166. this.$('#titlebar').addClass('hidden');
  167. this.viewport.removeClass('with-titlebar');
  168. }
  169. this.viewport.html(newView.render().el);
  170. if (newView.isChild) {
  171. $('#menu-button').html('previous');
  172. } else {
  173. $('#menu-button').html('list');
  174. }
  175. if (newView.afterRender) newView.afterRender();
  176. if (newView.setupScroll) {
  177. setTimeout(newView.setupScroll, 100);
  178. }
  179. },
  180. // Just a simple function to cancel any onclick events, which were following
  181. // through (delayed) from a tap on the menu item.
  182. stopLink: function(event) {
  183. if (/^www/.test(event.srcElement.pathname.substring(1))) {
  184. window.location = 'http://' + event.srcElement.pathname.substring(1);
  185. this.trigger('pagechange');
  186. return false;
  187. }
  188. if (event.srcElement.hash.charAt(0) === '#') {
  189. return false;
  190. }
  191. // If the element is a link tag pointing to a
  192. return (event.srcElement.nodeName === 'A' &&
  193. (event.srcElement.href.substring(0, 4) === 'tel:' ||
  194. event.srcElement.href.substring(0, 4) === 'http') &&
  195. !event.srcElement.hash);
  196. },
  197. // Works around the above, with the added bonus of circumventing the short
  198. // delay between tap and onclick events on mobile.
  199. followLink: function(event) {
  200. if (event.srcElement.hash.charAt(0) === '#') {
  201. window.app.navigate(event.srcElement.hash.substring(1), {trigger: true});
  202. this.trigger('pagechange');
  203. }
  204. // this.toggleMenu();
  205. },
  206. closeMenu: function closeMenu() {
  207. var elem = this.$('#viewport-wrapper'),
  208. width = $(window).width() - this.$('#menu-button').width();
  209. if (elem.css('-webkit-transform') === 'translateX(' + width + 'px)') {
  210. elem.css('-webkit-transform', 'translateX(0)');
  211. }
  212. },
  213. toggleMenu: function toggleMenu(event) {
  214. // TODO: Assess if necessary.
  215. if (event) event.stopPropagation();
  216. if (this.currentView.isChild) {
  217. window.history.back();
  218. return false;
  219. }
  220. var elem = this.$('#viewport-wrapper'),
  221. width = $(window).width() - this.$('#menu-button').width();
  222. console.log(elem.css('-webkit-transform'));
  223. if (elem.css('-webkit-transform') === 'translateX(' + width + 'px)') {
  224. elem.css('-webkit-transform', 'translateX(0)');
  225. } else {
  226. elem.css('-webkit-transform', 'translateX(' + width + 'px)');
  227. }
  228. },
  229. loadMask: function loadMask(display) {
  230. var loaderElem = this.$('#context-button');
  231. if (display) {
  232. loaderElem.addClass('active');
  233. } else {
  234. loaderElem.removeClass('active');
  235. }
  236. }
  237. // TODO: Cache any selectors which are used many times in functions.
  238. });
  239. var AccountsView = Backbone.View.extend({
  240. tagName: 'section',
  241. className: 'page',
  242. id: 'accounts-page',
  243. titlebar: {
  244. title: 'Accounts'
  245. },
  246. initialize: function() {
  247. _.bindAll(this);
  248. if (!collections.accounts.length) {
  249. collections.accounts.fetch();
  250. }
  251. collections.accounts.on('reset', this.render);
  252. },
  253. destroy: function destroy() {
  254. if (this.scroller) {
  255. this.scroller.destroy();
  256. this.scroller = null;
  257. }
  258. },
  259. render: function() {
  260. var that = this,
  261. accountList = $('<ul id="accounts-list"></ul>');
  262. collections.accounts.each(function(account) {
  263. accountList.append((new AccountListItemView({ model: account })).render().el);
  264. });
  265. this.$el.html(accountList);
  266. setTimeout(function() {
  267. new iScroll(that.id);
  268. }, 100);
  269. return this;
  270. }
  271. });
  272. var AccountListItemView = Backbone.View.extend({
  273. tagName: 'li',
  274. events: {
  275. 'tap': 'viewItem'
  276. },
  277. viewItem: function viewItem() {
  278. window.app.navigate('accounts/' + this.model.get('AccountNumber'), {trigger: true});
  279. },
  280. initialize: function initialize() {
  281. this.template = _.template($('#account-list-item-tpl').html());
  282. _.bindAll(this);
  283. },
  284. render: function render() {
  285. this.$el.html(this.template(this.model.toJSON()));
  286. return this;
  287. }
  288. })
  289. // CASES SECTION
  290. // ---------------------------------------------------------------------------
  291. var CasesView = Backbone.View.extend({
  292. tagName: 'section',
  293. className: 'page',
  294. id: 'cases-page',
  295. titlebar: {
  296. title: 'Cases'
  297. },
  298. events: {
  299. 'tap li': 'viewSingle'
  300. },
  301. initialize: function() {
  302. _.bindAll(this);
  303. if (!collections.cases.length) {
  304. collections.cases.fetch();
  305. }
  306. collections.cases.on('reset', this.render);
  307. },
  308. viewSingle: function viewSingle(event) {
  309. window.app.navigate('case/123', {trigger: true});
  310. },
  311. render: function() {
  312. var that = this;
  313. var list = $('<ul id="cases-list"></ul>');
  314. function buildList() {
  315. collections.cases.each(function(acase) {
  316. list.append('<li><span class="' + acase.get('Priority') + '-priority ' + acase.get('IsClosed') + '">' + acase.get('Subject') + '</span></li>');
  317. });
  318. that.$el.html(list);
  319. }
  320. buildList();
  321. setTimeout(function() {
  322. new iScroll(that.id);
  323. }, 100);
  324. return this;
  325. }
  326. });
  327. var SingleCasePage = Backbone.View.extend({
  328. tagName: 'section',
  329. className: 'page',
  330. id: 'single-case-page',
  331. titlebar: {
  332. title: 'Cases'
  333. },
  334. render: function render() {
  335. this.$el.html('<h1>Single Case Page</h1>');
  336. return this;
  337. }
  338. });
  339. var SingleAccountPage = Backbone.View.extend({
  340. tagName: 'section',
  341. className: 'page',
  342. id: 'single-account-page',
  343. model: null,
  344. titlebar: {
  345. title: 'Accounts'
  346. },
  347. events: {
  348. 'tap .account-head': 'openMap'
  349. },
  350. openMap: function openMap() {
  351. window.location = 'http://maps.google.com/maps?q=' + this.getEscapedAddress();
  352. },
  353. initialize: function() {
  354. _.bindAll(this);
  355. this.$el.swipeRight(this.goBack);
  356. },
  357. goBack: function() {
  358. window.history.back();
  359. },
  360. getEscapedAddress: function getEscapedAddress() {
  361. var address = [
  362. this.model.get('BillingStreet'),
  363. this.model.get('BillingCity'),
  364. this.model.get('BillingState'),
  365. this.model.get('BillingCountry')
  366. ].join(', ');
  367. return address.replace(/\s/g, '+');
  368. },
  369. render: function render() {
  370. var that = this,
  371. raw,
  372. i,
  373. width = $(window).width(),
  374. mapUrl = 'http://maps.googleapis.com/maps/api/staticmap?zoom=13&size=' + width + 'x' + Math.round(width * .67) + '&maptype=roadmap&sensor=false&center=';
  375. mapUrl += this.getEscapedAddress();
  376. // If we're on retina, upscale the image to keep quality perfect.
  377. mapUrl += window.devicePixelRatio > 1 ? '&scale=2' : '';
  378. console.log(mapUrl);
  379. this.$el.html(_.template($('#account-item-tpl').html(), this.model.toJSON()));
  380. raw = '<table>';
  381. for (i = 1; i < Object.keys(this.model.attributes).length; i++) {
  382. raw += '<tr><td>' + Object.keys(this.model.attributes)[i] + '</td><td>' + this.model.attributes[Object.keys(this.model.attributes)[i]] + '</td></tr>';
  383. }
  384. raw += '</table>';
  385. $(this.$('div')[0]).append(raw);
  386. this.$('.account-head').css('background-image', 'url(' + mapUrl + ')');
  387. setTimeout(function() {
  388. new iScroll(that.id);
  389. }, 100);
  390. return this;
  391. }
  392. });
  393. var CampaignsView = Backbone.View.extend({
  394. tagName: 'section',
  395. className: 'page',
  396. id: 'campaigns-page',
  397. titlebar: {
  398. title: 'Campaigns'
  399. },
  400. events: {
  401. // 'tap li': 'viewAccount'
  402. },
  403. initialize: function() {
  404. if (!collections.campaigns) {
  405. window.app.viewport.loadMask(true);
  406. $fh.act({
  407. act: 'listCampaigns',
  408. req: JSON.parse(localStorage.getItem('authData'))
  409. }, function(res) {
  410. console.log(res);
  411. collections.campaigns = new SalesforceCollection(res.records);
  412. window.app.viewport.loadMask(false);
  413. }, function(err) {
  414. alert(err);
  415. });
  416. }
  417. },
  418. render: function() {
  419. window.app.viewport.loadMask(true);
  420. var thisScroller;
  421. var that = this;
  422. this.$el.append('<ul id="campaigns-list">');
  423. function buildList() {
  424. collections.campaigns.each(function(campaign) {
  425. $('#campaigns-list').append('<li>' + campaign.get('Name') + '</li>');
  426. });
  427. that.$el.append('</ul>');
  428. thisScroller = new iScroll(that.id);
  429. window.app.viewport.loadMask(false);
  430. }
  431. if (!this.collection) {
  432. setTimeout(buildList, 1000);
  433. console.log(this.id);
  434. } else {
  435. buildList();
  436. }
  437. //if (this.$el.height() - this.$('#accounts-list').height() < 0) {
  438. // thisScroller = new iScroll(this.id);
  439. //}
  440. return this;
  441. }
  442. });
  443. var LoginPage = Backbone.View.extend({
  444. tagName: 'section',
  445. className: 'page',
  446. id: 'login-page',
  447. titlebar: false,
  448. events: {
  449. 'tap #login-message': 'fillForm',
  450. 'focusin input': 'toggleIconColor',
  451. 'focusout input': 'toggleIconColor',
  452. 'keyup input': 'toggleClear',
  453. 'tap .clear-input': 'clearInput',
  454. 'tap #login-button': 'login'
  455. },
  456. fillForm: function fillForm() {
  457. this.$('#email-input').val('gareth.murphy@feedhenry.com');
  458. this.$('#password-input').val('salesforce123lsjLcklI45Vcjm0DQ114M4y2');
  459. this.$('.clear-input').css('display', 'block');
  460. },
  461. toggleClear: function toggleClear(event) {
  462. var elem = event.srcElement;
  463. if (elem.value === '') {
  464. $(elem).siblings('.clear-input').css('display', 'none');
  465. } else {
  466. $(elem).siblings('.clear-input').css('display', 'block');
  467. }
  468. },
  469. initialize: function() {
  470. // We need to bing an extra function to the focusout event, to work around
  471. // a bug with 3rd part keyboards (Swype) not firing keyup events.
  472. console.log('Here we are.');
  473. this.$('input').on('focusout', this.toggleClear);
  474. },
  475. clearInput: function clearInput(event) {
  476. var elem = event.srcElement;
  477. $(elem).siblings('input').val('');
  478. elem.style.display = 'none';
  479. },
  480. login: function login() {
  481. var user = this.$('#email-input').val(),
  482. pass = this.$('#password-input').val();
  483. if(user === '' || pass === '') {
  484. alert('You need to enter the appropriate details to login!');
  485. return false;
  486. }
  487. doAct('login', {
  488. username: user,
  489. password: pass
  490. }, function(res) {
  491. localStorage.setItem('authData', JSON.stringify(res));
  492. window.app.navigate("accounts", {trigger: true, replace: true});
  493. }, function(err) {
  494. alert('Login failed; check details and try again.');
  495. });
  496. },
  497. toggleIconColor: function(event) {
  498. var elem = $(event.srcElement);
  499. var ssIcon = elem.siblings('.ss-icon:not(.clear-input)');
  500. var clearIcon = elem.siblings('.clear-input');
  501. if (ssIcon.css('color') === 'rgb(0, 0, 0)') {
  502. ssIcon.css('color', '#ccc');
  503. clearIcon.css('color', '#ccc');
  504. } else {
  505. ssIcon.css('color', '#000');
  506. // TODO: Find a way to make this more DRY.
  507. clearIcon.css('color', '#FF2752');
  508. }
  509. },
  510. render: function() {
  511. this.$el.html($('#login-page-template').html());
  512. // Animate the form in.
  513. setTimeout(function() {
  514. this.$('#login-content').css({
  515. '-webkit-transform': 'translateY(0)',
  516. 'opacity': 1
  517. }); }, 500);
  518. console.log('rendered me');
  519. return this;
  520. }
  521. });
  522. var IntroPage = Backbone.View.extend({
  523. tagName: 'section',
  524. className: 'page',
  525. id: 'intro-page',
  526. titlebar: {
  527. title: 'Welcome!'
  528. },
  529. events: {
  530. },
  531. render: function() {
  532. this.$el.html('<p>Introduction page with brief overview of features and simple guide.</p>');
  533. return this;
  534. }
  535. });
  536. var AppRouter = Backbone.Router.extend({
  537. viewport: null,
  538. routes: {
  539. '': 'startup',
  540. 'login': 'login',
  541. 'accounts': 'accounts',
  542. 'accounts/:id': 'singleAccount',
  543. 'cases': 'cases',
  544. 'cases/:id': 'singleCase',
  545. 'campaigns': 'campaigns',
  546. 'campaigns/:id': 'campaignDetail',
  547. 'opportunities': 'opportunities',
  548. 'opportunities/:id': 'opportunityDetail',
  549. 'logout': 'logout'
  550. },
  551. initialize: function() {
  552. _.bindAll(this);
  553. this.viewport = new AppViewport();
  554. },
  555. startup: function startup() {
  556. console.log('startup');
  557. var authData = localStorage.getItem('authData');
  558. if (authData && !isSessionTimedOut()) {
  559. console.log('Skipping login...');
  560. window.app.navigate('accounts', {trigger: true, replace: true});
  561. } else {
  562. window.app.navigate('login', {trigger: true, replace: true});
  563. }
  564. },
  565. login: function() {
  566. this.viewport.setView(LoginPage);
  567. },
  568. accounts: function() {
  569. this.viewport.setView(ListPage, {
  570. id: 'accounts-page',
  571. collectionName: 'accounts',
  572. titleField: 'Name',
  573. titlebar: {
  574. title: 'Accounts'
  575. },
  576. customiseBorder: function customiseBorder(item) {
  577. return item.get('Rating');
  578. }
  579. });
  580. },
  581. singleAccount: function singleAccount(id) {
  582. this.viewport.setView(ListDetailPage, {
  583. titlebar: {
  584. title: 'Accounts'
  585. },
  586. isChild: true,
  587. id: 'account-detail-page',
  588. theId: id,
  589. templateId: 'account-tpl',
  590. collectionName: 'accounts',
  591. customiseEl: function customiseEl(el) {
  592. var that = this,
  593. width = $(window).width(),
  594. mapUrl = 'http://maps.googleapis.com/maps/api/staticmap?zoom=13&size=' + width + 'x' + Math.round(width * .67) + '&maptype=roadmap&sensor=false&center=';
  595. function getEscapedAddress() {
  596. var address = [
  597. that.model.get('BillingStreet'),
  598. that.model.get('BillingCity'),
  599. that.model.get('BillingState'),
  600. that.model.get('BillingCountry')
  601. ].join(', ');
  602. return address.replace(/\s/g, '+');
  603. }
  604. mapUrl += getEscapedAddress();
  605. // If we're on retina, upscale the image to keep quality perfect.
  606. mapUrl += window.devicePixelRatio > 1 ? '&scale=2' : '';
  607. // Insert the raw data...
  608. raw = '<table>';
  609. for (i = 1; i < Object.keys(this.model.attributes).length; i++) {
  610. raw += '<tr><td>' + Object.keys(this.model.attributes)[i] + '</td><td>' + this.model.attributes[Object.keys(this.model.attributes)[i]] + '</td></tr>';
  611. }
  612. raw += '</table>';
  613. el.find('.raw').html(raw);
  614. el.find('.account-head').css('background-image', 'url(' + mapUrl + ')');
  615. el.find('.account-head').one('tap', function openMap() {
  616. window.location = 'http://maps.google.com/maps?q=' + getEscapedAddress();
  617. });
  618. }
  619. });
  620. },
  621. opportunities: function opportunities() {
  622. this.viewport.setView(ListPage, {
  623. id: 'opportunities-page',
  624. collectionName: 'opportunities',
  625. titleField: 'Name',
  626. subtitleField: 'StageName',
  627. titlebar: {
  628. title: 'Opportunities'
  629. },
  630. customiseBorder: function customiseBorder(item) {
  631. var probability = item.get('Probability'),
  632. elem = $('<div class="opp-probability"></div>'),
  633. num = $('<div>' + probability + '</div>'),
  634. className;
  635. if (probability > 70) {
  636. className = 'high';
  637. } else if (probability > 40) {
  638. className = 'medium';
  639. } else if (probability > 0) {
  640. className = 'low';
  641. }
  642. elem.addClass(className);
  643. elem.append(num);
  644. elem.css('width', probability + '%');
  645. return elem[0];
  646. }
  647. });
  648. },
  649. opportunityDetail: function opportunityDetail(id) {
  650. this.viewport.setView(ListDetailPage, {
  651. titlebar: {
  652. title: 'Opportunities'
  653. },
  654. isChild: true,
  655. id: 'opportunity-detail-page',
  656. theId: id,
  657. templateId: 'opportunity-tpl',
  658. collectionName: 'opportunities',
  659. customiseEl: function customiseEl(el) {
  660. var that = this,
  661. probability = this.model.get('Probability'),
  662. probBar = el.find('#probability-bar'),
  663. probFigure = probBar.find('#probability-figure'),
  664. className, raw;
  665. if (probability > 70) {
  666. className = 'high';
  667. } else if (probability > 40) {
  668. className = 'medium';
  669. } else if (probability > 0) {
  670. className = 'low';
  671. }
  672. probBar.addClass(className);
  673. probBar.css('width', probability + '%');
  674. probFigure.html(probability + '% probable');
  675. // Insert the raw data...
  676. raw = '<table>';
  677. for (i = 1; i < Object.keys(this.model.attributes).length; i++) {
  678. raw += '<tr><td>' + Object.keys(this.model.attributes)[i] + '</td><td>' + this.model.attributes[Object.keys(this.model.attributes)[i]] + '</td></tr>';
  679. }
  680. raw += '</table>';
  681. el.find('.raw').html(raw);
  682. el.find('.info').one('tap', function() {
  683. window.app.navigate('accounts/' + that.model.get('AccountId'), {trigger: true});
  684. });
  685. }
  686. });
  687. },
  688. cases: function cases() {
  689. this.viewport.setView(ListPage, {
  690. id: 'cases-page',
  691. collectionName: 'cases',
  692. titleField: 'Subject',
  693. titlebar: {
  694. title: 'Cases'
  695. },
  696. customiseBorder: function customiseBorder(item) {
  697. var classString;
  698. classString = item.get('Priority');
  699. if (item.get('IsClosed')) {
  700. classString += ' true';
  701. }
  702. return classString;
  703. }
  704. });
  705. },
  706. singleCase: function singleCase(id) {
  707. this.viewport.setView(ListDetailPage, {
  708. titlebar: {
  709. title: 'Cases'
  710. },
  711. isChild: true,
  712. id: 'cases-detail-page',
  713. theId: id,
  714. templateId: 'cases-tpl',
  715. collectionName: 'cases',
  716. customiseEl: function customiseEl(el) {
  717. // Insert the raw data...
  718. raw = '<table>';
  719. for (i = 1; i < Object.keys(this.model.attributes).length; i++) {
  720. raw += '<tr><td>' + Object.keys(this.model.attributes)[i] + '</td><td>' + this.model.attributes[Object.keys(this.model.attributes)[i]] + '</td></tr>';
  721. }
  722. raw += '</table>';
  723. el.find('.raw').html(raw);
  724. }
  725. });
  726. },
  727. campaigns: function campaigns() {
  728. this.viewport.setView(ListPage, {
  729. id: 'campaigns-page',
  730. collectionName: 'campaigns',
  731. titleField: 'Name',
  732. subtitleField: 'Status',
  733. titlebar: {
  734. title: 'Campaigns'
  735. },
  736. customiseBorder: function customiseBorder(item) {
  737. var budget = item.get('BudgetedCost'),
  738. actual = item.get('ActualCost'),
  739. pct = (actual/budget) * 100,
  740. elem, pctElem, overBudget;
  741. overBudget = (pct > 100);
  742. pct = (pct > 100) ? ((pct - 100) > 100) ? 100 : pct - 100 : pct;
  743. if (actual) {
  744. elem = $('<div class="cost-bar"></div>');
  745. if (overBudget) {
  746. elem.addClass('overbudget');
  747. }
  748. pctElem = $('<div class="cost-actual" style="width:' + pct + '%;"></div>');
  749. elem.append(pctElem);
  750. return elem[0];
  751. }
  752. return false;
  753. }
  754. });
  755. },
  756. campaignDetail: function campaignDetail(id) {
  757. this.viewport.setView(ListDetailPage, {
  758. titlebar: {
  759. title: 'Campaigns'
  760. },
  761. isChild: true,
  762. id: 'campaign-detail-page',
  763. theId: id,
  764. templateId: 'campaign-tpl',
  765. collectionName: 'campaigns',
  766. customiseEl: function customiseEl(el) {
  767. // Insert the raw data...
  768. raw = '<table>';
  769. for (i = 1; i < Object.keys(this.model.attributes).length; i++) {
  770. raw += '<tr><td>' + Object.keys(this.model.attributes)[i] + '</td><td>' + this.model.attributes[Object.keys(this.model.attributes)[i]] + '</td></tr>';
  771. }
  772. raw += '</table>';
  773. el.find('.raw').html(raw);
  774. }
  775. });
  776. },
  777. logout: function logout() {
  778. var collection;
  779. localStorage.removeItem('authData');
  780. for (collection in collections) {
  781. collections[collection].length = 0;
  782. }
  783. window.app.navigate('login', {trigger: true});
  784. }
  785. });
  786. // The only global variable we introduce is the main application router class.
  787. // We don't just instanciate it now due to needing the DOM to be ready for
  788. // initialization etc.
  789. window.App = AppRouter;
  790. var Page = Backbone.View.extend({
  791. className: 'page',
  792. scroller: null,
  793. initialize: function initialize() {
  794. _.bindAll(this);
  795. // Giving a string collectionName is recommended, as it allows us to
  796. // properly reference links to individual items.
  797. if (!this.collection && this.options.collectionName) {
  798. this.collection = collections[this.options.collectionName] || undefined;
  799. }
  800. if (this.collection) {
  801. this.collection.on('reset', this.refreshRender);
  802. if (!this.collection.length) {
  803. this.collection.fetch();
  804. }
  805. }
  806. if (this.options.isChild) {
  807. this.isChild = true;
  808. }
  809. if (this.extraInit) {
  810. this.extraInit();
  811. }
  812. },
  813. refreshRender: function refreshRender() {
  814. var that = this;
  815. this.render();
  816. setTimeout(that.setupScroll, 100);
  817. },
  818. setupScroll: function setupScroll() {
  819. var that = this,
  820. el = $(this.$el.children()[0]),
  821. tallEnough = (el.height() > $(window).height() * .8);
  822. // TODO: Enable better management of iScrolls, to prevent possible memory
  823. // leaks etc.
  824. if (tallEnough) {
  825. that.scroller = new iScroll(that.id);
  826. }
  827. }
  828. });
  829. var ListPage = Page.extend({
  830. collection: null,
  831. options: {
  832. titleField: null,
  833. subtitleField: null,
  834. listClass: null,
  835. // Stub function, you should provide your own when wanting to use the
  836. // color bars on the left of the list.
  837. customiseBorder: function customiseBorder(item) {
  838. return false;
  839. }
  840. },
  841. render: function render() {
  842. var that = this,
  843. ul = $('<ul></ul>');
  844. this.collection.each(function(item) {
  845. ul.append(that.renderItem(item));
  846. });
  847. this.$el.html(ul);
  848. return this;
  849. },
  850. renderItem: function renderItem(item) {
  851. var that = this,
  852. li = $('<li></li>'),
  853. span = $('<span></span>'),
  854. customBorder = this.options.customiseBorder(item),
  855. id = item.get('Id');
  856. if (this.options.collectionName) {
  857. li.attr('data-link', id);
  858. li.on('tap', function viewDetail() {
  859. var link = that.options.collectionName + '/' + li.attr('data-link');
  860. function removeTouchStyle() {
  861. li.removeClass('touched');
  862. }
  863. li.addClass('touched');
  864. setTimeout(removeTouchStyle, 100);
  865. window.app.navigate(link, {trigger: true});
  866. });
  867. li.on('longTap', function touchStyle() {
  868. li.addClass('touched');
  869. $('body').one('touchend', function removeTouchStyle() {
  870. li.removeClass('touched');
  871. });
  872. });
  873. }
  874. li.append(span);
  875. span.append('<h2>' + item.get(this.options.titleField) + '</h2>');
  876. if (customBorder) {
  877. if (_.isString(customBorder)) {
  878. span.addClass(customBorder);
  879. }
  880. if (_.isElement(customBorder)) {
  881. span.prepend(customBorder);
  882. }
  883. }
  884. if (this.options.subtitleField) {
  885. span.append('<aside>' + item.get(this.options.subtitleField) + '</aside>');
  886. }
  887. return li;
  888. }
  889. });
  890. var ListDetailPage = Page.extend({
  891. extraInit: function extraInit() {
  892. if (this.options.templateId) {
  893. this.template = _.template($('#' + this.options.templateId).html());
  894. }
  895. },
  896. render: function render() {
  897. var el;
  898. // If there's no collection loaded yet, bail out... the refreshRender will
  899. // take care of us after the collection loads.
  900. if (!this.collection.length) {
  901. return this;
  902. }
  903. this.model = this.collection.where({ Id: this.options.theId })[0];
  904. el = $(this.template(this.model.toJSON()));
  905. if (this.options.customiseEl) this.options.customiseEl.call(this, el);
  906. this.$el.html(el);
  907. return this;
  908. }
  909. });
  910. }());
  911. // When the DOM is ready we initiate the app.
  912. $fh.ready(function() {
  913. $fh.legacy.fh_timeout=60000;
  914. window.app = new window.App();
  915. // Setting the document root like this allows us to run our app from the
  916. // AppStudio Online IDE without hassle, with no side-effects elsewhere.
  917. Backbone.history.start({pushState: false, root: document.location.pathname});
  918. });