PageRenderTime 70ms CodeModel.GetById 35ms RepoModel.GetById 0ms app.codeStats 0ms

/javascript/shared-package/goals.js

https://bitbucket.org/pandapanda/debastabiilne
JavaScript | 747 lines | 600 code | 94 blank | 53 comment | 71 complexity | 945906dfe6a562a476748fd10ce2360e MD5 | raw file
Possible License(s): MIT, BSD-3-Clause, CC-BY-3.0, GPL-3.0
  1. var Goal = Backbone.Model.extend({
  2. defaults: {
  3. active: false,
  4. complete: false,
  5. progress: 0,
  6. title: "Unnamed goal",
  7. objectives: []
  8. },
  9. urlRoot: "/api/v1/user/goals",
  10. initialize: function() {
  11. // defaults for new models (e.g. not from server)
  12. // default created and updated values
  13. if (!this.has("created")) {
  14. var now = new Date().toISOString();
  15. this.set({created: now, updated: now});
  16. }
  17. // default progress value for all objectives
  18. _.each(this.get("objectives"), function(o) {
  19. if (o.progress == null) {
  20. o.progress = 0;
  21. o.status = "not-started";
  22. }
  23. if (o.status == null) {
  24. o.status = 'not-started';
  25. }
  26. });
  27. // a bunch of stuff needed to display goals in views. might need to be
  28. // refactored.
  29. this.calcDependents();
  30. this.bind("change", this.fireCustom, this);
  31. },
  32. calcDependents: function() {
  33. var progress = this.calcTotalProgress(this.get("objectives"));
  34. var objectiveWidth = 100 / this.get("objectives").length;
  35. _.each(this.get("objectives"), function(obj) {
  36. Goal.calcObjectiveDependents(obj, objectiveWidth);
  37. });
  38. this.set({
  39. progress: progress,
  40. progressStr: Goal.floatToPercentageStr(progress),
  41. complete: progress >= 1,
  42. // used to display 3/5 in goal summary area
  43. objectiveProgress: _.filter(this.get("objectives"), function(obj) {
  44. return obj.progress >= 1;
  45. }).length,
  46. // used to maintain sorted order in a GoalCollection
  47. updatedTime: parseISO8601(this.get("updated")).getTime()
  48. }, {silent: true});
  49. },
  50. calcTotalProgress: function(objectives) {
  51. objectives = objectives || this.get("objectives");
  52. var progress = 0;
  53. if (objectives.length) {
  54. progress = _.reduce(objectives, function(p, ob) { return p + ob.progress; }, 0);
  55. if (objectives.length > 0) {
  56. progress = progress / objectives.length;
  57. } else {
  58. progress = 0;
  59. }
  60. }
  61. return progress;
  62. },
  63. fireCustom: function() {
  64. this.calcDependents();
  65. if (this.hasChanged("progress")) {
  66. // we want to fire these events after all other listeners to 'change'
  67. // have had a chance to run
  68. var toFire = [];
  69. // check for goal completion
  70. if (this.get("progress") >= 1) {
  71. toFire.push(["goalcompleted", this]);
  72. }
  73. else {
  74. // now look for updated objectives
  75. oldObjectives = this.previous("objectives");
  76. _.each(this.get("objectives"), function(newObj, i) {
  77. var oldObj = oldObjectives[i];
  78. if (newObj.progress > oldObj.progress) {
  79. toFire.push(["progressed", this, newObj]);
  80. if (newObj.progress >= 1) {
  81. toFire.push(["completed", this, newObj]);
  82. }
  83. }
  84. }, this);
  85. }
  86. if (_.any(toFire)) {
  87. // register a callback to execute at the end of the rest of the
  88. // change callbacks
  89. this.collection.bind("change", function callback() {
  90. // this callback should only run once, so immediately unbind
  91. this.unbind("change", callback);
  92. // trigger all change notifications
  93. _.each(toFire, function(triggerArgs) {
  94. this.trigger.apply(this, triggerArgs);
  95. }, this);
  96. }, this.collection);
  97. }
  98. }
  99. }
  100. }, {
  101. calcObjectiveDependents: function(objective, objectiveWidth) {
  102. objective.complete = objective.progress >= 1;
  103. objective.progressStr = Goal.floatToPercentageStr(objective.progress);
  104. objective.iconFillHeight = Goal.calcIconFillHeight(objective);
  105. objective.objectiveWidth = objectiveWidth;
  106. objective.isVideo = (objective.type == "GoalObjectiveWatchVideo");
  107. objective.isAnyVideo = (objective.type == "GoalObjectiveAnyVideo");
  108. objective.isExercise = (objective.type == "GoalObjectiveExerciseProficiency");
  109. objective.isAnyExercise = (objective.type == "GoalObjectiveAnyExerciseProficiency");
  110. },
  111. calcIconFillHeight: function(objective) {
  112. var height = objective.type.toLowerCase().indexOf("exercise") >= 1 ? 13 : 12;
  113. var offset = objective.type.toLowerCase().indexOf("exercise") >= 1 ? 4 : 6;
  114. return Math.ceil(objective.progress * height) + offset;
  115. },
  116. floatToPercentageStr: function(progress) {
  117. return (progress * 100).toFixed(0);
  118. },
  119. objectiveUrlForType: {
  120. GoalObjectiveWatchVideo: function(objective) {
  121. return "/video/" + objective.internal_id;
  122. },
  123. GoalObjectiveAnyVideo: function(objective) {
  124. return "/";
  125. },
  126. GoalObjectiveExerciseProficiency: function(objective) {
  127. return "/exercise/" + objective.internal_id;
  128. },
  129. GoalObjectiveAnyExerciseProficiency: function(objective) {
  130. return "/exercisedashboard";
  131. }
  132. },
  133. objectiveUrl: function(objective) {
  134. return Goal.objectiveUrlForType[objective.type](objective);
  135. },
  136. defaultVideoProcessGoalAttrs_: {
  137. title: "Complete Five Videos",
  138. objectives: [
  139. { description: "Any video", type: "GoalObjectiveAnyVideo" },
  140. { description: "Any video", type: "GoalObjectiveAnyVideo" },
  141. { description: "Any video", type: "GoalObjectiveAnyVideo" },
  142. { description: "Any video", type: "GoalObjectiveAnyVideo" },
  143. { description: "Any video", type: "GoalObjectiveAnyVideo" }
  144. ]
  145. },
  146. defaultExerciseProcessGoalAttrs_: {
  147. title: "Complete Five Exercises",
  148. objectives: [
  149. { description: "Any exercise", type: "GoalObjectiveAnyExerciseProficiency" },
  150. { description: "Any exercise", type: "GoalObjectiveAnyExerciseProficiency" },
  151. { description: "Any exercise", type: "GoalObjectiveAnyExerciseProficiency" },
  152. { description: "Any exercise", type: "GoalObjectiveAnyExerciseProficiency" },
  153. { description: "Any exercise", type: "GoalObjectiveAnyExerciseProficiency" }
  154. ]
  155. },
  156. // TODO: investigate if this is still used.
  157. exidToExerciseName: function(exid) {
  158. return exid[0].toUpperCase() + exid.slice(1).replace(/_/g, ' ');
  159. },
  160. // TODO: investigate if this is still used.
  161. GoalObjectiveExerciseProficiency: function(exid) {
  162. var obj = {
  163. type: "GoalObjectiveExerciseProficiency",
  164. internal_id: exid,
  165. description: Goal.exidToExerciseName(exid)
  166. };
  167. obj.url = Goal.objectiveUrl(obj);
  168. return obj;
  169. }
  170. });
  171. var GoalCollection = Backbone.Collection.extend({
  172. model: Goal,
  173. initialize: function() {
  174. this.updateActive();
  175. // ensure updateActive is called whenever the collection changes
  176. this.bind("add", this.updateActive, this);
  177. this.bind("remove", this.updateActive, this);
  178. this.bind("reset", this.updateActive, this);
  179. },
  180. url: "/api/v1/user/goals",
  181. comparator: function(goal) {
  182. // display most recently updated goal at the top of the list.
  183. // http://stackoverflow.com/questions/5636812/sorting-strings-in-reverse-order-with-backbone-js/5639070#5639070
  184. return -goal.get("updatedTime");
  185. },
  186. active: function(goal) {
  187. var current = this.find(function(g) {return g.get("active");}) || null;
  188. if (goal && goal !== current) {
  189. // set active
  190. if (current !== null) {
  191. current.set({active: false});
  192. }
  193. goal.set({active: true});
  194. current = goal;
  195. }
  196. return current;
  197. },
  198. updateActive: function() {
  199. this.active(this.findActiveGoal());
  200. },
  201. incrementalUpdate: function(updatedGoals) {
  202. _.each(updatedGoals, function(newGoal) {
  203. oldGoal = this.get(newGoal.id) || null;
  204. if (oldGoal !== null) {
  205. oldGoal.set(newGoal);
  206. }
  207. else {
  208. // todo: remove this, do something better
  209. KAConsole.log("Error: brand new goal appeared from somewhere", newGoal);
  210. }
  211. }, this);
  212. },
  213. findGoalWithObjective: function(internalId, specificType, generalType) {
  214. return this.find(function(goal) {
  215. // find a goal with an objective for this exact entity
  216. return _.find(goal.get("objectives"), function(ob) {
  217. return ob.type == specificType && internalId == ob.internal_id;
  218. });
  219. }) || this.find(function(goal) {
  220. // otherwise find a goal with any entity proficiency
  221. return _.find(goal.get("objectives"), function(ob) {
  222. return ob.type == generalType;
  223. });
  224. }) || null;
  225. },
  226. // find the most appriate goal to display for a given URL
  227. findActiveGoal: function() {
  228. var matchingGoal = null;
  229. if (window.location.pathname.indexOf("/exercise") === 0 && window.userExerciseName) {
  230. matchingGoal = this.findGoalWithObjective(userExerciseName,
  231. "GoalObjectiveExerciseProficiency",
  232. "GoalObjectiveAnyExerciseProficiency");
  233. } else if (window.location.pathname.indexOf("/video") === 0 &&
  234. typeof Video.readableId !== "undefined") {
  235. matchingGoal = this.findGoalWithObjective(Video.readableId,
  236. "GoalObjectiveWatchVideo", "GoalObjectiveAnyVideo");
  237. }
  238. // if we're not on a matching exercise or video page, just show the
  239. // most recently upated one
  240. if (matchingGoal === null) {
  241. matchingGoal = this.at(0); // comparator is most recently updated
  242. }
  243. return matchingGoal;
  244. },
  245. processGoalContext: function() {
  246. return {
  247. hasExercise: this.any(function(goal) {
  248. return _.any(goal.get("objectives"), function(obj) {
  249. return obj.type === "GoalObjectiveAnyExerciseProficiency";
  250. });
  251. }),
  252. hasVideo: this.any(function(goal) {
  253. return _.any(goal.get("objectives"), function(obj) {
  254. return obj.type === "GoalObjectiveAnyVideo";
  255. });
  256. })
  257. };
  258. }
  259. });
  260. var GoalBookView = Backbone.View.extend({
  261. template: Templates.get("shared.goalbook"),
  262. isVisible: false,
  263. needsRerender: true,
  264. initialize: function() {
  265. $(this.el)
  266. .delegate(".close-button", "click", $.proxy(this.hide, this))
  267. // listen to archive button on goals
  268. .delegate(".goal.recently-completed", "mouseenter mouseleave", function(e) {
  269. var el = $(e.currentTarget);
  270. if (e.type == "mouseenter") {
  271. el.find(".goal-description .summary-light").hide();
  272. el.find(".goal-description .goal-controls").show();
  273. } else {
  274. el.find(".goal-description .goal-controls").hide();
  275. el.find(".goal-description .summary-light").show();
  276. }
  277. })
  278. .delegate(".archive", "click", $.proxy(function(e) {
  279. var jel = $(e.target).closest(".goal");
  280. var goal = this.model.get(jel.data("id"));
  281. this.animateGoalToHistory(jel).then($.proxy(function() {
  282. this.model.remove(goal);
  283. }, this));
  284. }, this))
  285. .delegate(".new-goal", "click", $.proxy(function(e) {
  286. e.preventDefault();
  287. this.hide();
  288. newGoalDialog.show();
  289. }, this))
  290. .delegate(".goal-history", "click",
  291. $.proxy(this.goalHistoryButtonClicked, this));
  292. this.model.bind("change", this.render, this);
  293. this.model.bind("reset", this.render, this);
  294. this.model.bind("remove", this.render, this);
  295. this.model.bind("add", this.added, this);
  296. this.model.bind("goalcompleted", this.show, this);
  297. },
  298. show: function() {
  299. this.isVisible = true;
  300. // render if necessary
  301. if (this.needsRerender) {
  302. this.render();
  303. }
  304. var that = this;
  305. // animate on the way down
  306. return $(this.el).slideDown("fast", function() {
  307. // listen for escape key
  308. $(document).bind("keyup.goalbook", function(e) {
  309. if (e.which == 27) {
  310. that.hide();
  311. }
  312. });
  313. // close the goalbook if user clicks elsewhere on page
  314. $("body").bind("click.goalbook", function(e) {
  315. if ($(e.target).closest("#goals-nav-container").length === 0) {
  316. that.hide();
  317. }
  318. });
  319. });
  320. },
  321. hide: function() {
  322. this.isVisible = false;
  323. $(document).unbind("keyup.goalbook");
  324. $("body").unbind("click.goalbook");
  325. // if there are completed goals, move them to history before closing
  326. var completed = this.model.filter(function(goal) { return goal.get("complete"); });
  327. var completedEls = this.$(".recently-completed");
  328. if (completedEls.length > 0) {
  329. this.animateThenHide(completedEls);
  330. } else {
  331. return $(this.el).slideUp("fast");
  332. }
  333. },
  334. goalHistoryButtonClicked: function(e) {
  335. // Stay on the page if the user is already looking at her profile
  336. // TODO: if we care about metaKey / control key / middle key working,
  337. // then there is probably more to test here.
  338. if (typeof Profile !== "undefined" && !e.metaKey) {
  339. e.preventDefault();
  340. this.hide();
  341. Profile.router.navigate("/goals", true);
  342. }
  343. },
  344. added: function(goal, options) {
  345. this.needsRerender = true;
  346. this.show();
  347. // add a highlight to the new goal
  348. $(".goal[data-id=" + goal.get("id") + "]").effect("highlight", {}, 2500);
  349. },
  350. animateThenHide: function(els) {
  351. var goals = _.map(els, function(el) {
  352. return this.model.get($(el).data("id"));
  353. }, this);
  354. // wait for the animation to complete and then close the goalbook
  355. this.animateGoalToHistory(els).then($.proxy(function() {
  356. $(this.el).slideUp("fast").promise().then($.proxy(function() {
  357. this.model.remove(goals);
  358. }, this));
  359. }, this));
  360. },
  361. render: function() {
  362. var jel = $(this.el);
  363. // delay rendering until the view is actually visible
  364. if (!this.isVisible) {
  365. this.needsRerender = true;
  366. }
  367. else {
  368. KAConsole.log("rendering GoalBookView", this);
  369. this.needsRerender = false;
  370. var json = _.pluck(this.model.models, "attributes");
  371. jel.html(this.template({
  372. goals: json,
  373. profileRoot: KA.profileRoot
  374. }));
  375. }
  376. return this;
  377. },
  378. animateGoalToHistory: function(els) {
  379. var btnGoalHistory = this.$("a.goal-history");
  380. var promises = $(els).map(function(i, el) {
  381. var dfd = $.Deferred();
  382. var jel = $(el);
  383. jel .children()
  384. .each(function() {
  385. $(this).css("overflow", "hidden").css("height", $(this).height());
  386. })
  387. .end()
  388. .delay(500)
  389. .animate({
  390. width: btnGoalHistory.width(),
  391. left: btnGoalHistory.position().left
  392. })
  393. .animate({
  394. top: btnGoalHistory.position().top - jel.position().top,
  395. height: "0",
  396. opacity: "toggle"
  397. },
  398. "easeInOutCubic",
  399. function() {
  400. $(this).remove();
  401. dfd.resolve();
  402. }
  403. );
  404. return dfd.promise();
  405. }).get();
  406. // once all the animations are done, make the history button glow
  407. var button = $.Deferred();
  408. $.when.apply(null, promises).then(function() {
  409. btnGoalHistory
  410. .animate({backgroundColor: "orange"})
  411. .animate({backgroundColor: "#ddd"}, button.resolve);
  412. });
  413. // return a promise that the history button is done animating
  414. return button.promise();
  415. }
  416. });
  417. var GoalSummaryView = Backbone.View.extend({
  418. template: Templates.get("shared.goal-summary-area"),
  419. initialize: function(args) {
  420. $(this.el).delegate("#goals-drawer", "click",
  421. $.proxy(args.goalBookView.show, args.goalBookView));
  422. this.model.bind("change", this.render, this);
  423. this.model.bind("reset", this.render, this);
  424. this.model.bind("remove", this.render, this);
  425. this.model.bind("add", this.render, this);
  426. this.model.bind("completed", this.justFinishedObjective, this);
  427. },
  428. render: function() {
  429. KAConsole.log("rendering GoalSummaryView", this);
  430. var active = this.model.active() || null;
  431. if (active !== null) {
  432. $(this.el).html(this.template(active.attributes));
  433. }
  434. else {
  435. // todo: put create a goal button here?
  436. $(this.el).empty();
  437. }
  438. return this;
  439. },
  440. justFinishedObjective: function(newGoal, newObj) {
  441. this.render();
  442. this.$("#goals-drawer").effect("highlight", {}, 2500);
  443. }
  444. });
  445. function finishLoadingMapsPackage() {
  446. KAConsole.log("Loaded Google Maps.");
  447. dynamicLoadPackage_maps(function(status, progress) {
  448. if (status == "complete") {
  449. KAConsole.log("Loaded maps package.");
  450. } else if (status == "failed") {
  451. KAConsole.log("Failed to load maps package.");
  452. setTimeout(finishLoadingMapsPackage, 5000); // Try again in 5 seconds
  453. } else if (status == "progress") {
  454. KAConsole.log("Maps package " + (progress * 100).toFixed(0) + "% loaded.");
  455. if (newCustomGoalDialog) {
  456. newCustomGoalDialog.$(".progress-bar")
  457. .progressbar("value", progress * 100);
  458. }
  459. }
  460. });
  461. }
  462. var NewGoalView = Backbone.View.extend({
  463. template: Templates.get("shared.goal-new"),
  464. events: {
  465. "click .newgoal.custom": "createCustomGoal",
  466. "click .newgoal.five_exercises": "createExerciseProcessGoal",
  467. "click .newgoal.five_videos": "createVideoProcessGoal"
  468. },
  469. initialize: function() {
  470. this.render();
  471. },
  472. render: function() {
  473. var context = this.model.processGoalContext();
  474. $(this.el).html(this.template(context));
  475. this.hookup();
  476. return this;
  477. },
  478. hookup: function() {
  479. var that = this;
  480. this.$(".newgoal").hoverIntent(
  481. function hfa(evt) {
  482. if ($(this).hasClass("disabled")) {
  483. return;
  484. }
  485. that.$(".newgoal").not(this).not(".disabled").hoverFlow(
  486. evt.type, { opacity: 0.2},
  487. 750, "easeInOutCubic");
  488. $(".info.pos-left", this).hoverFlow(
  489. evt.type, { left: "+=30px", opacity: "show" },
  490. 350, "easeInOutCubic");
  491. $(".info.pos-right, .info.pos-top", this).hoverFlow(
  492. evt.type, { right: "+=30px", opacity: "show" },
  493. 350, "easeInOutCubic");
  494. },
  495. function hfo(evt) {
  496. if ($(this).hasClass("disabled")) {
  497. return;
  498. }
  499. that.$(".newgoal").not(this).not(".disabled").hoverFlow(
  500. evt.type, { opacity: 1}, 175, "easeInOutCubic");
  501. $(".info.pos-left", this).hoverFlow(
  502. evt.type, { left: "-=30px", opacity: "hide" },
  503. 150, "easeInOutCubic");
  504. $(".info.pos-right, .info.pos-top", this).hoverFlow(
  505. evt.type, { right: "-=30px", opacity: "hide" },
  506. 150, "easeInOutCubic");
  507. }
  508. );
  509. },
  510. createVideoProcessGoal: function(e) {
  511. e.preventDefault();
  512. if ($(e.currentTarget).hasClass("disabled")) return;
  513. var goal = new Goal(Goal.defaultVideoProcessGoalAttrs_);
  514. this.createSimpleGoal(goal);
  515. },
  516. createExerciseProcessGoal: function(e) {
  517. e.preventDefault();
  518. if ($(e.currentTarget).hasClass("disabled")) return;
  519. var goal = new Goal(Goal.defaultExerciseProcessGoalAttrs_);
  520. this.createSimpleGoal(goal);
  521. },
  522. createSimpleGoal: function(goal) {
  523. this.model.add(goal);
  524. goal.save().fail($.proxy(function() {
  525. KAConsole.log("Error while saving new custom goal", goal);
  526. this.model.remove(goal);
  527. }, this));
  528. this.trigger("creating");
  529. },
  530. createCustomGoal: function(e) {
  531. this.trigger("creating");
  532. e.preventDefault();
  533. newCustomGoalDialog.show();
  534. }
  535. });
  536. var NewGoalDialog = Backbone.View.extend({
  537. template: Templates.get("shared.goal-new-dialog"),
  538. initialize: function() {
  539. this.render();
  540. },
  541. render: function() {
  542. var context = this.model.processGoalContext();
  543. // As we're assigning to this.el, event handlers need to be rebound
  544. // after each render.
  545. this.el = $(this.template(context)).appendTo(document.body).get(0);
  546. this.newGoalView = new NewGoalView({
  547. el: this.$(".viewcontents"),
  548. model: this.model
  549. });
  550. this.newGoalView.bind("creating", this.hide, this);
  551. return this;
  552. },
  553. show: function() {
  554. // rerender every time we show this in case some process goals should
  555. // be disabled
  556. this.newGoalView.render();
  557. return $(this.el).modal({
  558. keyboard: true,
  559. backdrop: true,
  560. show: true
  561. });
  562. },
  563. hide: function() {
  564. // hide all hover effects so they don't show up next time we show
  565. this.$(".info").hide();
  566. // now hide the dialog
  567. return $(this.el).modal("hide");
  568. }
  569. });
  570. var NewCustomGoalDialog = Backbone.View.extend({
  571. template: Templates.get("shared.goal-new-custom-dialog"),
  572. loaded: false,
  573. render: function() {
  574. // As we're assigning to this.el, event handlers need to be rebound
  575. // after each render.
  576. this.el = $(this.template()).appendTo(document.body).get(0);
  577. this.innerEl = this.$(".modal-body").get(0);
  578. // turn on fading just before we animate so that dragging is fast
  579. var $el = $(this.el);
  580. $el.bind("shown", function() { $el.removeClass("fade"); });
  581. $el.bind("hide", function() { $el.addClass("fade"); });
  582. return this;
  583. },
  584. _show: function() {
  585. return $(this.el).modal({
  586. keyboard: false,
  587. backdrop: true,
  588. show: true
  589. });
  590. },
  591. show: function() {
  592. if (!this.innerEl) {
  593. this.render();
  594. }
  595. // if we haven't yet loaded the contents of this dialog, do it
  596. if (!this.loaded) {
  597. this.loaded = true;
  598. this.load().error($.proxy(function() {
  599. this.loaded = false;
  600. }));
  601. this.$(".progress-bar").progressbar({value: 10}).slideDown("fast");
  602. }
  603. this._show();
  604. },
  605. load: function() {
  606. if (!dynamicPackageLoader.packageLoaded("maps")) {
  607. $('<script src="http://maps.google.com/maps/api/js?v=3.3&sensor=false&callback=finishLoadingMapsPackage" type="text/javascript"></script>').appendTo(document);
  608. }
  609. return $.ajax({url: "/goals/new", type: "GET", dataType: "html"})
  610. .done($.proxy(function(html) {
  611. KAConsole.log("Loaded /goals/new.");
  612. this.waitForMapsPackage(html);
  613. }, this))
  614. .error($.proxy(function() {
  615. KAConsole.log(Array.prototype.slice.call(arguments));
  616. $(this.innerEl).text("Page load failed. Please try again.");
  617. }, this));
  618. },
  619. hide: function() {
  620. $(this.el).modal("hide");
  621. },
  622. waitForMapsPackage: function(html) {
  623. if (!dynamicPackageLoader.packageLoaded("maps")) {
  624. var that = this;
  625. setTimeout(function() { that.waitForMapsPackage(html); }, 100);
  626. return;
  627. }
  628. KAConsole.log("Done loading.");
  629. $(this.innerEl).html(html);
  630. createGoalInitialize();
  631. }
  632. });
  633. $(function() {
  634. window.GoalBook = new GoalCollection(window.GoalsBootstrap || []);
  635. APIActionResults.register("updateGoals",
  636. $.proxy(GoalBook.incrementalUpdate, window.GoalBook));
  637. window.myGoalBookView = new GoalBookView({
  638. el: "#goals-nav-container",
  639. model: GoalBook
  640. });
  641. window.myGoalSummaryView = new GoalSummaryView({
  642. el: "#goals-container",
  643. model: GoalBook,
  644. goalBookView: myGoalBookView
  645. });
  646. myGoalSummaryView.render();
  647. window.newGoalDialog = new NewGoalDialog({model: GoalBook});
  648. window.newCustomGoalDialog = new NewCustomGoalDialog();
  649. });
  650. // todo: should we do this globally?
  651. Handlebars.registerPartial("goal-objectives", Templates.get("shared.goal-objectives"));
  652. Handlebars.registerPartial("goalbook-row", Templates.get("shared.goalbook-row"));
  653. Handlebars.registerPartial("goal-new", Templates.get("shared.goal-new"));