//"click > .checkbox": "toggleComplete",
"input > .text": "textChange",
"blur > .text": "render", // Because the model shouldn't update the view during active editing, add a re-render at the end
+ "keydown > .text": "backspace",
},
initialize: function() {
+ this.childViewPositions = [];
this.listenTo(this.model, "change", this.render);
this.listenTo(this.model, 'destroy', this.remove);
},
e.stopPropagation();
return this;
},
- startEditingText: function() {
- this.$el.find("> .text").focus();
+ startEditingText: function(options) {
+ options = options || {};
+ if (!options.atEnd) {
+ this.$el.find("> .text").focus();
+ } else {
+ this.$el.find("> .text").focus();
+ this.$el.find("> .text").val(this.$el.find("> .text"));
+ }
return this;
},
stopEditingText: function() {
decodeText: function(encodedText) {
return $("<div/>").html(encodedText).text();
},
+ backspace: function(e) {
+ // TODO: Handle backspace at beginning of non-empty line
+ if (e.keyCode !== 8 /* backspace */ && e.keyCode !== 46 /* delete */) {
+ return;
+ }
+ if (this.model.get("text") !== "") {
+ return;
+ }
+ e.preventDefault();
+ // TODO: Focus end of previous node
+ //var previousNode = this.model.previousNode(this.model.collection);
+ this.model.remove(this.model.collection);
+ //var previousNodeView = this.collectionView.getViewFor(previousNode);
+ //var previousNodeView.startEditingText({"atEnd":true});
+ },
textChange: function(e) {
- // TODO: Handle backspace on totally empty bullet and beginning of line
+ var collection = this.model.collection;
var lines = $(e.target).html().split(/<br\\?>/);
if (lines.length === 0) {
console.log("unexpected number of lines in textChange");
} else if (lines.length === 1) {
// Normal edit
this.model.setText(this.decodeText(lines[0]));
- } else if (lines.length === 2) {
- // If there's a line break in the text, they pressed "Enter", and it's more complicated.
- // We're here going to duplicate Workflowy's behavior:
- // If the line break is at the beginning
- // - Make a sibling node before, focus the first, empty node.
- // If the line break is in the middle
- // - Make a new sibling node after, and move all the children to that sibling node (basically, make a new sibling node BEFORE). Focus the second node.
- // If the line break is at the end
- // - If there are no children, make a sibling node after. Focus the second node.
- // - If there are children, make the new node the first child. Focus the second node.
- // NOTE: Copy-paste is overridden so there can't be more than one line break.
- // NOTE: Shift-enter is overridden and handled seperately, to allow "notes" spanning multiple lines.
- if (lines[1].length === 0) { // Line break at end
- console.log("TODO: Line breaks not implemented <end>");
- this.model.setText(this.decodeText(lines[0]));
- //var emptyAfter = this.model.addTodoAfter({text: this.decodeText(lines[1])}); // Child or not depending on whether this has children
- this.stopEditingText();
- //emptyAfter.view.startEditingText();
- // Focus emptyAfter
- } else if (lines[0].length === 0) { // Line break at beginning
- console.log("TODO: Line breaks not implemented <start>");
- //var emptyBefore = this.model.addTodoBefore({text: this.decodeText(lines[0])});
- this.model.setText(this.decodeText(lines[1]));
- this.stopEditingText();
- //emptyBefore.view.startEditingText();
- // Focus emptyBefore
- } else { // Line break in middle
- console.log("TODO: Line breaks not implemented <middle>");
- //var newNode = this.model.addTodoBefore({text: this.decodeText(lines[1])});
- this.model.setText(this.decodeText(lines[1]));
- this.stopEditingText(); // For re-render
- this.startEditingText();
- }
+
+ // If there's a line break in the text, they pressed "Enter", and it's more complicated.
+ // We're here going to duplicate Workflowy's behavior:
+ // If the line break is at the beginning
+ // - Make a sibling node before, focus the first, empty node.
+ // If the line break is in the middle
+ // - Make a new sibling node after, and move all the children to that sibling node (basically, make a new sibling node BEFORE). Focus the second node.
+ // If the line break is at the end
+ // - If there are no children, make a sibling node after. Focus the second node.
+ // - If there are children, make the new node the first child. Focus the second node.
+ // NOTE: Copy-paste is overridden so there can't be more than one line break.
+ // NOTE: Shift-enter is overridden and handled seperately, to allow "notes" spanning multiple lines.
+
+ } else if ((lines.length === 2 && lines[1].length === 0) || (lines.length === 3 && lines[1].length === 0 && lines[2].length === 0)) { // Line break at end
+ console.log("TODO: Line breaks not implemented <end>");
+ this.model.setText(this.decodeText(lines[0]));
+ var emptyAfter = this.model.addTodoAfter({text: this.decodeText(lines[1])}, collection); // Child or not depending on whether this has children
+ this.stopEditingText();
+ //emptyAfter.view.startEditingText();
+ // TODO: Focus emptyAfter
+ } else if (lines.length === 2 && lines[0].length === 0) { // Line break at beginning
+ console.log("TODO: Line breaks not implemented <start>");
+ var emptyBefore = this.model.addTodoBefore({text: this.decodeText(lines[0])}, collection);
+ this.model.setText(this.decodeText(lines[1]));
+ this.stopEditingText();
+ //emptyBefore.view.startEditingText();
+ // TODO: Focus emptyBefore
+ } else if (lines.length === 2) { // Line break in middle
+ console.log("TODO: Line breaks not implemented <middle>");
+ var newNode = this.model.addTodoBefore({text: this.decodeText(lines[0])}, collection);
+ this.model.setText(this.decodeText(lines[1]));
+ this.stopEditingText(); // For re-render
+ this.startEditingText();
+ // Keep focus on current node (second half)
} else if (lines.length > 2) {
+ console.log(lines.length);
+ console.log($(e.target).html());
+ console.log(lines);
console.log("TODO: Support copy-paste in textChange");
-
} else {
console.log("unexpected number of lines in textChange");
}
},
template: "<div class=\"text\" contenteditable=\"plaintext-only\">{{text}}</div><div class=\"bullets\"></div>",
addChild: function(el, position) {
- if(typeof position === 'undefined') {
+ if(typeof position === 'undefined' || position === -1) {
console.log("TodoView:addChild called without a position");
+ return this;
+ }
+ // Position was given as an index into the children array, not relative to inserted elements in the DOM.
+ // So, convert to index relative to things inserted so far
+ var relativePosition = _.chain(this.childViewPositions).where(function(x) { return x<= position; }).size().value();
+ this.childViewPositions.push(position);
+ if (relativePosition === 0) {
+ this.$el.find("> .bullets").prepend(el);
+ } else {
+ this.$el.find("> .bullets > *").eq(relativePosition-1).after(el);
}
- this.$el.find("> .bullets").append(el);
return this;
},
render: function() {
isParentLoaded: function(collection, collectionView) {
return this.isTopLevel(collection) ? true : (this.getParent(collection) && collectionView.getView(this.getParent(collection)));
},
- addTodoBefore: function(todo) {
+ findChild: function(childId) {
+ return this.get("bullets").indexOf(childId);
+ },
+ insertChild: function(childTodoModel, index) {
+ childTodoModel.set("parent", this.id);
+ var bullets = this.get("bullets");
+ bullets.splice(index, 0, childTodoModel.id);
+ this.set("bullets", bullets);
+ return this;
+ },
+ removeChild: function(childTodoModel, collection) {
+ if (childTodoModel.get("bullets").length > 0) {
+ console.log("Cannot delete node with children.");
+ return;
+ }
+ var index = this.findChild(childTodoModel.id);
+ if (index < 0) {
+ console.log("Tried to delete node from incorrect parent.");
+ return;
+ }
+ var parent = this;
+ childTodoModel.destroy({"success": function(model, response) {
+ var bullets = parent.get("bullets");
+ bullets.splice(index, 1);
+ parent.save({"bullets":bullets});
+ }});
+ return this;
+ },
+ remove: function(collection) {
+ this.getParent(collection).removeChild(this, collection);
+ },
+ addTodoBefore: function(todo, collection) {
// Todo always goes as the previous sibling
- // TODO
+ var parent = this.getParent(collection);
+ var todoModel = collection.create(_.extend(todo, {"parent": parent.id}));
+ parent.insertChild(todoModel, parent.findChild(this.id));
+ parent.save();
+
+ collection.trigger("add", todoModel);
+ return todoModel;
},
- addTodoAfter: function(todo) {
+ addTodoAfter: function(todo, collection) {
// If there are children, the todo goes as the first child.
// Otherwise, the todo goes as the next sibling
- // TODO
+ var todoModel;
+ if (this.get("bullets").length > 0) {
+ todoModel = collection.create(_.extend(todo, {"parent": this.id}));
+ this.insertChild(todoModel, 0);
+ this.save();
+ } else {
+ parent = this.getParent(collection);
+ todoModel = collection.create(_.extend(todo, {"parent": parent.id}));
+ parent.insertChild(todoModel, parent.findChild(this.id)+1);
+ parent.save();
+ }
+
+ collection.trigger("add", todoModel);
+ return todoModel;
}
});
},
model: TodoModel,
localStorage: new Backbone.LocalStorage("todos-backbone"),
- nextOrder: function() {
- if (!this.length) return 1;
- return this.last().get('order') + 1;
- },
- comparator: 'order',
+ comparator: 'id',
});
/**
var testTodos = [
new TodoModel({
parent: null,
+ id: 0,
+ text: "Root (will be invisible in final)",
+ bullets: [1, 5],
+ }),
+ new TodoModel({
+ parent: 0,
id: 1,
text: "Daily todos",
bullets: [2,3,4],
text: "Eat green eggs and ham",
}),
new TodoModel({
- parent: null,
+ parent: 0,
id: 5,
text: "To do this year",
collapsed: true,
el: $("#todo-app"),
initialize: function(options) {
options = _.defaults({}, options, appDefaults);
- this.listenTo(todos, 'add', this.addOne);
- this.listenTo(todos, 'reset', this.addAll);
- this.list = options.list || new FlowDocModel();
+ this.list = options.list || new FlowyDocModel();
+ this.listenTo(this.list, 'add', this.addOne);
+ this.listenTo(this.list, 'reset', this.addAll);
+ var self = this;
+ this.$("#reset-button").on("click", function() {
+ var model;
+ for (var i = self.list.length - 1; i >= 0; i--) {
+ self.list.at(i).destroy();
+ }
+ self.list.reset(testTodos);
+ self.list.each(function(e) {
+ e.save();
+ });
+ });
this.views = {}; // A list of views for each element in the collection
this.list.fetch();
},
console.log("View rendered more than once for todo #" + todo.id);
return;
}
- var view = new TodoView({model: todo});
+ var view = new TodoView({model: todo, collection: this.list});
if (todo.isTopLevel(this.list)) {
this.setView(todo, view);
this.$("#todo-list").append(view.render().el);
- } else if (todo.isParentLoaded(this.list, this)) {
+ } else if (todo.isParentLoaded(this.list, this) && todo.getParent(this.list).findChild(todo.id) >= 0 /* because move/insert is nonatomic */) {
this.setView(todo, view);
var parent = todo.getParent(this.list);
var parentView = this.getView(parent);
- parentView.addChild(view.render().el);
+ parentView.addChild(view.render().el, parent.findChild(todo.id));
}
// Find unrendered descendents and render them, too.
return this.views[model.id];
},
addAll: function() {
+ this.views = {};
+ this.$("#todo-list").html(null);
this.list.each(this.addOne, this);
},
});
//"click > .checkbox": "toggleComplete",
"input > .text": "textChange",
"blur > .text": "render", // Because the model shouldn't update the view during active editing, add a re-render at the end
+ "keydown > .text": "backspace",
},
initialize: function() {
+ this.childViewPositions = [];
this.listenTo(this.model, "change", this.render);
this.listenTo(this.model, 'destroy', this.remove);
},
e.stopPropagation();
return this;
},
- startEditingText: function() {
- this.$el.find("> .text").focus();
+ startEditingText: function(options) {
+ options = options || {};
+ if (!options.atEnd) {
+ this.$el.find("> .text").focus();
+ } else {
+ this.$el.find("> .text").focus();
+ this.$el.find("> .text").val(this.$el.find("> .text"));
+ }
return this;
},
stopEditingText: function() {
decodeText: function(encodedText) {
return $("<div/>").html(encodedText).text();
},
+ backspace: function(e) {
+ // TODO: Handle backspace at beginning of non-empty line
+ if (e.keyCode !== 8 /* backspace */ && e.keyCode !== 46 /* delete */) {
+ return;
+ }
+ if (this.model.get("text") !== "") {
+ return;
+ }
+ e.preventDefault();
+ // TODO: Focus end of previous node
+ //var previousNode = this.model.previousNode(this.model.collection);
+ this.model.remove(this.model.collection);
+ //var previousNodeView = this.collectionView.getViewFor(previousNode);
+ //var previousNodeView.startEditingText({"atEnd":true});
+ },
textChange: function(e) {
- // TODO: Handle backspace on totally empty bullet and beginning of line
+ var collection = this.model.collection;
var lines = $(e.target).html().split(/<br\\?>/);
if (lines.length === 0) {
console.log("unexpected number of lines in textChange");
} else if (lines.length === 1) {
// Normal edit
this.model.setText(this.decodeText(lines[0]));
- } else if (lines.length === 2) {
- // If there's a line break in the text, they pressed "Enter", and it's more complicated.
- // We're here going to duplicate Workflowy's behavior:
- // If the line break is at the beginning
- // - Make a sibling node before, focus the first, empty node.
- // If the line break is in the middle
- // - Make a new sibling node after, and move all the children to that sibling node (basically, make a new sibling node BEFORE). Focus the second node.
- // If the line break is at the end
- // - If there are no children, make a sibling node after. Focus the second node.
- // - If there are children, make the new node the first child. Focus the second node.
- // NOTE: Copy-paste is overridden so there can't be more than one line break.
- // NOTE: Shift-enter is overridden and handled seperately, to allow "notes" spanning multiple lines.
- if (lines[1].length === 0) { // Line break at end
- console.log("TODO: Line breaks not implemented <end>");
- this.model.setText(this.decodeText(lines[0]));
- //var emptyAfter = this.model.addTodoAfter({text: this.decodeText(lines[1])}); // Child or not depending on whether this has children
- this.stopEditingText();
- //emptyAfter.view.startEditingText();
- // Focus emptyAfter
- } else if (lines[0].length === 0) { // Line break at beginning
- console.log("TODO: Line breaks not implemented <start>");
- //var emptyBefore = this.model.addTodoBefore({text: this.decodeText(lines[0])});
- this.model.setText(this.decodeText(lines[1]));
- this.stopEditingText();
- //emptyBefore.view.startEditingText();
- // Focus emptyBefore
- } else { // Line break in middle
- console.log("TODO: Line breaks not implemented <middle>");
- //var newNode = this.model.addTodoBefore({text: this.decodeText(lines[1])});
- this.model.setText(this.decodeText(lines[1]));
- this.stopEditingText(); // For re-render
- this.startEditingText();
- }
+
+ // If there's a line break in the text, they pressed "Enter", and it's more complicated.
+ // We're here going to duplicate Workflowy's behavior:
+ // If the line break is at the beginning
+ // - Make a sibling node before, focus the first, empty node.
+ // If the line break is in the middle
+ // - Make a new sibling node after, and move all the children to that sibling node (basically, make a new sibling node BEFORE). Focus the second node.
+ // If the line break is at the end
+ // - If there are no children, make a sibling node after. Focus the second node.
+ // - If there are children, make the new node the first child. Focus the second node.
+ // NOTE: Copy-paste is overridden so there can't be more than one line break.
+ // NOTE: Shift-enter is overridden and handled seperately, to allow "notes" spanning multiple lines.
+
+ } else if ((lines.length === 2 && lines[1].length === 0) || (lines.length === 3 && lines[1].length === 0 && lines[2].length === 0)) { // Line break at end
+ console.log("TODO: Line breaks not implemented <end>");
+ this.model.setText(this.decodeText(lines[0]));
+ var emptyAfter = this.model.addTodoAfter({text: this.decodeText(lines[1])}, collection); // Child or not depending on whether this has children
+ this.stopEditingText();
+ //emptyAfter.view.startEditingText();
+ // TODO: Focus emptyAfter
+ } else if (lines.length === 2 && lines[0].length === 0) { // Line break at beginning
+ console.log("TODO: Line breaks not implemented <start>");
+ var emptyBefore = this.model.addTodoBefore({text: this.decodeText(lines[0])}, collection);
+ this.model.setText(this.decodeText(lines[1]));
+ this.stopEditingText();
+ //emptyBefore.view.startEditingText();
+ // TODO: Focus emptyBefore
+ } else if (lines.length === 2) { // Line break in middle
+ console.log("TODO: Line breaks not implemented <middle>");
+ var newNode = this.model.addTodoBefore({text: this.decodeText(lines[0])}, collection);
+ this.model.setText(this.decodeText(lines[1]));
+ this.stopEditingText(); // For re-render
+ this.startEditingText();
+ // Keep focus on current node (second half)
} else if (lines.length > 2) {
+ console.log(lines.length);
+ console.log($(e.target).html());
+ console.log(lines);
console.log("TODO: Support copy-paste in textChange");
-
} else {
console.log("unexpected number of lines in textChange");
}
},
template: "<div class=\"text\" contenteditable=\"plaintext-only\">{{text}}</div><div class=\"bullets\"></div>",
addChild: function(el, position) {
- if(typeof position === 'undefined') {
+ if(typeof position === 'undefined' || position === -1) {
console.log("TodoView:addChild called without a position");
+ return this;
+ }
+ // Position was given as an index into the children array, not relative to inserted elements in the DOM.
+ // So, convert to index relative to things inserted so far
+ var relativePosition = _.chain(this.childViewPositions).where(function(x) { return x<= position; }).size().value();
+ this.childViewPositions.push(position);
+ if (relativePosition === 0) {
+ this.$el.find("> .bullets").prepend(el);
+ } else {
+ this.$el.find("> .bullets > *").eq(relativePosition-1).after(el);
}
- this.$el.find("> .bullets").append(el);
return this;
},
render: function() {
isParentLoaded: function(collection, collectionView) {
return this.isTopLevel(collection) ? true : (this.getParent(collection) && collectionView.getView(this.getParent(collection)));
},
- addTodoBefore: function(todo) {
+ findChild: function(childId) {
+ return this.get("bullets").indexOf(childId);
+ },
+ insertChild: function(childTodoModel, index) {
+ childTodoModel.set("parent", this.id);
+ var bullets = this.get("bullets");
+ bullets.splice(index, 0, childTodoModel.id);
+ this.set("bullets", bullets);
+ return this;
+ },
+ removeChild: function(childTodoModel, collection) {
+ if (childTodoModel.get("bullets").length > 0) {
+ console.log("Cannot delete node with children.");
+ return;
+ }
+ var index = this.findChild(childTodoModel.id);
+ if (index < 0) {
+ console.log("Tried to delete node from incorrect parent.");
+ return;
+ }
+ var parent = this;
+ childTodoModel.destroy({"success": function(model, response) {
+ var bullets = parent.get("bullets");
+ bullets.splice(index, 1);
+ parent.save({"bullets":bullets});
+ }});
+ return this;
+ },
+ remove: function(collection) {
+ this.getParent(collection).removeChild(this, collection);
+ },
+ addTodoBefore: function(todo, collection) {
// Todo always goes as the previous sibling
- // TODO
+ var parent = this.getParent(collection);
+ var todoModel = collection.create(_.extend(todo, {"parent": parent.id}));
+ parent.insertChild(todoModel, parent.findChild(this.id));
+ parent.save();
+
+ collection.trigger("add", todoModel);
+ return todoModel;
},
- addTodoAfter: function(todo) {
+ addTodoAfter: function(todo, collection) {
// If there are children, the todo goes as the first child.
// Otherwise, the todo goes as the next sibling
- // TODO
+ var todoModel;
+ if (this.get("bullets").length > 0) {
+ todoModel = collection.create(_.extend(todo, {"parent": this.id}));
+ this.insertChild(todoModel, 0);
+ this.save();
+ } else {
+ parent = this.getParent(collection);
+ todoModel = collection.create(_.extend(todo, {"parent": parent.id}));
+ parent.insertChild(todoModel, parent.findChild(this.id)+1);
+ parent.save();
+ }
+
+ collection.trigger("add", todoModel);
+ return todoModel;
}
});
},
model: TodoModel,
localStorage: new Backbone.LocalStorage("todos-backbone"),
- nextOrder: function() {
- if (!this.length) return 1;
- return this.last().get('order') + 1;
- },
- comparator: 'order',
+ comparator: 'id',
});
/**
var testTodos = [
new TodoModel({
parent: null,
+ id: 0,
+ text: "Root (will be invisible in final)",
+ bullets: [1, 5],
+ }),
+ new TodoModel({
+ parent: 0,
id: 1,
text: "Daily todos",
bullets: [2,3,4],
text: "Eat green eggs and ham",
}),
new TodoModel({
- parent: null,
+ parent: 0,
id: 5,
text: "To do this year",
collapsed: true,
el: $("#todo-app"),
initialize: function(options) {
options = _.defaults({}, options, appDefaults);
- this.listenTo(todos, 'add', this.addOne);
- this.listenTo(todos, 'reset', this.addAll);
- this.list = options.list || new FlowDocModel();
+ this.list = options.list || new FlowyDocModel();
+ this.listenTo(this.list, 'add', this.addOne);
+ this.listenTo(this.list, 'reset', this.addAll);
+ var self = this;
+ this.$("#reset-button").on("click", function() {
+ var model;
+ for (var i = self.list.length - 1; i >= 0; i--) {
+ self.list.at(i).destroy();
+ }
+ self.list.reset(testTodos);
+ self.list.each(function(e) {
+ e.save();
+ });
+ });
this.views = {}; // A list of views for each element in the collection
this.list.fetch();
},
console.log("View rendered more than once for todo #" + todo.id);
return;
}
- var view = new TodoView({model: todo});
+ var view = new TodoView({model: todo, collection: this.list});
if (todo.isTopLevel(this.list)) {
this.setView(todo, view);
this.$("#todo-list").append(view.render().el);
- } else if (todo.isParentLoaded(this.list, this)) {
+ } else if (todo.isParentLoaded(this.list, this) && todo.getParent(this.list).findChild(todo.id) >= 0 /* because move/insert is nonatomic */) {
this.setView(todo, view);
var parent = todo.getParent(this.list);
var parentView = this.getView(parent);
- parentView.addChild(view.render().el);
+ parentView.addChild(view.render().el, parent.findChild(todo.id));
}
// Find unrendered descendents and render them, too.
return this.views[model.id];
},
addAll: function() {
+ this.views = {};
+ this.$("#todo-list").html(null);
this.list.each(this.addOne, this);
},
});
<div id="todo-list">
</div>
+ <button id="reset-button">Everyone heard me say "reset button", right?</button>
</div>
</body>
</html>
<div id="todo-list">
</div>
+ <button id="reset-button">Everyone heard me say "reset button", right?</button>
</div>
</body>
</html>
},
model: TodoModel,
localStorage: new Backbone.LocalStorage("todos-backbone"),
- nextOrder: function() {
- if (!this.length) return 1;
- return this.last().get('order') + 1;
- },
- comparator: 'order',
+ comparator: 'id',
});
isParentLoaded: function(collection, collectionView) {
return this.isTopLevel(collection) ? true : (this.getParent(collection) && collectionView.getView(this.getParent(collection)));
},
- addTodoBefore: function(todo) {
+ findChild: function(childId) {
+ return this.get("bullets").indexOf(childId);
+ },
+ insertChild: function(childTodoModel, index) {
+ childTodoModel.set("parent", this.id);
+ var bullets = this.get("bullets");
+ bullets.splice(index, 0, childTodoModel.id);
+ this.set("bullets", bullets);
+ return this;
+ },
+ removeChild: function(childTodoModel, collection) {
+ if (childTodoModel.get("bullets").length > 0) {
+ console.log("Cannot delete node with children.");
+ return;
+ }
+ var index = this.findChild(childTodoModel.id);
+ if (index < 0) {
+ console.log("Tried to delete node from incorrect parent.");
+ return;
+ }
+ var parent = this;
+ childTodoModel.destroy({"success": function(model, response) {
+ var bullets = parent.get("bullets");
+ bullets.splice(index, 1);
+ parent.save({"bullets":bullets});
+ }});
+ return this;
+ },
+ remove: function(collection) {
+ this.getParent(collection).removeChild(this, collection);
+ },
+ addTodoBefore: function(todo, collection) {
// Todo always goes as the previous sibling
- // TODO
+ var parent = this.getParent(collection);
+ var todoModel = collection.create(_.extend(todo, {"parent": parent.id}));
+ parent.insertChild(todoModel, parent.findChild(this.id));
+ parent.save();
+
+ collection.trigger("add", todoModel);
+ return todoModel;
},
- addTodoAfter: function(todo) {
+ addTodoAfter: function(todo, collection) {
// If there are children, the todo goes as the first child.
// Otherwise, the todo goes as the next sibling
- // TODO
+ var todoModel;
+ if (this.get("bullets").length > 0) {
+ todoModel = collection.create(_.extend(todo, {"parent": this.id}));
+ this.insertChild(todoModel, 0);
+ this.save();
+ } else {
+ parent = this.getParent(collection);
+ todoModel = collection.create(_.extend(todo, {"parent": parent.id}));
+ parent.insertChild(todoModel, parent.findChild(this.id)+1);
+ parent.save();
+ }
+
+ collection.trigger("add", todoModel);
+ return todoModel;
}
});
var testTodos = [
new TodoModel({
parent: null,
+ id: 0,
+ text: "Root (will be invisible in final)",
+ bullets: [1, 5],
+ }),
+ new TodoModel({
+ parent: 0,
id: 1,
text: "Daily todos",
bullets: [2,3,4],
text: "Eat green eggs and ham",
}),
new TodoModel({
- parent: null,
+ parent: 0,
id: 5,
text: "To do this year",
collapsed: true,
el: $("#todo-app"),
initialize: function(options) {
options = _.defaults({}, options, appDefaults);
- this.listenTo(todos, 'add', this.addOne);
- this.listenTo(todos, 'reset', this.addAll);
- this.list = options.list || new FlowDocModel();
+ this.list = options.list || new FlowyDocModel();
+ this.listenTo(this.list, 'add', this.addOne);
+ this.listenTo(this.list, 'reset', this.addAll);
+ var self = this;
+ this.$("#reset-button").on("click", function() {
+ var model;
+ for (var i = self.list.length - 1; i >= 0; i--) {
+ self.list.at(i).destroy();
+ }
+ self.list.reset(testTodos);
+ self.list.each(function(e) {
+ e.save();
+ });
+ });
this.views = {}; // A list of views for each element in the collection
this.list.fetch();
},
console.log("View rendered more than once for todo #" + todo.id);
return;
}
- var view = new TodoView({model: todo});
+ var view = new TodoView({model: todo, collection: this.list});
if (todo.isTopLevel(this.list)) {
this.setView(todo, view);
this.$("#todo-list").append(view.render().el);
- } else if (todo.isParentLoaded(this.list, this)) {
+ } else if (todo.isParentLoaded(this.list, this) && todo.getParent(this.list).findChild(todo.id) >= 0 /* because move/insert is nonatomic */) {
this.setView(todo, view);
var parent = todo.getParent(this.list);
var parentView = this.getView(parent);
- parentView.addChild(view.render().el);
+ parentView.addChild(view.render().el, parent.findChild(todo.id));
}
// Find unrendered descendents and render them, too.
return this.views[model.id];
},
addAll: function() {
+ this.views = {};
+ this.$("#todo-list").html(null);
this.list.each(this.addOne, this);
},
});
//"click > .checkbox": "toggleComplete",
"input > .text": "textChange",
"blur > .text": "render", // Because the model shouldn't update the view during active editing, add a re-render at the end
+ "keydown > .text": "backspace",
},
initialize: function() {
+ this.childViewPositions = [];
this.listenTo(this.model, "change", this.render);
this.listenTo(this.model, 'destroy', this.remove);
},
e.stopPropagation();
return this;
},
- startEditingText: function() {
- this.$el.find("> .text").focus();
+ startEditingText: function(options) {
+ options = options || {};
+ if (!options.atEnd) {
+ this.$el.find("> .text").focus();
+ } else {
+ this.$el.find("> .text").focus();
+ this.$el.find("> .text").val(this.$el.find("> .text"));
+ }
return this;
},
stopEditingText: function() {
decodeText: function(encodedText) {
return $("<div/>").html(encodedText).text();
},
+ backspace: function(e) {
+ // TODO: Handle backspace at beginning of non-empty line
+ if (e.keyCode !== 8 /* backspace */ && e.keyCode !== 46 /* delete */) {
+ return;
+ }
+ if (this.model.get("text") !== "") {
+ return;
+ }
+ e.preventDefault();
+ // TODO: Focus end of previous node
+ //var previousNode = this.model.previousNode(this.model.collection);
+ this.model.remove(this.model.collection);
+ //var previousNodeView = this.collectionView.getViewFor(previousNode);
+ //var previousNodeView.startEditingText({"atEnd":true});
+ },
textChange: function(e) {
- // TODO: Handle backspace on totally empty bullet and beginning of line
+ var collection = this.model.collection;
var lines = $(e.target).html().split(/<br\\?>/);
if (lines.length === 0) {
console.log("unexpected number of lines in textChange");
} else if (lines.length === 1) {
// Normal edit
this.model.setText(this.decodeText(lines[0]));
- } else if (lines.length === 2) {
- // If there's a line break in the text, they pressed "Enter", and it's more complicated.
- // We're here going to duplicate Workflowy's behavior:
- // If the line break is at the beginning
- // - Make a sibling node before, focus the first, empty node.
- // If the line break is in the middle
- // - Make a new sibling node after, and move all the children to that sibling node (basically, make a new sibling node BEFORE). Focus the second node.
- // If the line break is at the end
- // - If there are no children, make a sibling node after. Focus the second node.
- // - If there are children, make the new node the first child. Focus the second node.
- // NOTE: Copy-paste is overridden so there can't be more than one line break.
- // NOTE: Shift-enter is overridden and handled seperately, to allow "notes" spanning multiple lines.
- if (lines[1].length === 0) { // Line break at end
- console.log("TODO: Line breaks not implemented <end>");
- this.model.setText(this.decodeText(lines[0]));
- //var emptyAfter = this.model.addTodoAfter({text: this.decodeText(lines[1])}); // Child or not depending on whether this has children
- this.stopEditingText();
- //emptyAfter.view.startEditingText();
- // Focus emptyAfter
- } else if (lines[0].length === 0) { // Line break at beginning
- console.log("TODO: Line breaks not implemented <start>");
- //var emptyBefore = this.model.addTodoBefore({text: this.decodeText(lines[0])});
- this.model.setText(this.decodeText(lines[1]));
- this.stopEditingText();
- //emptyBefore.view.startEditingText();
- // Focus emptyBefore
- } else { // Line break in middle
- console.log("TODO: Line breaks not implemented <middle>");
- //var newNode = this.model.addTodoBefore({text: this.decodeText(lines[1])});
- this.model.setText(this.decodeText(lines[1]));
- this.stopEditingText(); // For re-render
- this.startEditingText();
- }
+
+ // If there's a line break in the text, they pressed "Enter", and it's more complicated.
+ // We're here going to duplicate Workflowy's behavior:
+ // If the line break is at the beginning
+ // - Make a sibling node before, focus the first, empty node.
+ // If the line break is in the middle
+ // - Make a new sibling node after, and move all the children to that sibling node (basically, make a new sibling node BEFORE). Focus the second node.
+ // If the line break is at the end
+ // - If there are no children, make a sibling node after. Focus the second node.
+ // - If there are children, make the new node the first child. Focus the second node.
+ // NOTE: Copy-paste is overridden so there can't be more than one line break.
+ // NOTE: Shift-enter is overridden and handled seperately, to allow "notes" spanning multiple lines.
+
+ } else if ((lines.length === 2 && lines[1].length === 0) || (lines.length === 3 && lines[1].length === 0 && lines[2].length === 0)) { // Line break at end
+ console.log("TODO: Line breaks not implemented <end>");
+ this.model.setText(this.decodeText(lines[0]));
+ var emptyAfter = this.model.addTodoAfter({text: this.decodeText(lines[1])}, collection); // Child or not depending on whether this has children
+ this.stopEditingText();
+ //emptyAfter.view.startEditingText();
+ // TODO: Focus emptyAfter
+ } else if (lines.length === 2 && lines[0].length === 0) { // Line break at beginning
+ console.log("TODO: Line breaks not implemented <start>");
+ var emptyBefore = this.model.addTodoBefore({text: this.decodeText(lines[0])}, collection);
+ this.model.setText(this.decodeText(lines[1]));
+ this.stopEditingText();
+ //emptyBefore.view.startEditingText();
+ // TODO: Focus emptyBefore
+ } else if (lines.length === 2) { // Line break in middle
+ console.log("TODO: Line breaks not implemented <middle>");
+ var newNode = this.model.addTodoBefore({text: this.decodeText(lines[0])}, collection);
+ this.model.setText(this.decodeText(lines[1]));
+ this.stopEditingText(); // For re-render
+ this.startEditingText();
+ // Keep focus on current node (second half)
} else if (lines.length > 2) {
+ console.log(lines.length);
+ console.log($(e.target).html());
+ console.log(lines);
console.log("TODO: Support copy-paste in textChange");
-
} else {
console.log("unexpected number of lines in textChange");
}
},
template: "<div class=\"text\" contenteditable=\"plaintext-only\">{{text}}</div><div class=\"bullets\"></div>",
addChild: function(el, position) {
- if(typeof position === 'undefined') {
+ if(typeof position === 'undefined' || position === -1) {
console.log("TodoView:addChild called without a position");
+ return this;
+ }
+ // Position was given as an index into the children array, not relative to inserted elements in the DOM.
+ // So, convert to index relative to things inserted so far
+ var relativePosition = _.chain(this.childViewPositions).where(function(x) { return x<= position; }).size().value();
+ this.childViewPositions.push(position);
+ if (relativePosition === 0) {
+ this.$el.find("> .bullets").prepend(el);
+ } else {
+ this.$el.find("> .bullets > *").eq(relativePosition-1).after(el);
}
- this.$el.find("> .bullets").append(el);
return this;
},
render: function() {