From e0a32e38f1215de5a66e90ecccb41c2bab95db53 Mon Sep 17 00:00:00 2001 From: Is Isilon Date: Sun, 31 Jan 2016 15:34:34 +0800 Subject: [PATCH] Shuffling works, and lots of refactoring --- README.md | 7 + public/index.html | 41 ----- public/javascripts/collections/list.js | 33 +++- public/javascripts/models/task.js | 4 + public/javascripts/util/constants.js | 1 + public/javascripts/views/list.js | 32 +++- public/javascripts/views/page.js | 4 + public/javascripts/views/task.js | 222 +++++++++++++++++-------- public/stylesheets/app.css | 8 +- public/templates/task.html | 51 +++--- 10 files changed, 251 insertions(+), 152 deletions(-) diff --git a/README.md b/README.md index c8fc68d..91569cd 100644 --- a/README.md +++ b/README.md @@ -28,13 +28,20 @@ Or proceed manually as follow: ## Controls * UP & DOWN: navigate through tasks +* CNTRL+UP & CNTRL+DOWN: shuffle tasks * TAB: right-indent * SHIFT + TAB: left-indent +* BACKSPACE: Remove an empty task +* ENTER: New task +* Click on a bullet point to fold it +* Hover on a bullet point and click complete to complete it ## Technologies used * Node + Socket.io * Backbone +* Backbone.marionette +* Backbone.localforage * Foundation ## To-do diff --git a/public/index.html b/public/index.html index 91c69bf..9b0a856 100644 --- a/public/index.html +++ b/public/index.html @@ -67,46 +67,5 @@ -
- - - - - - diff --git a/public/javascripts/collections/list.js b/public/javascripts/collections/list.js index b702b45..45b0330 100644 --- a/public/javascripts/collections/list.js +++ b/public/javascripts/collections/list.js @@ -17,6 +17,37 @@ localforageBackbone model: Task, offlineSync: Backbone.localforage.sync("tasks"), + + initialize: function(){ + // update order on add, remove. + // Ref: http://stackoverflow.com/a/11665085/221742 + this.on('add remove', this.updateModelPriority); + }, + + moveUp: function(model) { // I see move up as the -1 + var index = this.indexOf(model); + if (index > 0) { + this.remove(model, {silent: true}); // silence this to stop excess event triggers + this.add(model, {at: index-1}); + } + }, + + moveDown: function(model) { // I see move up as the -1 + var index = this.indexOf(model); + if (index < this.models.length) { + this.remove(model, {silent: true}); // silence this to stop excess event triggers + this.add(model, {at: index+1}); + } + }, + + /** Updated priority of each member of list **/ + updateModelPriority: function() { + this.each(function(model, index) { + if (model) + model.set('priority', index); + }, this); + }, + /** switches sync between server and local databases **/ sync: function(){ //var self = this; @@ -31,7 +62,7 @@ localforageBackbone /** sort by priority then created date **/ comporator: function(child){ - return [child.get('priority'),child.get('createdAt')]; + return [child.get('priority'), child.get('createdAt')]; }, url: '/tasks' diff --git a/public/javascripts/models/task.js b/public/javascripts/models/task.js index c01f551..9e6bfa5 100644 --- a/public/javascripts/models/task.js +++ b/public/javascripts/models/task.js @@ -45,6 +45,10 @@ function ( }); } }); + }, + + focusOnView: function(){ + return this.view.$('input:first').focus(); } }); diff --git a/public/javascripts/util/constants.js b/public/javascripts/util/constants.js index b6c8006..0814ddc 100644 --- a/public/javascripts/util/constants.js +++ b/public/javascripts/util/constants.js @@ -8,6 +8,7 @@ function() { ENTER_KEY :13, UP_ARROW: 38, DOWN_ARROW: 40, + BACKSPACE: 8, TAB: 9, CNTRL: 17, diff --git a/public/javascripts/views/list.js b/public/javascripts/views/list.js index 81cb339..a6f3499 100644 --- a/public/javascripts/views/list.js +++ b/public/javascripts/views/list.js @@ -23,7 +23,8 @@ define( el: $("#main .children"), childView: TaskView, - template: '#list-view-template', + viewComparator: List.prototype.comporator, + template: _.template(listTemplate), events: { 'click #add': 'addTask' @@ -32,7 +33,13 @@ define( initialize: function () { var self = this; - this.collection = Tasks = new List(); + // this wholeCollection holds all items + this.wholeCollection = Tasks = new List(); + //this.collection = new List(); + + // custom events + this.listenTo(this, 'childview:rerender', this.render); + // this.listenTo(this.collection, 'add remove', this.render); /** Load demo data **/ function loadDemoData() { @@ -44,11 +51,11 @@ define( function success(children, data, promise) { // load demo data if the server returns nothing - if (children.length === 0) + var directChildren = children.filter(this.filterDirectChildren); + if (directChildren.length === 0) loadDemoData(); - else { - this.render(); - } + this.collection = new List(Tasks.filter(this.filterDirectChildren)); + this.render(); } Tasks.fetch({ @@ -70,10 +77,21 @@ define( }, // Only show direct children - filter: function (child, index, collection) { + filterDirectChildren: function (child, index, collection) { return child.get('parentId') === 0; }, + /** This is the root view in the tree **/ + getParentView: function () { + return this; + }, + + /** Update parentId when added to collection **/ + onAddChild: function(childView){ + if (childView.model.get('parentId')!==0) + childView.model.save({parentId: 0}); + }, + }); return ListView; diff --git a/public/javascripts/views/page.js b/public/javascripts/views/page.js index 9bdeca2..b3b7c8e 100644 --- a/public/javascripts/views/page.js +++ b/public/javascripts/views/page.js @@ -24,6 +24,10 @@ Task initialize: function() { listView = this.listView = new ListView(); this.input = $('#newTask'); + + // stop browser going back a page when jamming backspace + window.addEventListener('keydown',function(e){if(e.keyIdentifier=='U+0008'||e.keyIdentifier=='Backspace'){if(e.target==document.body){e.preventDefault();}}},true); + }, createNewTask: function(e) { diff --git a/public/javascripts/views/task.js b/public/javascripts/views/task.js index 9d1f66b..68161bf 100644 --- a/public/javascripts/views/task.js +++ b/public/javascripts/views/task.js @@ -2,6 +2,7 @@ define( ['jquery', 'backbone', 'socket', + 'collections/list', 'util/constants', 'text!../../templates/task.html', 'marionette', @@ -11,46 +12,77 @@ define( $, Backbone, io, + List, constants, taskTemplate, Marionette ) { - // The recursive tree view http://jsfiddle.net/wassname/zf61mLvh/2/ + // The recursive tree view. Ref:http://jsfiddle.net/wassname/zf61mLvh/2/ var TaskView = Backbone.Marionette.CompositeView.extend({ - template: '#task-view-template', + template: _.template(taskTemplate), //'#task-view-template', tagName: 'ul', className: "shift", + childView: TaskView, + childViewContainer: '.children', + childViewOptions: { + reorderOnSort: true + }, ui: { - input: '.task input', - options: '.task .options:first' + input: '.task input:first', + options: '.task .options:first', + // ui elements are bound before child render + // so the below child hashes are only available because + // render:collection triggers this.bindUIElements + children: '.children>ul', + descendants: '.children ul', }, events: { 'click .task:first': 'edit', 'blur .edit:first': 'close', - 'keydown .edit:first': 'handleKey', - 'keypress .edit:first': 'handleKey', + 'keyup .edit:first': 'handleKeyUp', + 'keydown .edit:first': 'handleKeyDown', + 'keypress .edit:first': 'handleKeyPress', 'mouseover .link:first': 'showOptions', 'mouseout .link:first': 'hideOptions', 'click .complete:first': 'markComplete', 'click .uncomplete:first': 'unmarkComlete', 'click .note:first': 'addNote', - 'click .mouse-tip:first': 'foldChildren' + 'click .mouse-tip:first': 'foldChildren', + }, - initialize: function () { + collectionEvents: {}, + + childEvents: {}, + + initialize: function (options) { var task = this; + // options + if (!('reorderOnSort' in options)) { + options.reorderOnSort = true; + } + // backlink this.model.view = this; - this.collection = this.model.collection; + var children = Tasks.filter( + function (child, index, collection) { + return child.get('parentId') === this.model.id; + }, this); + this.collection = new List(children); // events this.listenTo(this.model, 'change', this.render); this.listenTo(this.model, 'destroy', this.remove); + // refresh ui hashes after children are rendered + this.listenTo(this, 'render:collection', this.bindUIElements); + // custom event + this.listenTo(this, 'childview:rerender', this.render); + this.listenTo(this, 'focus', this.model.focusOnView); // updates from server if (!window.hackflowyOffline) { @@ -70,71 +102,111 @@ define( }, // override marionette filter to filter displayed children - filter: function (child, index, collection) { - return child.get('parentId') === this.model.get('id'); - }, - - /** Get the parent view or root view **/ - getParentView: function(){ - var parentId = this.model.get('parentId'); - if (parentId>0) return Tasks.get(parentId).view; - else return listView; - }, + // filterDirectChildren: function (child, index, collection) { + // return child.get('parentId') === this.model.get('id'); + // }, edit: function () { this.$el.addClass('editing'); this.$('.edit:first').focus(); }, - handleKey: function (e) { + /** Get the parent view or root view **/ + getParentView: function () { + var parent = Tasks.get(this.model.get('parentId')); + if (parent) return parent.view; + else return listView; + }, + + /** Update parentId when added to collection **/ + onAddChild: function(childView){ + if (childView.model.get('parentId')!==this.model.id) + childView.model.save({parentId: this.model.id}); + }, + + /** Focus on the next visible element down despite list level **/ + focusOnPrev: function () { + var all = listView.$el.find('ul:visible'); + var prev = $(all[all.index(this.$el) - 1]); + if (prev.length){ + prev.find('input:first').focus(); + } + return prev; + }, + + focusOnNext: function () { + var all = listView.$el.find('ul:visible'); + var next = $(all[all.index(this.$el) + 1]); + if (next) + next.find('input:first').focus(); + return next; + }, + + handleKeyUp: function(e){ + }, + + handleKeyPress: function(e){ + }, + + handleKeyDown: function (e) { + if (e.keyCode == constants.BACKSPACE) { + // remove if backspace pressed on empty/soon-to-be-empty item + if (this.ui.input.val().length < 2 && this.$('.children>ul').length===0) { + e.preventDefault(); + prev = this.focusOnPrev(); + this.destroy(); + this.model.destroy(); + // set the cursor to the end of the previous input + prev.find('input:first').focus(); + var input = prev.find('input:first')[0]; + input.selectionStart = input.selectionEnd = input.value.length; + return false; + } + } if (e.keyCode === constants.ENTER_KEY) this.addNote(e.currentTarget); - else if (e.ctrlKey && e.keyCode == constants.DOWN_ARROW){ + else if (e.ctrlKey && e.keyCode == constants.DOWN_ARROW) { + // move down list by swapping priority with next sibling e.preventDefault(); - this.model.save({priority: this.model.get('priority')-1}); - this.getParentView().collection.sortBy(); - this.getParentView().resortView(); - this.model.view.ui.input.focus(); - } else if (e.ctrlKey && e.keyCode == constants.UP_ARROW){ + this.getParentView().collection.moveDown(this.model); + this.trigger('rerender'); + this.model.focusOnView(); + } else if (e.ctrlKey && e.keyCode == constants.UP_ARROW) { + // move up the list e.preventDefault(); - this.model.save({priority: this.model.get('priority')+1}); - this.getParentView().collection.sortBy(); - this.getParentView().resortView(); - this.model.view.ui.input.focus(); - } else if (e.keyCode == constants.DOWN_ARROW){ - var all = listView.$el.find('ul:visible'); - var next = $(all[all.index(this.$el)+1]); - if (next) - next.find('input:first').focus(); - } else if (e.keyCode == constants.UP_ARROW){ - var all = listView.$el.find('ul:visible'); - var prev = $(all[all.index(this.$el)-1]); - if (prev) - prev.find('input:first').focus(); - } - // indent one less, by changing parent - else if (e.shiftKey && e.keyCode == constants.TAB) { + this.getParentView().collection.moveUp(this.model); + this.trigger('rerender'); + this.model.focusOnView(); + } else if (e.keyCode == constants.DOWN_ARROW) { + this.focusOnNext(); + } else if (e.keyCode == constants.UP_ARROW) { + this.focusOnPrev(); + } else if (e.shiftKey && e.keyCode == constants.TAB) { + // indent one less, by changing parent e.preventDefault(); - var parent = this.$el.parents('ul:first'); - var grandparentId = parent.find('input:first').data('parent-id') || 0; - if (this.model.get('parentId') !== grandparentId) { - this.model.save({parentId: grandparentId}); - this.getParentView().render(); - this.model.view.ui.input.focus(); + var parentId = this.$el.parents('ul:first').find('input:first').data('id'); + if (parentId===0){ + + } else if (parentId){ + parentModel = Tasks.get(parentId); + this.getParentView().collection.remove(this.model); + var index = parentModel.view.getParentView().collection.indexOf(parentModel); + if (index<0) index=this.getParentView().collection.length-1; + parentModel.view.getParentView().collection.add(this.model, {at:index+1}); + this.model.focusOnView(); } else { console.warn("Can't untab any further"); } - } - // indent one more, by changing parent - else if (e.keyCode == constants.TAB) { + } else if (e.keyCode == constants.TAB) { + // indent one more, by changing parent e.preventDefault(); var prevSibling = this.$el.prev('ul'); - if (prevSibling.length > 0) { - var siblingId = prevSibling.find('input:first').data('id'); - this.model.save({parentId: siblingId}); - this.model.view.remove(); - this.getParentView().render(); - this.model.view.ui.input.focus(); + if (prevSibling.length) { + var prevSibId = prevSibling.find('input:first').data('id'); + var prevSibView =Tasks.get(prevSibId).view; + this.getParentView().collection.remove(this.model); + prevSibView.collection.add(this.model); + this.model.focusOnView(); } else { console.warn("Can't tab any further"); } @@ -155,11 +227,6 @@ define( /** Finish editing an item **/ close: function () { var value = this.$('.edit:first').val().trim(); - if (value === '') - // remove empty items - // TODO let us tab and untab empty ones - this.model.destroy(); - else this.model.save({ content: value }); @@ -176,7 +243,7 @@ define( markComplete: function () { this.model.toggelCompletedStatus(true); - this.socket.emit('task', { + if (!window.hackflowyOffline) this.socket.emit('task', { id: this.model.id, parentId: this.model.parentId, content: this.model.toJSON().content, @@ -186,7 +253,7 @@ define( unmarkComlete: function () { this.model.toggelCompletedStatus(false); - this.socket.emit('task', { + if (!window.hackflowyOffline) this.socket.emit('task', { id: this.model.id, parentId: this.model.parentId, content: this.model.toJSON().content, @@ -194,23 +261,34 @@ define( }); }, - /** Add a new blank note **/ + /** Add a new blank note below this **/ addNote: function () { - // TODO add it after the current one var currentId = this.ui.input.data('id') || 0; parentId = this.ui.input.data('parent-id'); - var task = Tasks.add({ - parentId: parentId - }); + + var index= this.getParentView().collection.indexOf(this.model); + + var task = Tasks.add({parentId: this.model.get('parentId')}); + task.save(); + if (index>=0) + this.getParentView().collection.add(task, {at:index+1}); + else + this.getParentView().collection.add(task); + console.log(index); this.ui.input.blur(); task.view.ui.input.focus(); }, /** Fold children of the clicked element */ foldChildren: function () { - this.$el.find('ul').toggle(); - this.$el.find('li').toggleClass('folded'); + if (this.ui.children.length > 0) { + this.ui.children.toggle(); + this.$el.find('li:first').toggleClass('folded'); + } else { + this.ui.children.show(); + this.$el.find('li:first').removeClass('folded'); + } }, }); diff --git a/public/stylesheets/app.css b/public/stylesheets/app.css index c659a62..5dc4700 100644 --- a/public/stylesheets/app.css +++ b/public/stylesheets/app.css @@ -5681,6 +5681,9 @@ body #hackflowy #main .children li.shift8 { body #hackflowy #main .children li.shift9 { margin-left: 270px; } +body #hackflowy #main .children ul.shift { + margin-bottom: 0px; +} /* line 167, ../sass/app.scss */ body #hackflowy #main > .children { margin-left: 0; @@ -5692,11 +5695,6 @@ body #hackflowy #footer { font-size: 12px; } -/* line 138, ../sass/app.scss */ -body #hackflowy #main .children ul.shift { - margin-left: 30px; - margin-bottom: 0px; -} .options-div{ display: none; diff --git a/public/templates/task.html b/public/templates/task.html index 5ff37c1..7ddece2 100644 --- a/public/templates/task.html +++ b/public/templates/task.html @@ -1,29 +1,28 @@ -<% if(content){%>
  • -
    -
    - +
    +
    + - <% if(isCompleted){%> - - <%}else {%> - - <%}%> -
    -
    + <% if(isCompleted) { %> + + <% }else { %> + + <% } %> +
    +
  • -<%}%> +