From: Zachary Vance Date: Thu, 9 Apr 2015 05:59:46 +0000 (-0700) Subject: Add basic editing X-Git-Url: https://git.za3k.com/?a=commitdiff_plain;h=42e5d563ad0b8db6f8b001badd667a1b9bd12804;p=flowy.git Add basic editing --- diff --git a/dist/flowy.js b/dist/flowy.js index 1174feb..acf7799 100644 --- a/dist/flowy.js +++ b/dist/flowy.js @@ -3,7 +3,9 @@ var TodoView = Backbone.View.extend({ 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); @@ -12,8 +14,69 @@ var TodoView = Backbone.View.extend({ 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 $("
").html(encodedText).text(); }, - template: "
{{text}}
", + textChange: function(e) { + // TODO: Handle backspace on totally empty bullet and beginning of line + var lines = $(e.target).html().split(//); + 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 "); + 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 "); + //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 "); + //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: "
{{text}}
", addChild: function(el, position) { if(typeof position === 'undefined') { console.log("TodoView:addChild called without a position"); @@ -22,6 +85,10 @@ var TodoView = Backbone.View.extend({ 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')); @@ -64,6 +131,15 @@ var TodoModel = Backbone.Model.extend({ 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 + } }); /** diff --git a/dist/flowy.unwrapped.js b/dist/flowy.unwrapped.js index 0dcaa6c..9cb41b7 100644 --- a/dist/flowy.unwrapped.js +++ b/dist/flowy.unwrapped.js @@ -2,7 +2,9 @@ var TodoView = Backbone.View.extend({ 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); @@ -11,8 +13,69 @@ var TodoView = Backbone.View.extend({ 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 $("
").html(encodedText).text(); }, - template: "
{{text}}
", + textChange: function(e) { + // TODO: Handle backspace on totally empty bullet and beginning of line + var lines = $(e.target).html().split(//); + 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 "); + 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 "); + //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 "); + //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: "
{{text}}
", addChild: function(el, position) { if(typeof position === 'undefined') { console.log("TodoView:addChild called without a position"); @@ -21,6 +84,10 @@ var TodoView = Backbone.View.extend({ 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')); @@ -63,6 +130,15 @@ var TodoModel = Backbone.Model.extend({ 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 + } }); /** diff --git a/src/models/todo.js b/src/models/todo.js index 1b5f230..b04d922 100644 --- a/src/models/todo.js +++ b/src/models/todo.js @@ -31,4 +31,13 @@ var TodoModel = Backbone.Model.extend({ 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 + } }); diff --git a/src/views/todo.js b/src/views/todo.js index abba964..f4caa52 100644 --- a/src/views/todo.js +++ b/src/views/todo.js @@ -2,7 +2,9 @@ var TodoView = Backbone.View.extend({ 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); @@ -11,8 +13,69 @@ var TodoView = Backbone.View.extend({ 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 $("
").html(encodedText).text(); + }, + textChange: function(e) { + // TODO: Handle backspace on totally empty bullet and beginning of line + var lines = $(e.target).html().split(//); + 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 "); + 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 "); + //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 "); + //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: "
{{text}}
", + template: "
{{text}}
", addChild: function(el, position) { if(typeof position === 'undefined') { console.log("TodoView:addChild called without a position"); @@ -21,6 +84,10 @@ var TodoView = Backbone.View.extend({ 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'));