From ed4641650b4850b7e99d327fa0ba3232ce0c756a Mon Sep 17 00:00:00 2001 From: Is Isilon Date: Sat, 30 Jan 2016 07:26:13 +0800 Subject: [PATCH 1/6] Fix for offline sync --- public/javascripts/collections/list.js | 26 ++++++--- public/javascripts/models/task.js | 79 ++++++++++++++------------ 2 files changed, 61 insertions(+), 44 deletions(-) diff --git a/public/javascripts/collections/list.js b/public/javascripts/collections/list.js index 1fd7ba4..a2467e5 100644 --- a/public/javascripts/collections/list.js +++ b/public/javascripts/collections/list.js @@ -13,22 +13,30 @@ localforage, localforageBackbone ) { - var List = Backbone.Collection.extend({ + var List = Backbone.Collection.extend({ - - model: Task, - offlineSync: Backbone.localforage.sync("tasks"), - /** switches sync between server and local databases **/ - sync: function(){ + model: Task, + offlineSync: Backbone.localforage.sync("tasks"), + /** switches sync between server and local databases **/ + sync: function(){ + //var self = this; + _localforageNamespace = this.offlineSync._localforageNamespace; + _localeForageKeyFn=this.offlineSync._localeForageKeyFn; + localforageKey = this.offlineSync._localeForageKeyFn; if (window.hackflowyOffline) return this.offlineSync.apply(this, arguments); else return Backbone.sync.apply(this, arguments); - }, + }, - url: '/tasks' + url: '/tasks' - }); + }); + + // a couple of vars backbone.localforage needs in the sync function + List.prototype.sync.localforage = List.prototype.offlineSync._localeForageKeyFn; + List.prototype.sync._localeForageKeyFn = List.prototype.offlineSync._localeForageKeyFn; + List.prototype.sync._localforageNamespace = List.prototype.offlineSync._localforageNamespace; return List; diff --git a/public/javascripts/models/task.js b/public/javascripts/models/task.js index 0f0d318..ac48cff 100644 --- a/public/javascripts/models/task.js +++ b/public/javascripts/models/task.js @@ -1,48 +1,57 @@ define( ['backbone', -'localforage', -'localforagebackbone' + 'localforage', + 'localforagebackbone' ], -function( -Backbone, -localforage, -localforageBackbone +function ( + Backbone, + localforage, + localforageBackbone ) { - var TaskModel = Backbone.Model.extend({ + var TaskModel = Backbone.Model.extend({ - offlineSync: Backbone.localforage.sync('TaskModel'), - /** switches sync between server and local databases **/ - sync: function(){ - if (window.hackflowyOffline) - return this.offlineSync.apply(this,arguments); - else - return Backbone.sync.apply(this, arguments); - }, + offlineSync: Backbone.localforage.sync('TaskModel'), + /** switches sync between server and local databases **/ + sync: function () { - defaults: { - parentId: 0, - content: '', - isCompleted: 0, - priority: 0 - }, + if (window.hackflowyOffline) + return this.offlineSync.apply(this, arguments); + else + return Backbone.sync.apply(this, arguments); + }, - toggelCompletedStatus:function(isCompleted){ - var prev_isCompleted = isCompleted, - self = this; - this.save({'isCompleted':isCompleted}, - { - success:function(){}, - error:function(){ - //REVERT BACK ON ERROR - self.set({'isCompleted':prev_isCompleted}); - } - }); - } + defaults: { + parentId: 0, + content: '', + isCompleted: 0, + priority: 0 + }, - }); + toggelCompletedStatus: function (isCompleted) { + var prev_isCompleted = isCompleted, + self = this; + this.save({ + 'isCompleted': isCompleted + }, { + success: function () {}, + error: function () { + //REVERT BACK ON ERROR + self.set({ + 'isCompleted': prev_isCompleted + }); + } + }); + } -return TaskModel; + }); + + // a couple of vars backbone.localforage needs in the sync function + TaskModel.prototype.sync.localforage = TaskModel.prototype.offlineSync._localeForageKeyFn; + TaskModel.prototype.sync._localeForageKeyFn = TaskModel.prototype.offlineSync._localeForageKeyFn; + TaskModel.prototype.sync._localforageNamespace = TaskModel.prototype.offlineSync._localforageNamespace; + + return TaskModel; }); From cf89df815dbd07ba374e483114eed7feb3e600a8 Mon Sep 17 00:00:00 2001 From: Is Isilon Date: Sat, 30 Jan 2016 07:59:39 +0800 Subject: [PATCH 2/6] Improved insertion order of new nodes --- public/javascripts/views/list.js | 22 +++++++++++++++++----- public/templates/task.html | 4 ++-- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/public/javascripts/views/list.js b/public/javascripts/views/list.js index 0f02a72..12e844c 100644 --- a/public/javascripts/views/list.js +++ b/public/javascripts/views/list.js @@ -27,7 +27,7 @@ define( this.listenTo(this.collection, 'add', this.renderTask); - /** Load demo data and warn users **/ + /** Load demo data **/ function loadDemoData() { for (var i = 0; i < demoData.length; i++) { var task = Tasks.add(demoData[i]); @@ -44,7 +44,7 @@ define( this.collection.fetch({ success: success, error: function () { - // switch to localforage database if server isn't present + // switch to localforage database if server isn't present and fetch again window.hackflowyOffline=true; $('#header').append('
Running in offline mode, data may be lost
'); Tasks.fetch({ @@ -67,18 +67,30 @@ define( }); var a = taskView.render(); if (a.model.get('parentId') === 0) { + // inset it at the end of the root list this.$el.append(a.el); } else { + // insert after the currently edited sibling (same parent) + // or after the last sibling var parent = $('*[data-id="' + a.model.get('parentId') + '"]'); - if (parent.length === 0) { + var siblings = $('*[data-parent-id="' + a.model.get('parentId') + '"]').parents('li') + if (siblings.length>0){ + var editingSibling = siblings.filter('.editing') + var lastSibling = siblings.filter(':last'); + if (editingSibling) + a.$el.insertAfter(editingSibling); + else + a.$el.insertAfter(lastSibling); + } + else if (parent.length < 0) { + a.$el.insertAfter(parent.parents('li:first')); + } else { // TODO deal with loading order console.error("Parent not rendered yet: ", { selector: parent.selector, task: task }); this.$el.append(a.el); - } else { - a.$el.insertBefore(parent.parents('li:first')); } } } diff --git a/public/templates/task.html b/public/templates/task.html index 8eb73bc..ab36747 100644 --- a/public/templates/task.html +++ b/public/templates/task.html @@ -17,9 +17,9 @@ <% if(model.isCompleted){%> - + <%}else {%> - + <%}%> From c17412477d5dbb454a331ec4f4fb7ba4cce11030 Mon Sep 17 00:00:00 2001 From: Is Isilon Date: Sat, 30 Jan 2016 21:00:33 +0800 Subject: [PATCH 3/6] Added nested rendering, collapsing nodes, uses marionette --- bower.json | 8 +- public/index.html | 44 +++++- public/javascripts/app.js | 11 +- public/javascripts/models/task.js | 4 +- public/javascripts/util/constants.js | 9 +- public/javascripts/views/list.js | 121 ++++++++++------ public/javascripts/views/page.js | 2 +- public/javascripts/views/task.js | 207 +++++++++++++-------------- public/stylesheets/app.css | 17 ++- public/templates/task.html | 12 +- 10 files changed, 264 insertions(+), 171 deletions(-) diff --git a/bower.json b/bower.json index 4857b9e..21be4e3 100644 --- a/bower.json +++ b/bower.json @@ -15,13 +15,13 @@ "text": "requirejs-text#~2.0.14", "jquery": "~2.2.0", "backbone": "~1.2.3", - "backbone.localStorage": "1.1.16", "lodash": "~4.0.1", "requirejs": "~2.1.22", - "underscore": "~1.8.3", "modernizr": "~3.3.1", - "zepto": "~1.1.6", "localforage": "~1.3.3", - "localforage-backbone": "~0.6.2" + "localforage-backbone": "~0.6.2", + "normalize-css": "normalize.css#~3.0.3", + "backbone.marionette": "~2.4.4", + "underscore": "~1.8.3" } } diff --git a/public/index.html b/public/index.html index 116828e..91c69bf 100644 --- a/public/index.html +++ b/public/index.html @@ -53,7 +53,7 @@ var elem = document.getElementById("footer-qoute"); var newQoute = qoutes[Math.floor(Math.random() * qoutes.length)] + ' <3 Open Source.'; //if (newQoute.length>100){ - // TODO(wassname) truncate long qoutes with a more link + // TODO(wassname) truncate long qoutes with a more link //} elem.textContent = newQoute @@ -67,4 +67,46 @@ +
+ + + + + + + diff --git a/public/javascripts/app.js b/public/javascripts/app.js index 7ffa76f..2176cec 100644 --- a/public/javascripts/app.js +++ b/public/javascripts/app.js @@ -3,23 +3,18 @@ require.config({ //load lib files required paths: { jquery: '../bower_components/jquery/dist/jquery', - lodash: "../bower_components/lodash/dist/lodash.min", + underscore: "../bower_components/underscore/underscore", backbone: '../bower_components/backbone/backbone', localforage: '../bower_components/localforage/dist/localforage', localforagebackbone: '../bower_components/localforage-backbone/dist/localforage.backbone', modernizr: "vendor/custom.modernizr", socket: "../bower_components/socket.io-client/socket.io", text: '../bower_components/text/text', - }, - map: { - "*": { - // alias underscore to lodash for backbone - "underscore": "lodash" - } + marionette: '../bower_components/backbone.marionette/lib/backbone.marionette' }, shim: { backbone: { - deps: ["lodash", "jquery"], + deps: ["underscore", "jquery"], exports: "Backbone" } }, diff --git a/public/javascripts/models/task.js b/public/javascripts/models/task.js index ac48cff..c01f551 100644 --- a/public/javascripts/models/task.js +++ b/public/javascripts/models/task.js @@ -13,6 +13,7 @@ function ( var TaskModel = Backbone.Model.extend({ offlineSync: Backbone.localforage.sync('TaskModel'), + /** switches sync between server and local databases **/ sync: function () { @@ -26,7 +27,8 @@ function ( parentId: 0, content: '', isCompleted: 0, - priority: 0 + priority: 0, + id: '', }, toggelCompletedStatus: function (isCompleted) { diff --git a/public/javascripts/util/constants.js b/public/javascripts/util/constants.js index f193243..52cc4bb 100644 --- a/public/javascripts/util/constants.js +++ b/public/javascripts/util/constants.js @@ -2,13 +2,16 @@ define( [], function() { - + var constants = { - ENTER_KEY :13 + ENTER_KEY :13, + UP_ARROW: 38, + DOWN_ARROW: 40, + TAB: 9, }; return constants; -}); \ No newline at end of file +}); diff --git a/public/javascripts/views/list.js b/public/javascripts/views/list.js index 12e844c..c295db6 100644 --- a/public/javascripts/views/list.js +++ b/public/javascripts/views/list.js @@ -3,7 +3,9 @@ define( 'backbone', 'collections/list', 'views/task', - 'data/demo' + 'data/demo', + 'text!../../templates/task.html', + 'marionette' ], function ( @@ -11,21 +13,29 @@ define( Backbone, List, TaskView, - demoData + demoData, + listTemplate, + Marionette ) { - var ListView = Backbone.View.extend({ + // The tree's root: a simple collection view that renders + // a recursive tree structure for each item in the collection + var ListView = Backbone.Marionette.CollectionView.extend({ el: $("#main .children"), + childView: TaskView, + template: '#list-view-template', events: { 'click #add': 'addTask' }, initialize: function () { - Tasks = this.collection = new List(); + var self = this; - this.listenTo(this.collection, 'add', this.renderTask); + Tasks = new List(); + + // this.listenTo(this.collection, 'add', this.renderTask); /** Load demo data **/ function loadDemoData() { @@ -35,64 +45,91 @@ define( } } - function success(data) { + function success(children, data, promise) { // load demo data if the server returns nothing - if (data.length === 0) + if (children.length === 0) loadDemoData(); + else { + this.collection = {models:Tasks.filter({'parentId':0})}; + this.render(); + } } - this.collection.fetch({ + Tasks.fetch({ success: success, error: function () { + // switch to localforage database if server isn't present and fetch again window.hackflowyOffline=true; $('#header').append('
Running in offline mode, data may be lost
'); Tasks.fetch({ - success: success + success: success, + context: this }); - } + }, + context: this }); }, - render: function () { - this.collection.each(function (task) { - this.renderTask(task); - }, this); - }, + // Only show direct children + filter: function (child, index, collection) { + return child.get('parentId') === 0; + }, + + // render: function () { + // var models = Tasks.filter({'parentId':0}); + // + // _.each(models, function(model, index) { + // var taskView = new this.childView({ + // model: model, + // collection: model.collection + // }); + // var a = taskView.render(); + // this.$el.append(a.el); + // }, this); + // + // this.triggerMethod('render', this); + // return this; + // }, renderTask: function (task) { var taskView = new TaskView({ model: task }); var a = taskView.render(); - if (a.model.get('parentId') === 0) { - // inset it at the end of the root list - this.$el.append(a.el); - } else { - // insert after the currently edited sibling (same parent) - // or after the last sibling - var parent = $('*[data-id="' + a.model.get('parentId') + '"]'); - var siblings = $('*[data-parent-id="' + a.model.get('parentId') + '"]').parents('li') - if (siblings.length>0){ - var editingSibling = siblings.filter('.editing') - var lastSibling = siblings.filter(':last'); - if (editingSibling) - a.$el.insertAfter(editingSibling); - else - a.$el.insertAfter(lastSibling); - } - else if (parent.length < 0) { - a.$el.insertAfter(parent.parents('li:first')); - } else { - // TODO deal with loading order - console.error("Parent not rendered yet: ", { - selector: parent.selector, - task: task - }); - this.$el.append(a.el); - } - } + this.$el.append(a.el); + // + // if (a.model.get('parentId') === 0) { + // // insert it at the end of the top-level items + // this.$el.append(a.el); + // } else { + // + // // TODO use dom nesting instead of data attributes + // var parent = $('*[data-id="' + a.model.get('parentId') + '"]').parents('li:first'); + // var siblings = parent.find('.children:first li'); + // var editingSibling = siblings.filter('.editing:first'); + // + // if (editingSibling.length > 0){ + // // insert it after open sibling + // a.$el.insertAfter(editingSibling); + // } + // else if (parent.length > 0) { + // // insert under parent + // a.$el.appendTo(parent.find('.children:first')); + // } else { + // // we have an orphan :( + // // insert at root for lack of a better option + // this.$el.append(a.el); + // + // // TODO deal with loading order + // console.error("Parent not rendered yet: ", { + // selector: parent.selector, + // task: task + // }); + // + // } + // } } }); diff --git a/public/javascripts/views/page.js b/public/javascripts/views/page.js index 6868d93..9bdeca2 100644 --- a/public/javascripts/views/page.js +++ b/public/javascripts/views/page.js @@ -22,7 +22,7 @@ Task }, initialize: function() { - this.listView = new ListView(); + listView = this.listView = new ListView(); this.input = $('#newTask'); }, diff --git a/public/javascripts/views/task.js b/public/javascripts/views/task.js index 84b0e20..9b6c2c6 100644 --- a/public/javascripts/views/task.js +++ b/public/javascripts/views/task.js @@ -5,7 +5,8 @@ define( 'backbone', 'socket', 'util/constants', - 'text!../../templates/task.html' + 'text!../../templates/task.html', + 'marionette', ], function ( @@ -13,136 +14,130 @@ define( Backbone, io, constants, - taskTemplate + taskTemplate, + Marionette ) { + // The recursive tree view http://jsfiddle.net/wassname/zf61mLvh/2/ + var TaskView = Backbone.Marionette.CompositeView.extend({ - var TaskView = Backbone.View.extend({ + template: '#task-view-template', + tagName: 'ul', + className: "shift", - tagName: 'li', - template: taskTemplate, + ui: { + input: '.task input', + options: '.task .options:first' + }, events: { - 'click .task': 'edit', - 'blur .edit': 'close', - 'keyup .edit': 'handleKeyup', - 'keypress .edit': 'update', - 'mouseover .link': 'showOptions', - 'mouseout .link': 'hideOptions', - 'click .complete': 'markComplete', - 'click .uncomplete': 'unmarkComlete', - 'click .note': 'addNote' + 'click .task:first': 'edit', + 'blur .edit:first': 'close', + 'keydown .edit:first': 'handleKey', + 'keypress .edit:first': 'handleKey', + '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' }, initialize: function () { + var task = this; + + // backlink + this.model.view = this; + + this.collection = this.model.collection; + + // events this.listenTo(this.model, 'change', this.render); this.listenTo(this.model, 'destroy', this.remove); - this.socket = io.connect(); - var task = this; - this.socket.on('task', function (data) { - if (task.model.id == data.id) { - task.model.set({ - 'content': data.content, - 'isCompleted': data.isCompleted - }); - } - }); + + // updates from server + if (!window.hackflowyOffline){ + this.socket = io.connect(); + this.socket.on('task', function (data) { + if (task.model.id == data.id) { + task.model.set({ + 'content': data.content, + 'isCompleted': data.isCompleted + }); + } else { + console.error("task.model.id != data.id",task.model.id , data.id); + } + }); + } }, - render: function () { - var tmpl = _.template(this.template); - var task = this; - this.$el.html(tmpl({ - model: this.model.toJSON() - })); - if (this.model.get('parentId') != 0) { - // add a shift[n] class for n-indents - this.$el.addClass('shift1'); - var className = $('*[data-id="' + this.model.get('parentId') + '"]').parents('li:first').attr('class'); - if (className != undefined && className != 0 && className.substring(0, 5) == 'shift') { - this.$el.removeClass(); - this.$el.addClass('shift' + (parseInt(className.charAt(5)) + 1)); - } - } - this.$input = this.$('.edit:first'); - - return this; + // Only show direct children + filter: function (child, index, collection) { + return child.get('parentId') === this.model.get('id'); }, edit: function () { this.$el.addClass('editing'); - this.$input.focus(); + this.$('.edit:first').focus(); }, - handleKeyup: function (e) { - // down arrow - if (e.keyCode == 40) + handleKey: function (e) { + if (e.keyCode === constants.ENTER_KEY) + this.addNote(e.currentTarget); + if (e.keyCode == constants.DOWN_ARROW) this.$el.next('li').find('input').focus(); - // Up arrow - else if (e.keyCode == 38) + else if (e.keyCode == constants.UP_ARROW) this.$el.prev('li').find('input').focus(); // shift and tab - if (e.shiftKey && e.keyCode == 9) { - var model = this.$el.next('li').find('input').data('id'); - model = Tasks.get(model); - var old_parent = model.get('parentId'); - old_parent = Tasks.get(old_parent); - var new_parent = old_parent.get('parentId'); - if (new_parent == null) new_parent = 0; - model.set('parentId', new_parent); - model.save({ - content: model.get('content'), - parentId: model.get('parentId') - }); + if (e.shiftKey && e.keyCode == constants.TAB) { + e.preventDefault(); + var newParentId = this.$el.parents('ul:first').find('input:first').data('parent-id'); + if (newParentId === null) newParentId = 0; + this.model.set('parentId', newParentId); + this.model.save(); + Tasks.get(newParentId).view.render() + this.model.view.ui.input.focus(); } - // tab - else if (e.keyCode == 9) { - var parent = this.$el.prev('li').prev('li').find('input').data('id'); - var current = this.$el.prev('li').find('input').data('id'); - var model = Tasks.get(current); - model.set('parentId', parent); - model.save({ - content: model.get('content'), - parentId: model.get('parentId') - }); + else if (e.keyCode == constants.TAB) { + e.preventDefault(); + var newParentId = this.$el.prev('ul').find('input:first').data('id'); + this.model.set('parentId', newParentId); + this.model.save(); + this.model.view.remove(); + Tasks.get(newParentId).view.render(); + this.model.view.ui.input.focus(); } - this.socket.emit('task', { - id: this.model.id, - parentId: this.model.parentId, - content: this.$input.val().trim(), - isCompleted: this.model.toJSON().isCompleted - }); + if (!window.hackflowyOffline){ + this.socket.emit('task', { + id: this.model.id, + parentId: this.model.parentId, + content: this.$('.edit:first').val().trim(), + isCompleted: this.model.toJSON().isCompleted + }); + } else { + + } }, - update: function (e) { - if (e.which === constants.ENTER_KEY) { - this.addNote(e.currentTarget); - } - - }, /** Finish editing an item **/ close: function () { - var value = this.$input.val().trim(); - if (value === '') { + var value = this.$('.edit:first').val().trim(); + if (value === '') + // remove empty items this.model.destroy(); - // collection.at(this.model.get('id')).destroy(); - } else { - this.model.save({ - content: value, - parentId: this.model.attributes.parentId - }); - } + else + this.model.save({content: value}); this.$el.removeClass('editing'); }, showOptions: function () { - this.$el.find('.options').show(); + this.ui.options.show(); }, hideOptions: function () { - this.$el.find('.options').hide(); + this.ui.options.hide(); }, markComplete: function () { @@ -165,23 +160,23 @@ define( }); }, - /** - * Add a new blank note - * @param {object} inputEle Input elelement from current item - */ - addNote: function (inputEle) { - var $inputEle = $(inputEle); - var currentId = $inputEle.data('id') || 0; - parentId = currentId !== 0 ? Tasks.get(currentId).get('parentId') : 0; + /** Add a new blank note **/ + addNote: function () { + var currentId = this.ui.input.data('id') || 0; + parentId = this.ui.input.data('parent-id'); - Tasks.add({ - content: '', + var task = Tasks.add({ parentId: parentId }); - $inputEle.blur(); - $inputEle.closest('li').next('li').find('input').focus(); - } + 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'); + }, }); return TaskView; diff --git a/public/stylesheets/app.css b/public/stylesheets/app.css index fdbdde4..c659a62 100644 --- a/public/stylesheets/app.css +++ b/public/stylesheets/app.css @@ -5603,12 +5603,20 @@ body #hackflowy #main .children li .task .with-children > .link { body #hackflowy #main .children li .task .open > .link { background-color: white; } + +/* Show that this item has folded contents */ +body #hackflowy #main .children li.folded .task .link { + z-index: 7; + background-color: #ddd; +} + /* line 105, ../sass/app.scss */ body #hackflowy #main .children li .task .link:hover { cursor: pointer; z-index: 8; background-color: #aaa; } + /* line 110, ../sass/app.scss */ body #hackflowy #main .children li .task input.edit { box-shadow: none; @@ -5684,6 +5692,12 @@ 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; } @@ -5713,7 +5727,7 @@ cursor: pointer; background: #ddd; border: 1px solid #bbb; width: 80px; -height: 58px; +min-height: 58px; padding: 10px 0 5px 10px; margin-bottom: 10px; border-radius: 4px; @@ -5773,6 +5787,7 @@ border-left: 15px solid transparent; border-right: 15px solid transparent; } + .task-completed{ text-decoration: line-through; } diff --git a/public/templates/task.html b/public/templates/task.html index ab36747..5ff37c1 100644 --- a/public/templates/task.html +++ b/public/templates/task.html @@ -1,10 +1,12 @@ +<% if(content){%> +
  • +
  • +<%}%> From e1bb801e9f059b015258e90689be606c5fa80b91 Mon Sep 17 00:00:00 2001 From: Is Isilon Date: Sun, 31 Jan 2016 00:03:55 +0800 Subject: [PATCH 4/6] Fixed indenting, up and down arrows, basic sorting --- public/javascripts/collections/list.js | 5 ++ public/javascripts/util/constants.js | 1 + public/javascripts/views/list.js | 70 ++------------------ public/javascripts/views/task.js | 89 ++++++++++++++++++-------- 4 files changed, 74 insertions(+), 91 deletions(-) diff --git a/public/javascripts/collections/list.js b/public/javascripts/collections/list.js index a2467e5..b702b45 100644 --- a/public/javascripts/collections/list.js +++ b/public/javascripts/collections/list.js @@ -29,6 +29,11 @@ localforageBackbone return Backbone.sync.apply(this, arguments); }, + /** sort by priority then created date **/ + comporator: function(child){ + return [child.get('priority'),child.get('createdAt')]; + }, + url: '/tasks' }); diff --git a/public/javascripts/util/constants.js b/public/javascripts/util/constants.js index 52cc4bb..b6c8006 100644 --- a/public/javascripts/util/constants.js +++ b/public/javascripts/util/constants.js @@ -9,6 +9,7 @@ function() { UP_ARROW: 38, DOWN_ARROW: 40, TAB: 9, + CNTRL: 17, }; diff --git a/public/javascripts/views/list.js b/public/javascripts/views/list.js index c295db6..81cb339 100644 --- a/public/javascripts/views/list.js +++ b/public/javascripts/views/list.js @@ -18,8 +18,7 @@ define( Marionette ) { - // The tree's root: a simple collection view that renders - // a recursive tree structure for each item in the collection + // renders recursive tree structure for each item in collection var ListView = Backbone.Marionette.CollectionView.extend({ el: $("#main .children"), @@ -33,9 +32,7 @@ define( initialize: function () { var self = this; - Tasks = new List(); - - // this.listenTo(this.collection, 'add', this.renderTask); + this.collection = Tasks = new List(); /** Load demo data **/ function loadDemoData() { @@ -50,7 +47,6 @@ define( if (children.length === 0) loadDemoData(); else { - this.collection = {models:Tasks.filter({'parentId':0})}; this.render(); } } @@ -60,7 +56,8 @@ define( error: function () { // switch to localforage database if server isn't present and fetch again - window.hackflowyOffline=true; + // from there + window.hackflowyOffline = true; $('#header').append('
    Running in offline mode, data may be lost
    '); Tasks.fetch({ success: success, @@ -74,63 +71,8 @@ define( // Only show direct children filter: function (child, index, collection) { - return child.get('parentId') === 0; - }, - - // render: function () { - // var models = Tasks.filter({'parentId':0}); - // - // _.each(models, function(model, index) { - // var taskView = new this.childView({ - // model: model, - // collection: model.collection - // }); - // var a = taskView.render(); - // this.$el.append(a.el); - // }, this); - // - // this.triggerMethod('render', this); - // return this; - // }, - - renderTask: function (task) { - var taskView = new TaskView({ - model: task - }); - var a = taskView.render(); - this.$el.append(a.el); - // - // if (a.model.get('parentId') === 0) { - // // insert it at the end of the top-level items - // this.$el.append(a.el); - // } else { - // - // // TODO use dom nesting instead of data attributes - // var parent = $('*[data-id="' + a.model.get('parentId') + '"]').parents('li:first'); - // var siblings = parent.find('.children:first li'); - // var editingSibling = siblings.filter('.editing:first'); - // - // if (editingSibling.length > 0){ - // // insert it after open sibling - // a.$el.insertAfter(editingSibling); - // } - // else if (parent.length > 0) { - // // insert under parent - // a.$el.appendTo(parent.find('.children:first')); - // } else { - // // we have an orphan :( - // // insert at root for lack of a better option - // this.$el.append(a.el); - // - // // TODO deal with loading order - // console.error("Parent not rendered yet: ", { - // selector: parent.selector, - // task: task - // }); - // - // } - // } - } + return child.get('parentId') === 0; + }, }); diff --git a/public/javascripts/views/task.js b/public/javascripts/views/task.js index 9b6c2c6..9d1f66b 100644 --- a/public/javascripts/views/task.js +++ b/public/javascripts/views/task.js @@ -1,5 +1,3 @@ - - define( ['jquery', 'backbone', @@ -55,7 +53,7 @@ define( this.listenTo(this.model, 'destroy', this.remove); // updates from server - if (!window.hackflowyOffline){ + if (!window.hackflowyOffline) { this.socket = io.connect(); this.socket.on('task', function (data) { if (task.model.id == data.id) { @@ -64,16 +62,23 @@ define( 'isCompleted': data.isCompleted }); } else { - console.error("task.model.id != data.id",task.model.id , data.id); + console.error("task.model.id != data.id", task.model.id, data.id); } }); } }, - // Only show direct children + // override marionette filter to filter displayed children filter: function (child, index, collection) { - return child.get('parentId') === this.model.get('id'); + 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; }, edit: function () { @@ -84,32 +89,58 @@ define( handleKey: function (e) { if (e.keyCode === constants.ENTER_KEY) this.addNote(e.currentTarget); - if (e.keyCode == constants.DOWN_ARROW) - this.$el.next('li').find('input').focus(); - else if (e.keyCode == constants.UP_ARROW) - this.$el.prev('li').find('input').focus(); - - // shift and tab - if (e.shiftKey && e.keyCode == constants.TAB) { + else if (e.ctrlKey && e.keyCode == constants.DOWN_ARROW){ e.preventDefault(); - var newParentId = this.$el.parents('ul:first').find('input:first').data('parent-id'); - if (newParentId === null) newParentId = 0; - this.model.set('parentId', newParentId); - this.model.save(); - Tasks.get(newParentId).view.render() + 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){ + 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) { + 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(); + } else { + console.warn("Can't untab any further"); + } + } + // indent one more, by changing parent else if (e.keyCode == constants.TAB) { e.preventDefault(); - var newParentId = this.$el.prev('ul').find('input:first').data('id'); - this.model.set('parentId', newParentId); - this.model.save(); - this.model.view.remove(); - Tasks.get(newParentId).view.render(); - this.model.view.ui.input.focus(); + 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(); + } else { + console.warn("Can't tab any further"); + } } - if (!window.hackflowyOffline){ + if (!window.hackflowyOffline) { this.socket.emit('task', { id: this.model.id, parentId: this.model.parentId, @@ -126,9 +157,12 @@ define( 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}); + this.model.save({ + content: value + }); this.$el.removeClass('editing'); }, @@ -162,6 +196,7 @@ define( /** Add a new blank note **/ addNote: function () { + // TODO add it after the current one var currentId = this.ui.input.data('id') || 0; parentId = this.ui.input.data('parent-id'); @@ -173,7 +208,7 @@ define( }, /** Fold children of the clicked element */ - foldChildren: function(){ + foldChildren: function () { this.$el.find('ul').toggle(); this.$el.find('li').toggleClass('folded'); }, From e0a32e38f1215de5a66e90ecccb41c2bab95db53 Mon Sep 17 00:00:00 2001 From: Is Isilon Date: Sun, 31 Jan 2016 15:34:34 +0800 Subject: [PATCH 5/6] 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 { %> + + <% } %> +
    +
  • -<%}%> +
    From 7c802873e0482f896181a97229b72f02b376d95d Mon Sep 17 00:00:00 2001 From: Is Isilon Date: Sun, 31 Jan 2016 15:35:09 +0800 Subject: [PATCH 6/6] List template --- public/templates/list.html | 1 + 1 file changed, 1 insertion(+) create mode 100644 public/templates/list.html diff --git a/public/templates/list.html b/public/templates/list.html new file mode 100644 index 0000000..1080d6a --- /dev/null +++ b/public/templates/list.html @@ -0,0 +1 @@ +
  • <%= nodeName %>