tagName: 'div',
className: 'todo',
events: {
- "click > .text": "toggleComplete",
+ //"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
},
initialize: function() {
this.listenTo(this.model, "change", this.render);
toggleComplete: function(e) {
this.model.toggleComplete();
e.stopPropagation();
+ return this;
+ },
+ startEditingText: function() {
+ this.$el.find("> .text").focus();
+ return this;
+ },
+ stopEditingText: function() {
+ this.$el.find("> .text").blur();
+ return this;
+ },
+ decodeText: function(encodedText) {
+ return $("<div/>").html(encodedText).text();
},
- template: "<div class=\"text\">{{text}}</div><div class=\"bullets\"></div>",
+ textChange: function(e) {
+ // TODO: Handle backspace on totally empty bullet and beginning of line
+ 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();
+ }
+ } else if (lines.length > 2) {
+ console.log("TODO: Support copy-paste in textChange");
+
+ } else {
+ console.log("unexpected number of lines in textChange");
+ }
+ return this;
+ },
+ template: "<div class=\"text\" contenteditable=\"plaintext-only\">{{text}}</div><div class=\"bullets\"></div>",
addChild: function(el, position) {
if(typeof position === 'undefined') {
console.log("TodoView:addChild called without a position");
return this;
},
render: function() {
+ if (this.$el.find("> .text").is(":focus")) {
+ // Don't re-render during editing
+ return this;
+ }
var oldChildren = this.$el.find("> .bullets > *").detach(); // detach keeps handlers
this.$el.html(Mustache.to_html(this.template, this.model.toJSON())); // Should hopefully be model.attributes
this.$el.toggleClass('completed', this.model.get('completed'));
isParentLoaded: function(collection, collectionView) {
return this.isTopLevel(collection) ? true : (this.getParent(collection) && collectionView.getView(this.getParent(collection)));
},
+ addTodoBefore: function(todo) {
+ // Todo always goes as the previous sibling
+ // TODO
+ },
+ addTodoAfter: function(todo) {
+ // If there are children, the todo goes as the first child.
+ // Otherwise, the todo goes as the next sibling
+ // TODO
+ }
});
/**
tagName: 'div',
className: 'todo',
events: {
- "click > .text": "toggleComplete",
+ //"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
},
initialize: function() {
this.listenTo(this.model, "change", this.render);
toggleComplete: function(e) {
this.model.toggleComplete();
e.stopPropagation();
+ return this;
+ },
+ startEditingText: function() {
+ this.$el.find("> .text").focus();
+ return this;
+ },
+ stopEditingText: function() {
+ this.$el.find("> .text").blur();
+ return this;
+ },
+ decodeText: function(encodedText) {
+ return $("<div/>").html(encodedText).text();
},
- template: "<div class=\"text\">{{text}}</div><div class=\"bullets\"></div>",
+ textChange: function(e) {
+ // TODO: Handle backspace on totally empty bullet and beginning of line
+ 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();
+ }
+ } else if (lines.length > 2) {
+ console.log("TODO: Support copy-paste in textChange");
+
+ } else {
+ console.log("unexpected number of lines in textChange");
+ }
+ return this;
+ },
+ template: "<div class=\"text\" contenteditable=\"plaintext-only\">{{text}}</div><div class=\"bullets\"></div>",
addChild: function(el, position) {
if(typeof position === 'undefined') {
console.log("TodoView:addChild called without a position");
return this;
},
render: function() {
+ if (this.$el.find("> .text").is(":focus")) {
+ // Don't re-render during editing
+ return this;
+ }
var oldChildren = this.$el.find("> .bullets > *").detach(); // detach keeps handlers
this.$el.html(Mustache.to_html(this.template, this.model.toJSON())); // Should hopefully be model.attributes
this.$el.toggleClass('completed', this.model.get('completed'));
isParentLoaded: function(collection, collectionView) {
return this.isTopLevel(collection) ? true : (this.getParent(collection) && collectionView.getView(this.getParent(collection)));
},
+ addTodoBefore: function(todo) {
+ // Todo always goes as the previous sibling
+ // TODO
+ },
+ addTodoAfter: function(todo) {
+ // If there are children, the todo goes as the first child.
+ // Otherwise, the todo goes as the next sibling
+ // TODO
+ }
});
/**
isParentLoaded: function(collection, collectionView) {
return this.isTopLevel(collection) ? true : (this.getParent(collection) && collectionView.getView(this.getParent(collection)));
},
+ addTodoBefore: function(todo) {
+ // Todo always goes as the previous sibling
+ // TODO
+ },
+ addTodoAfter: function(todo) {
+ // If there are children, the todo goes as the first child.
+ // Otherwise, the todo goes as the next sibling
+ // TODO
+ }
});
tagName: 'div',
className: 'todo',
events: {
- "click > .text": "toggleComplete",
+ //"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
},
initialize: function() {
this.listenTo(this.model, "change", this.render);
toggleComplete: function(e) {
this.model.toggleComplete();
e.stopPropagation();
+ return this;
+ },
+ startEditingText: function() {
+ this.$el.find("> .text").focus();
+ return this;
+ },
+ stopEditingText: function() {
+ this.$el.find("> .text").blur();
+ return this;
+ },
+ decodeText: function(encodedText) {
+ return $("<div/>").html(encodedText).text();
+ },
+ textChange: function(e) {
+ // TODO: Handle backspace on totally empty bullet and beginning of line
+ 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();
+ }
+ } else if (lines.length > 2) {
+ console.log("TODO: Support copy-paste in textChange");
+
+ } else {
+ console.log("unexpected number of lines in textChange");
+ }
+ return this;
},
- template: "<div class=\"text\">{{text}}</div><div class=\"bullets\"></div>",
+ template: "<div class=\"text\" contenteditable=\"plaintext-only\">{{text}}</div><div class=\"bullets\"></div>",
addChild: function(el, position) {
if(typeof position === 'undefined') {
console.log("TodoView:addChild called without a position");
return this;
},
render: function() {
+ if (this.$el.find("> .text").is(":focus")) {
+ // Don't re-render during editing
+ return this;
+ }
var oldChildren = this.$el.find("> .bullets > *").detach(); // detach keeps handlers
this.$el.html(Mustache.to_html(this.template, this.model.toJSON())); // Should hopefully be model.attributes
this.$el.toggleClass('completed', this.model.get('completed'));