PageRenderTime 52ms CodeModel.GetById 23ms RepoModel.GetById 1ms app.codeStats 0ms

/public/app.js

https://github.com/jakschu/todo-stack_meteor
JavaScript | 339 lines | 272 code | 47 blank | 20 comment | 28 complexity | 6b6c0eb1d22d3cc770a0fa5de27cc4ba MD5 | raw file
  1. // backbone examples:
  2. // http://documentcloud.github.com/backbone/docs/todos.html
  3. // http://documentcloud.github.com/backbone/examples/todos/index.html
  4. // http://coenraets.org/blog/2011/12/backbone-js-wine-cellar-tutorial-part-1-getting-started/
  5. // http://www.quora.com/What-are-some-good-resources-for-Backbone-js
  6. function log() {
  7. return;
  8. try {
  9. console.log.apply(console, arguments);
  10. } catch(e) {}
  11. }
  12. var Task = Backbone.Model.extend({
  13. defaults: function() {
  14. return {
  15. active: true,
  16. text: '',
  17. i: tasks.nextOrder()
  18. };
  19. },
  20. initialize: function() {
  21. log('[init] Task', this.get('i'));
  22. // if (!this.get('text')) {
  23. // this.set({'text': this.defaults.text});
  24. // }
  25. this.bind('change:text', function() {
  26. log('[change:text] now: ' + this.get('text'));
  27. });
  28. },
  29. toggle: function() {
  30. this.save({active: !this.get('active')});
  31. },
  32. clear: function() {
  33. this.destroy();
  34. }
  35. // validate: function(attrs) {
  36. // if (attrs.text.length == 0) {
  37. // return 'Must enter text!';
  38. // }
  39. // return undefined;
  40. // }
  41. });
  42. var TaskList = Backbone.Collection.extend({
  43. model: Task,
  44. localStorage: new Store('stack-backbone'),
  45. active: function() {
  46. return this.filter(function(task) { return task.get('active'); });
  47. },
  48. inactive: function() {
  49. return this.without.apply(this, this.active());
  50. },
  51. nextOrder: function() {
  52. if (!this.length) return 1;
  53. return this.last().get('i') + 1;
  54. },
  55. comparator: function(task1, task2) {
  56. log('task', task1, 'i', task1.get('i'));
  57. return task1.get('i') - task2.get('i');
  58. },
  59. initialize: function() {
  60. log('[init] TaskList');
  61. this.bind('remove', this.onRemove, this);
  62. },
  63. onRemove: function() {
  64. log('onRemove!', this);
  65. _.each(this.models, function(model, i) {
  66. model.set('i', i+1);
  67. });
  68. }
  69. });
  70. var tasks = new TaskList;
  71. var TaskView = Backbone.View.extend({
  72. className: 'box',
  73. template: _.template($('#template-box').html()),
  74. events: {
  75. 'click a.pop': 'pop',
  76. 'click div.text': 'edit',
  77. 'keydown div.text': 'edit',
  78. 'focus a.pop': 'popFocus',
  79. 'keydown .edit': 'updateOnEnterOrEsc',
  80. 'blur .edit': 'close',
  81. 'makeSelected': 'makeSelected',
  82. 'hover': 'makeSelected'
  83. },
  84. initialize: function() {
  85. this.model.bind('change', this.render, this);
  86. this.model.bind('destroy', this.remove, this);
  87. this.model.bind('edit', this.edit, this);
  88. this.$window = $(window);
  89. this.$header = $('#header');
  90. },
  91. render: function() {
  92. this.$el.html(this.template(this.model.toJSON()));
  93. this.$el.toggleClass('active', this.model.get('active'));
  94. this.$input = this.$('.edit');
  95. return this;
  96. },
  97. pop: function() {
  98. this.clear();
  99. },
  100. popFocus: function(e) {
  101. var scrollTop = this.$window.scrollTop(),
  102. headerHeight = this.$header.outerHeight(),
  103. elTop = this.$el.offset().top,
  104. extraPadding = 20;
  105. if (elTop < scrollTop + headerHeight) {
  106. window.scrollTo(this.$window.scrollLeft(), eltTop - headerHeight - 20);
  107. }
  108. },
  109. makeSelected: function() {
  110. this.$el.siblings('.selected').removeClass('selected');
  111. this.$el.addClass('selected');
  112. },
  113. edit: function(e) {
  114. if (e && e.type != 'click' && e.keyCode != 13) return;
  115. this.$input.val(this.model.get('text'));
  116. this.$el.addClass('editing');
  117. this.makeSelected();
  118. this.$input.focus().select();
  119. },
  120. close: function(e, wasEscape) {
  121. log("[CLOSE]", wasEscape);
  122. if (!wasEscape || !this.model.get('text')) {
  123. var value = this.$input.val();
  124. if (!value) this.clear();
  125. else this.model.save({text: value});
  126. }
  127. this.$el.removeClass('editing');
  128. },
  129. updateOnEnterOrEsc: function(e) {
  130. if (e.keyCode == 13) this.close(e); // enter
  131. else if (e.keyCode == 27) this.close(e, true); // esc
  132. },
  133. clear: function() {
  134. log('[TaskView.clear]');
  135. if (this.model.get('text')) {
  136. $('<div>', {
  137. 'class': 'box',
  138. html: this.$el.html()
  139. }).prependTo('#under>.inner');
  140. }
  141. this.model.clear();
  142. }
  143. });
  144. var AppView = Backbone.View.extend({
  145. el: $('body')[0],
  146. events: {
  147. 'click a.add': 'addNew',
  148. 'click #clear-inactive': 'clearInactive',
  149. 'click #pop-all': 'popAll'
  150. },
  151. initialize: function() {
  152. tasks.bind('add', this.addOne, this); //TODO - can these be chained?
  153. tasks.bind('reset', this.addAll, this);
  154. tasks.bind('change', this.render, this);
  155. tasks.bind('all', this.render, this);
  156. tasks.fetch();
  157. var self = this, $tasks = self.$('#tasks');
  158. function next(prev) {
  159. var $selected = $tasks.find('.box.selected').first(),
  160. $new = ($selected.length ?
  161. $selected[prev?'prev':'next']('.box') :
  162. null);
  163. if (!$new || !$new.size()) {
  164. $new = $tasks.find('.box')[prev?'last':'first']();
  165. }
  166. $new.trigger('makeSelected');
  167. }
  168. // key events
  169. var keyevents,
  170. helpTemplate = _.template($('#template-help').html());
  171. function hideHelp() {
  172. var m = document.getElementById('hotkey-modal');
  173. if (m) {
  174. $(m).remove();
  175. return true;
  176. }
  177. return false;
  178. }
  179. function help() {
  180. if (!hideHelp()) {
  181. var html = helpTemplate({keyevents: keyevents});
  182. $('body').append(html);
  183. }
  184. }
  185. keyevents = [
  186. {
  187. evt: 'keyup',
  188. key: 'a',
  189. fn: function(e) {
  190. // note - there's a mysterious bug where
  191. // this gets triggered sometimes on firefox
  192. // on mac osx when using cmd+tab to switch
  193. // applications...
  194. self.addNew();
  195. },
  196. help: 'Add a new task'
  197. },
  198. {
  199. evt: 'keypress',
  200. key: 'j',
  201. fn: function() { next(); },
  202. help: 'Select the next task'
  203. },
  204. {
  205. evt: 'keypress',
  206. key: 'k',
  207. fn: function() { next(true); },
  208. help: 'Select the previous task'
  209. },
  210. {
  211. evt: 'keyup',
  212. key: 'e',
  213. fn: function() {
  214. $tasks.find('.box.selected').find('div.text').click();
  215. },
  216. help: 'Edit the selected task'
  217. },
  218. {
  219. evt: 'keyup',
  220. key: '#',
  221. fn: function() {
  222. var $cur = $tasks.find('.box.selected');
  223. next();
  224. $cur.find('a.pop').click();
  225. },
  226. help: 'Mark the current task as complete'
  227. },
  228. {
  229. evt: 'keyup',
  230. key: '?',
  231. exactKey: 'shift+/',
  232. fn: help,
  233. help: 'Show the help modal'
  234. }
  235. ];
  236. var $doc = $(document);
  237. _.each(keyevents, function(obj) {
  238. $doc.bind(obj.evt, obj.exactKey || obj.key, obj.fn);
  239. });
  240. $doc.bind('keyup', 'esc', hideHelp);
  241. },
  242. render: function() {
  243. var numActive = tasks.active().length,
  244. numInactive = tasks.inactive().length;
  245. log('[APPVIEW.RENDER]');
  246. //tasks.render();
  247. var numModels = tasks.models.length;
  248. _.each(tasks.models, function(model, i) {
  249. var mI = model.get('i');
  250. log('model', mI, i, model);
  251. });
  252. if (tasks.length) {
  253. $('#add-helper').hide();
  254. } else {
  255. $('#add-helper').show();
  256. }
  257. },
  258. addOne: function(task, i) {
  259. log('[APPVIEW.ADDONE]', task, i, 'PLUCK', tasks.pluck('i'));
  260. if (typeof(i) == 'number' && task.get('i') != i) {
  261. task.set('i', i);
  262. }
  263. var view = new TaskView({model: task});
  264. this.$('#tasks').prepend(view.render().el);
  265. },
  266. addAll: function() {
  267. log('[APPVIEW.ADDALL]');
  268. var self = this;
  269. tasks.each(function(task, i) {
  270. self.addOne(task, i+1);
  271. });
  272. },
  273. addNew: function(e) {
  274. log('[APPVIEW.ADDNEW]', e);
  275. window.scrollTo(0, 0);
  276. var task = tasks.create();
  277. task.trigger('edit');
  278. },
  279. clearInactive: function() {
  280. _.each(tasks.inactive(), function(task) {
  281. task.clear();
  282. });
  283. return false;
  284. },
  285. popAll: function() {
  286. log('[AppView.popAll]');
  287. tasks.each(function(task) {
  288. task.save({'active': false});
  289. });
  290. }
  291. });
  292. var app = new AppView;