]> git.za3k.com Git - flowy.git/commitdiff
Add basic editing
authorZachary Vance <vanceza@gmail.com>
Thu, 9 Apr 2015 05:59:46 +0000 (22:59 -0700)
committerZachary Vance <vanceza@gmail.com>
Thu, 9 Apr 2015 05:59:46 +0000 (22:59 -0700)
dist/flowy.js
dist/flowy.unwrapped.js
src/models/todo.js
src/views/todo.js

index 1174feb84b199041bf04ff0cd180f6767ec0306b..acf7799a2abcff742ae269aae8aa393713085659 100644 (file)
@@ -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 $("<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");
@@ -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
+    }
 });
 
 /**
index 0dcaa6c5b78bfaa841ebd755bd280a4cfc694c27..9cb41b7b8ac86ab16976dc8daf04e296e6c3fa18 100644 (file)
@@ -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 $("<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");
@@ -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
+    }
 });
 
 /**
index 1b5f2305dcf6d444a91a3ef3311d5e23a18f651d..b04d9226c1991d2949eb3c926917209a91cd97ce 100644 (file)
@@ -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
+    }
 });
index abba964515994744f77af5c3c0eac5255e8c02b7..f4caa524344470b366180d707840c0d38f69c828 100644 (file)
@@ -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 $("<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");
@@ -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'));