]> git.za3k.com Git - flowy.git/commitdiff
Integrate Shortcut library with Backbone (bubbling is in wrong order)
authorZachary Vance <vanceza@gmail.com>
Wed, 20 May 2015 02:16:29 +0000 (19:16 -0700)
committerZachary Vance <vanceza@gmail.com>
Wed, 20 May 2015 02:16:29 +0000 (19:16 -0700)
dist/flowy.js
dist/flowy.unwrapped.js
src/library/viewShortcuts.js [new file with mode: 0644]
src/models/todo.js
src/views/app.js
src/views/todo.js

index c717510c25637c1bd4824eb90548d49714901091..3563e04c3bc3e6c84694ce529361e8b433128933 100644 (file)
@@ -60,8 +60,255 @@ function setRangeAtMarker(markerElement, options) {
   }
 }
 
+// TODO: More documentation
+// TODO: Removing objects/shortcuts/rebinding won't work until this bug is fixed in mousetrap: https://github.com/ccampbell/mousetrap/issues/267
+
+// firstShortcut = Shortcut.registerShortcut("Control + k", function() { console.log("Shortcut pressed."); });
+// firstShortcut.rebind("Control + m");
+// Shortcut.registerShortcut({keybinding: "L", objectType: "textbox", action: function() { console.log("L pressed while inside the textbox."); }});
+// o = Shortcut.addObject(aTextbox, "textbox"); // Can come before or after keyboard binding
+// o.remove() // Once we lose textbox focus, stop keeping track of it, for example
+
+var Shortcut = (function(document, _) {
+    _.mixin({
+        guid : function(){
+            return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
+                var r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8);
+                return v.toString(16);
+            });
+        }
+    });
+
+    var globalMousetrap = new Mousetrap();
+    var globalObject = {element: document, type: "global", isGlobal: true, mousetrap: globalMousetrap};
+    var Shortcut = {
+        _globalObject: globalObject, // The object passed to callbacks when a global keybinding is invoked.
+        globalMousetrap: globalMousetrap,
+        boundObjects: [globalObject],
+        shortcuts: {},
+        registerShortcut: function(shortcut) {
+            if (arguments.length > 1) {
+                shortcut = {
+                    keybinding: arguments[0],
+                    action: arguments[1],
+                    object: arguments[2],
+                };
+            }
+            var manager = this;
+            if (!shortcut.ownedByUs) shortcut = _.clone(shortcut);
+            _.defaults(shortcut, {
+                register: function() { manager.registerShortcut(this); },
+                unregister: function() { manager.unregisterShortcut(this); },
+                bind: function(object) { manager.bindShortcut(this, object); },
+                unbind: function(object) { manager.unbindShortcut(this, object); },
+                rebind: function(keybinding) { manager.rebindShortcut(this, keybinding); },
+                object: "global",
+
+                _keybinding: shortcut.keybinding || "",
+                action: function() {},
+                boundObjects: [],
+                ownedByUs: true,
+                id: _.guid(),
+            });
+            Object.defineProperty(shortcut, "keybinding", {
+                set: function (newKeybinding) { manager.rebindShortcut(shortcut, newKeybinding); },
+                get: function () { return shortcut._keybinding; },
+            });
+            if (this.shortcuts[shortcut.id]) return this.shortcuts[shortcut.id];
+            this.shortcuts[shortcut.id] = shortcut;
+            _.chain(this.boundObjects).where({'type':shortcut.object}).each(_.partial(this.bindShortcut, shortcut, _), this).value();
+            return shortcut;
+        },
+        unregisterShortcut: function(shortcut) {
+            _.each(_.clone(shortcut.boundObjects), _.partial(this.unbindShortcut, shortcut, _), this);
+            delete this.shortcuts[shortcut.id];
+        },
+        bindShortcut: function(shortcut, object) { // Do not call directly
+            if (!_.contains(shortcut.boundObjects, object)) {
+                object.mousetrap.bind(shortcut.keybinding, _.bind(this.shortcutPressed, this, shortcut, object));
+                shortcut.boundObjects.push(object);
+            }
+        },
+        unbindShortcut: function(shortcut, object) { // Do not call directly with an object
+            if (!object){
+                _.each(shortcut.boundObjects, _.partial(this.unbindShortcut, shortcut, _), this);
+            }
+            object.mousetrap.unbind(shortcut.keybinding);
+            shortcut.boundObjects = _.without(shortcut.boundObjects, object);
+        },
+        rebindShortcut: function(shortcut, keybinding) {
+            if (keybinding === shortcut.keybinding) return;
+            var oldKeybinding = shortcut.keybinding;
+            var objects = shortcut.boundObjects;
+            shortcut.unbind();
+            shorcut._keybinding = keybinding;
+            _.each(objects, shortcut.bind, shortcut);
+            if (this.onRebindShortcut) this.onRebindShortcut(shortcut, keybinding, oldKeybinding);
+        },
+        userRebindShortcut: function(shortcut, options) {
+            // Cancels on focus loss or if the user presses the escape key
+            options = _.defaults({}, options, {
+                element: undefined, // If the user clicks outside this element, cancel
+                onCancel: function() {},
+            });
+            var manager = this;
+            var cancelled = false;
+            var cancel = function() {
+                if (!cancelled) {
+                    cancelled = true;
+                    if (options.onCancel) options.onCancel();
+                }
+            };
+            if (element) element.addEventListener("blur", cancel);
+            Mousetrap.record(function (keybinding) { // TODO: requires plugin
+                if (_.equal(keybinding, ["ESCAPE"])) { // TODO: Test
+                    cancel();
+                } else {
+                    manager.rebindShortcut(shortcut, keybinding);
+                    if (element) element.removeEventListener("blur", cancel);
+                }
+            });
+        },
+        shortcutPressed: function(shortcut, object) {
+            if (shortcut.action) shortcut.action(object.element, shortcut, object.type);
+        },
+        onRebindShortcut: function(shortcut, keybinding, oldKeybinding) {
+            // NOTE: You may want to hook into this if you want to save/load user preferences
+            // shortcut.id is probably what you want and not the whole shortcut
+        },
+        addObject: function(element, type) {
+            var manager = this;
+            object = {
+                element:element,
+                type:type,
+                mousetrap: Mousetrap(element),
+                remove: function() { manager.removeObject(this); },
+                add: function() { manager.addObject(this.element, this.type); },
+            };
+            _.chain(this.shortcuts).where({'object': type}).each(_.partial(this.bindShortcut, _, object), this).value();
+            if (!_.contains(this.boundObjects)) this.boundObjects.push(object);
+            return object;
+        },
+        removeObject: function(object) {
+            _.each(this.shortcuts, _.partial(this.unbindShortcut, _, object), this);
+            this.boundObjects = _.without(this.boundObjects, object);
+        },
+        get globalObject() {
+            return this._globalObject;
+        },
+        set globalObject(newElement) {
+            this.removeObject(this.globalObject, "global");
+            this._globalObject = {
+                element: newElement,
+                type: "global",
+                isGlobal: true,
+                mousetrap: this.globalMousetrap,
+            };
+            _.chain(this.shortcuts).where({'object': "global"}).each(_.partial(this.bindShortcut, _, this.globalObject), this).value();
+            if (!_.contains(this.boundObjects)) this.boundObjects.push(this.globalObject);
+            return this.globalObject;
+        },
+        pause: function() { // TODO: These require plugin, test
+            this.boundObjects.each(function(object) {
+                object.mousetrap.pause();
+            });
+        },
+        unpause: function() {
+            this.boundObjects.each(function(object) {
+                object.mousetrap.unpause();
+            });
+        },
+        displayShortcuts: function(shortcuts, options) {
+            options = _.defaults({}, options, {
+                objectGrouping: "default", // Display one list, or group under headers by object type? "true" "false", or "default"
+                displayMultiple: true, // Display multiple shortcuts or just the canonical one?
+                allowRebind: false, // Insert javascript to allow rebinding shortcuts
+                highlightRepeats: true, // Warn about shortcuts which are bound to multiple actions (if user can remap shortcuts)
+            });
+            if (!shortcuts) shortcuts = this.shortcuts;
+            if (options.objectGrouping === "default") {
+                var multipleObjectTypes = _.chain(shortcuts).pluck('object').unique().count().value() > 1;
+                options.objectGrouping = multipleObjectTypes;
+            }
+            // TODO: Return HTML
+            return "";
+        },
+        _displayKeybinding: function(keybinding, options) {
+            options = _.defaults({}, options, { displayMultiple: true });
+            // TODO: Return HTML
+            return "";
+        },
+    };
+    return Shortcut;
+})(document, _);
+
+/**
+ * @depend ../library/shortcut.js
+ */
+
+
+// Integrates "Shortcuts" library with backbone views, to allow events in the form "Shortcut(id, description, default keycombo)": action
+// Shortcut selectors are not supported
+(function (View, Shortcut) {
+    var delegateEvents = View.delegateEvents;
+    var undelegateEvents = View.undelegateEvents;
+    var ShortcutRegex = /^Shortcut\("([^")]*)", ?"([^")]*)", ?"([^")]*)"\) (.*)$/;
+    function delegate(id, description, defaultKeybinding, objectType, callback){
+        var shortcut = Shortcut.registerShortcut({
+            id: id,
+            description: description,
+            keybinding: defaultKeybinding, // TODO: Add support for rebinding
+            object: objectType,
+            action: callback
+        });
+    }
+    function delegateMousetrapEvents(events, objectType) {
+        for (var key in events) {
+            var method = events[key];
+            if (!_.isFunction(method)) method = this[events[key]];
+            if (!method) continue;
+            var match = key.match(ShortcutRegex);
+            delegate.call(this, match[1], match[2], match[3], objectType, _.bind(method, this));
+        }
+        this.shortcutObjects = this.shortcutObjects || [];
+        if (objectType === "global") {
+            Shortcut.globalObject = this.$el[0];
+            this._shortcutObject = Shortcut.globalObject;
+        } else {
+            this._shortcutObject = Shortcut.addObject(this.$el[0], objectType);
+        }
+    }
+    View.delegateEvents = function(events) {
+        if (!(events || (events = _.result(this, 'events')))) return this;
+        var mousetrapEvents = {};
+        var nonMousetrapEvents = {};
+        for (var key in events) {
+            if (key.match(ShortcutRegex)) {
+                mousetrapEvents[key] = events[key];
+            } else {
+                nonMousetrapEvents[key] = events[key];
+            }
+        }
+        delegateEvents.call(this, nonMousetrapEvents);
+        if (_.size(mousetrapEvents) > 0) {
+            var type = this.shortcutObject || this.className;
+            if (!type) {
+                throw "Mousetrap events need to specify a type on the view object via .className or .shortcutObject. 'global' will catch all keystrokes rather than binding to the selector";
+            }
+            delegateMousetrapEvents.call(this, mousetrapEvents, type);
+        }
+        return this;
+    };
+    View.undelegateEvents = function() {
+        if (this._shortcutObject) Shortcut.removeObject(this._shortcutObject);
+        // Also undelegate mousetrap events
+        undelegateEvents.apply(this, arguments);
+    };
+})(Backbone.View.prototype, Shortcut);
+
 /**
  * @depend ../library/cursorToEnd.js
+ * @depend ../library/viewShortcuts.js
  */
 
 
@@ -69,21 +316,18 @@ var TodoView = Backbone.View.extend({
   tagName: 'div',
   className: 'todo',
   events: {
-    //"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": "keydown",
+    'Shortcut("toggleComplete", "Mark an item as complete or not", "ctrl+enter") > .text': "toggleComplete",
+    'Shortcut("backspace", "Combine an item with the previous item", "backspace") > .text': "backspace",
+    'Shortcut("delete", "Combine an item with the next item", "del") > .text': "delete",
   },
   initialize: function() {
     this.childViewPositions = [];
     this.listenTo(this.model, "change", this.render);
     this.listenTo(this.model, 'destroy', this.remove);
   },
-  toggleComplete: function(e) {
-    this.model.toggleComplete();
-    e.stopPropagation();
-    return this;
-  },
   startEditingText: function(options) {
     options = options || {};
     if (options.atEnd) {
@@ -110,16 +354,7 @@ var TodoView = Backbone.View.extend({
   decodeText: function(encodedText) {
     return $("<div/>").html(encodedText).text();
   },
-  keydown: function(e) {
-    if (e.keyCode == 46 /* backspace */ || e.keyCode == 8 /* delete */) {
-        return this.backspace(e);
-    } else if (e.keyCode == 17) {
-        return this.control(e);
-    } else {
-        console.log(e.keyCode + " key pressed");
-    }
-  },
-  control: function(e) {
+  toggleComplete: function(e) {
     this.stopEditingText();
     this.model.toggleComplete();
     var next = this.model.nextNode(this.model.collection, {"childrenAllowed":false});
@@ -137,7 +372,7 @@ var TodoView = Backbone.View.extend({
         e.preventDefault();
         this.model.remove(this.model.collection);
         previousNode.getView().startEditingText({"atEnd":true});
-    } else if (this.isFocusAtBeginning() && e.keyCode !== 46 /* backspace only */) {
+    } else if (this.isFocusAtBeginning()) {
         e.preventDefault();
         var text = this.model.get("text");
         this.model.remove(this.model.collection);
@@ -146,6 +381,20 @@ var TodoView = Backbone.View.extend({
         previousNode.getView().startEditingText({"atMarker": ".focus"});
     }
   },
+  "delete": function(e) {
+    if (this.model.hasChildren()) {
+        return;
+    }
+    var previousNode = this.model.previousNode(this.model.collection);
+    if (!previousNode) {
+        return;
+    }
+    if (this.model.get("text") === "") {
+        e.preventDefault();
+        this.model.remove(this.model.collection);
+        previousNode.getView().startEditingText({"atEnd":true});
+    }
+  },
   textChange: function(e) {
     var collection = this.model.collection;
     var lines = $(e.target).html().split(/<br\\?>/);
@@ -193,7 +442,7 @@ var TodoView = Backbone.View.extend({
     }
     return this;
   },
-  template: "<div class=\"text\" contenteditable=\"plaintext-only\">{{{text}}}</div><div class=\"bullets\"></div>",
+  template: "<div class=\"text mousetrap\" contenteditable=\"plaintext-only\">{{{text}}}</div><div class=\"bullets\"></div>",
   addChild: function(el, position) {
      if(typeof position === 'undefined' || position === -1) {
         console.log("TodoView:addChild called without a position");
@@ -260,12 +509,14 @@ var TodoModel = Backbone.Model.extend({
     },
     getPreviousSibling: function(collection) {
         var parent = this.getParent(collection);
+        if (!parent) return undefined;
         var index = parent.findChild(this.id);
         if (index < 0 || index === 0) return undefined;
         return parent.getChild(collection, index - 1);
     },
     getNextSibling: function(collection) {
         var parent = this.getParent(collection);
+        if (!parent) return undefined;
         var index = parent.findChild(this.id);
         var numChildren = parent.getChildrenCount();
         if (index < 0 || index === numChildren - 1) return undefined;
@@ -377,181 +628,11 @@ var FlowyDocModel = Backbone.Collection.extend({
     comparator: 'id',
 });
 
-// TODO: More documentation
-// TODO: Removing objects/shortcuts/rebinding won't work until this bug is fixed in mousetrap: https://github.com/ccampbell/mousetrap/issues/267
-
-// firstShortcut = Shortcut.registerShortcut("Control + k", function() { console.log("Shortcut pressed."); });
-// firstShortcut.rebind("Control + m");
-// Shortcut.registerShortcut({keybinding: "L", objectType: "textbox", action: function() { console.log("L pressed while inside the textbox."); }});
-// o = Shortcut.addObject(aTextbox, "textbox"); // Can come before or after keyboard binding
-// o.remove() // Once we lose textbox focus, stop keeping track of it, for example
-
-var Shortcut = (function(document, _) {
-    var globalMousetrap = new Mousetrap();
-    var globalObject = {element: document, type: "global", isGlobal: true, mousetrap: globalMousetrap};
-    var Shortcut = {
-        _globalObject: globalObject, // The object passed to callbacks when a global keybinding is invoked.
-        globalMousetrap: globalMousetrap,
-        boundObjects: [globalObject],
-        shortcuts: [],
-        registerShortcut: function(shortcut) {
-            if (arguments.length > 1) {
-                shortcut = {
-                    keybinding: arguments[0],
-                    action: arguments[1],
-                    object: arguments[2],
-                };
-            }
-            var manager = this;
-            if (!shortcut.ownedByUs) shortcut = _.clone(shortcut);
-            _.defaults(shortcut, {
-                register: function() { manager.registerShortcut(this); },
-                unregister: function() { manager.unregisterShortcut(this); },
-                bind: function(object) { manager.bindShortcut(this, object); },
-                unbind: function(object) { manager.unbindShortcut(this, object); },
-                rebind: function(keybinding) { manager.rebindShortcut(this, keybinding); },
-                object: "global",
-
-                _keybinding: shortcut.keybinding || "",
-                action: function() {},
-                boundObjects: [],
-                ownedByUs: true
-            });
-            Object.defineProperty(shortcut, "keybinding", {
-                set: function (newKeybinding) { manager.rebindShortcut(shortcut, newKeybinding); },
-                get: function () { return shortcut._keybinding; },
-            });
-            this.shortcuts.push(shortcut);
-            _.chain(this.boundObjects).where({'type':shortcut.object}).each(_.partial(this.bindShortcut, shortcut, _), this).value();
-            return shortcut;
-        },
-        unregisterShortcut: function(shortcut) {
-            _.each(_.clone(shortcut.boundObjects), _.partial(this.unbindShortcut, shortcut, _), this);
-        },
-        bindShortcut: function(shortcut, object) { // Do not call directly
-            if (!_.contains(shortcut.boundObjects, object)) {
-                object.mousetrap.bind(shortcut.keybinding, _.bind(this.shortcutPressed, this, shortcut, object));
-                shortcut.boundObjects.push(object);
-            }
-        },
-        unbindShortcut: function(shortcut, object) { // Do not call directly with an object
-            if (!object){
-                _.each(shortcut.boundObjects, _.partial(this.unbindShortcut, shortcut, _), this);
-            }
-            object.mousetrap.unbind(shortcut.keybinding);
-            shortcut.boundObjects = _.without(shortcut.boundObjects, object);
-        },
-        rebindShortcut: function(shortcut, keybinding) {
-            if (keybinding === shortcut.keybinding) return;
-            var oldKeybinding = shortcut.keybinding;
-            var objects = shortcut.boundObjects;
-            shortcut.unbind();
-            shorcut._keybinding = keybinding;
-            _.each(objects, shortcut.bind, shortcut);
-            if (this.onRebindShortcut) this.onRebindShortcut(shortcut, keybinding, oldKeybinding);
-        },
-        userRebindShortcut: function(shortcut, options) {
-            // Cancels on focus loss or if the user presses the escape key
-            options = _.defaults({}, options, {
-                element: undefined, // If the user clicks outside this element, cancel
-                onCancel: function() {},
-            });
-            var manager = this;
-            var cancelled = false;
-            var cancel = function() {
-                if (!cancelled) {
-                    cancelled = true;
-                    if (options.onCancel) options.onCancel();
-                }
-            };
-            if (element) element.addEventListener("blur", cancel);
-            Mousetrap.record(function (keybinding) { // TODO: requires plugin
-                if (_.equal(keybinding, ["ESCAPE"])) { // TODO: Test
-                    cancel();
-                } else {
-                    manager.rebindShortcut(shortcut, keybinding);
-                    if (element) element.removeEventListener("blur", cancel);
-                }
-            });
-        },
-        shortcutPressed: function(shortcut, object) {
-            if (shortcut.action) shortcut.action(object.element, shortcut, object.type);
-        },
-        onRebindShortcut: function(shortcut, keybinding, oldKeybinding) {
-            // NOTE: You may want to hook into this if you want to save/load user preferences
-            // shortcut.id is probably what you want and not the whole shortcut
-        },
-        addObject: function(element, type) {
-            var manager = this;
-            object = {
-                element:element,
-                type:type,
-                mousetrap: new Mousetrap(element),
-                remove: function() { manager.removeObject(this); },
-                add: function() { manager.addObject(this.element, this.type); },
-            };
-            _.chain(this.shortcuts).where({'object': type}).each(_.partial(this.bindShortcut, _, object), this).value();
-            if (!_.contains(this.boundObjects)) this.boundObjects.push(object);
-            return object;
-        },
-        removeObject: function(object) {
-            _.each(this.shortcuts, _.partial(this.unbindShortcut, _, object), this);
-            this.boundObjects = _.without(this.boundObjects, object);
-        },
-        get globalObject() {
-            return this._globalObject;
-        },
-        set globalObject(newElement) {
-            this.removeObject(this.globalObject, "global");
-            this._globalObject = {
-                element: newElement,
-                type: "global",
-                isGlobal: true,
-                mousetrap: this.globalMousetrap,
-            };
-            _.chain(this.shortcuts).where({'object': "global"}).each(_.partial(this.bindShortcut, _, this.globalObject), this).value();
-            if (!_.contains(this.boundObjects)) this.boundObjects.push(this.globalObject);
-            return this.globalObject;
-        },
-        pause: function() { // TODO: These require plugin, test
-            this.boundObjects.each(function(object) {
-                object.mousetrap.pause();
-            });
-        },
-        unpause: function() {
-            this.boundObjects.each(function(object) {
-                object.mousetrap.unpause();
-            });
-        },
-        displayShortcuts: function(shortcuts, options) {
-            options = _.defaults({}, options, {
-                objectGrouping: "default", // Display one list, or group under headers by object type? "true" "false", or "default"
-                displayMultiple: true, // Display multiple shortcuts or just the canonical one?
-                allowRebind: false, // Insert javascript to allow rebinding shortcuts
-                highlightRepeats: true, // Warn about shortcuts which are bound to multiple actions (if user can remap shortcuts)
-            });
-            if (!shortcuts) shortcuts = this.shortcuts;
-            if (options.objectGrouping === "default") {
-                var multipleObjectTypes = _.chain(shortcuts).pluck('object').unique().count().value() > 1;
-                options.objectGrouping = multipleObjectTypes;
-            }
-            // TODO: Return HTML
-            return "";
-        },
-        _displayKeybinding: function(keybinding, options) {
-            options = _.defaults({}, options, { displayMultiple: true });
-            // TODO: Return HTML
-            return "";
-        },
-    };
-    return Shortcut;
-})(document, _);
-
 /**
  * @depend ../views/todo.js
  * @depend ../models/flowyDoc.js
  * @depend ../models/todo.js
- * @depend ../library/shortcut.js
+ * @depend ../library/viewShortcuts.js
  */
 
 var testTodos = [
@@ -610,6 +691,7 @@ var todos = new FlowyDocModel({
 var appDefaults = { list: todos };
 var AppView = Backbone.View.extend({
     el: $("#todo-app"),
+    shortcutObject: "global",
     initialize: function(options) {
         options = _.defaults({}, options, appDefaults);
         this.list = options.list || new FlowyDocModel();
index 7ca8adaf6dd49a79144f74dc2c496de310af8d3b..86ed1df617c89bea26a7ce880f5345d8693d28f0 100644 (file)
@@ -59,8 +59,255 @@ function setRangeAtMarker(markerElement, options) {
   }
 }
 
+// TODO: More documentation
+// TODO: Removing objects/shortcuts/rebinding won't work until this bug is fixed in mousetrap: https://github.com/ccampbell/mousetrap/issues/267
+
+// firstShortcut = Shortcut.registerShortcut("Control + k", function() { console.log("Shortcut pressed."); });
+// firstShortcut.rebind("Control + m");
+// Shortcut.registerShortcut({keybinding: "L", objectType: "textbox", action: function() { console.log("L pressed while inside the textbox."); }});
+// o = Shortcut.addObject(aTextbox, "textbox"); // Can come before or after keyboard binding
+// o.remove() // Once we lose textbox focus, stop keeping track of it, for example
+
+var Shortcut = (function(document, _) {
+    _.mixin({
+        guid : function(){
+            return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
+                var r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8);
+                return v.toString(16);
+            });
+        }
+    });
+
+    var globalMousetrap = new Mousetrap();
+    var globalObject = {element: document, type: "global", isGlobal: true, mousetrap: globalMousetrap};
+    var Shortcut = {
+        _globalObject: globalObject, // The object passed to callbacks when a global keybinding is invoked.
+        globalMousetrap: globalMousetrap,
+        boundObjects: [globalObject],
+        shortcuts: {},
+        registerShortcut: function(shortcut) {
+            if (arguments.length > 1) {
+                shortcut = {
+                    keybinding: arguments[0],
+                    action: arguments[1],
+                    object: arguments[2],
+                };
+            }
+            var manager = this;
+            if (!shortcut.ownedByUs) shortcut = _.clone(shortcut);
+            _.defaults(shortcut, {
+                register: function() { manager.registerShortcut(this); },
+                unregister: function() { manager.unregisterShortcut(this); },
+                bind: function(object) { manager.bindShortcut(this, object); },
+                unbind: function(object) { manager.unbindShortcut(this, object); },
+                rebind: function(keybinding) { manager.rebindShortcut(this, keybinding); },
+                object: "global",
+
+                _keybinding: shortcut.keybinding || "",
+                action: function() {},
+                boundObjects: [],
+                ownedByUs: true,
+                id: _.guid(),
+            });
+            Object.defineProperty(shortcut, "keybinding", {
+                set: function (newKeybinding) { manager.rebindShortcut(shortcut, newKeybinding); },
+                get: function () { return shortcut._keybinding; },
+            });
+            if (this.shortcuts[shortcut.id]) return this.shortcuts[shortcut.id];
+            this.shortcuts[shortcut.id] = shortcut;
+            _.chain(this.boundObjects).where({'type':shortcut.object}).each(_.partial(this.bindShortcut, shortcut, _), this).value();
+            return shortcut;
+        },
+        unregisterShortcut: function(shortcut) {
+            _.each(_.clone(shortcut.boundObjects), _.partial(this.unbindShortcut, shortcut, _), this);
+            delete this.shortcuts[shortcut.id];
+        },
+        bindShortcut: function(shortcut, object) { // Do not call directly
+            if (!_.contains(shortcut.boundObjects, object)) {
+                object.mousetrap.bind(shortcut.keybinding, _.bind(this.shortcutPressed, this, shortcut, object));
+                shortcut.boundObjects.push(object);
+            }
+        },
+        unbindShortcut: function(shortcut, object) { // Do not call directly with an object
+            if (!object){
+                _.each(shortcut.boundObjects, _.partial(this.unbindShortcut, shortcut, _), this);
+            }
+            object.mousetrap.unbind(shortcut.keybinding);
+            shortcut.boundObjects = _.without(shortcut.boundObjects, object);
+        },
+        rebindShortcut: function(shortcut, keybinding) {
+            if (keybinding === shortcut.keybinding) return;
+            var oldKeybinding = shortcut.keybinding;
+            var objects = shortcut.boundObjects;
+            shortcut.unbind();
+            shorcut._keybinding = keybinding;
+            _.each(objects, shortcut.bind, shortcut);
+            if (this.onRebindShortcut) this.onRebindShortcut(shortcut, keybinding, oldKeybinding);
+        },
+        userRebindShortcut: function(shortcut, options) {
+            // Cancels on focus loss or if the user presses the escape key
+            options = _.defaults({}, options, {
+                element: undefined, // If the user clicks outside this element, cancel
+                onCancel: function() {},
+            });
+            var manager = this;
+            var cancelled = false;
+            var cancel = function() {
+                if (!cancelled) {
+                    cancelled = true;
+                    if (options.onCancel) options.onCancel();
+                }
+            };
+            if (element) element.addEventListener("blur", cancel);
+            Mousetrap.record(function (keybinding) { // TODO: requires plugin
+                if (_.equal(keybinding, ["ESCAPE"])) { // TODO: Test
+                    cancel();
+                } else {
+                    manager.rebindShortcut(shortcut, keybinding);
+                    if (element) element.removeEventListener("blur", cancel);
+                }
+            });
+        },
+        shortcutPressed: function(shortcut, object) {
+            if (shortcut.action) shortcut.action(object.element, shortcut, object.type);
+        },
+        onRebindShortcut: function(shortcut, keybinding, oldKeybinding) {
+            // NOTE: You may want to hook into this if you want to save/load user preferences
+            // shortcut.id is probably what you want and not the whole shortcut
+        },
+        addObject: function(element, type) {
+            var manager = this;
+            object = {
+                element:element,
+                type:type,
+                mousetrap: Mousetrap(element),
+                remove: function() { manager.removeObject(this); },
+                add: function() { manager.addObject(this.element, this.type); },
+            };
+            _.chain(this.shortcuts).where({'object': type}).each(_.partial(this.bindShortcut, _, object), this).value();
+            if (!_.contains(this.boundObjects)) this.boundObjects.push(object);
+            return object;
+        },
+        removeObject: function(object) {
+            _.each(this.shortcuts, _.partial(this.unbindShortcut, _, object), this);
+            this.boundObjects = _.without(this.boundObjects, object);
+        },
+        get globalObject() {
+            return this._globalObject;
+        },
+        set globalObject(newElement) {
+            this.removeObject(this.globalObject, "global");
+            this._globalObject = {
+                element: newElement,
+                type: "global",
+                isGlobal: true,
+                mousetrap: this.globalMousetrap,
+            };
+            _.chain(this.shortcuts).where({'object': "global"}).each(_.partial(this.bindShortcut, _, this.globalObject), this).value();
+            if (!_.contains(this.boundObjects)) this.boundObjects.push(this.globalObject);
+            return this.globalObject;
+        },
+        pause: function() { // TODO: These require plugin, test
+            this.boundObjects.each(function(object) {
+                object.mousetrap.pause();
+            });
+        },
+        unpause: function() {
+            this.boundObjects.each(function(object) {
+                object.mousetrap.unpause();
+            });
+        },
+        displayShortcuts: function(shortcuts, options) {
+            options = _.defaults({}, options, {
+                objectGrouping: "default", // Display one list, or group under headers by object type? "true" "false", or "default"
+                displayMultiple: true, // Display multiple shortcuts or just the canonical one?
+                allowRebind: false, // Insert javascript to allow rebinding shortcuts
+                highlightRepeats: true, // Warn about shortcuts which are bound to multiple actions (if user can remap shortcuts)
+            });
+            if (!shortcuts) shortcuts = this.shortcuts;
+            if (options.objectGrouping === "default") {
+                var multipleObjectTypes = _.chain(shortcuts).pluck('object').unique().count().value() > 1;
+                options.objectGrouping = multipleObjectTypes;
+            }
+            // TODO: Return HTML
+            return "";
+        },
+        _displayKeybinding: function(keybinding, options) {
+            options = _.defaults({}, options, { displayMultiple: true });
+            // TODO: Return HTML
+            return "";
+        },
+    };
+    return Shortcut;
+})(document, _);
+
+/**
+ * @depend ../library/shortcut.js
+ */
+
+
+// Integrates "Shortcuts" library with backbone views, to allow events in the form "Shortcut(id, description, default keycombo)": action
+// Shortcut selectors are not supported
+(function (View, Shortcut) {
+    var delegateEvents = View.delegateEvents;
+    var undelegateEvents = View.undelegateEvents;
+    var ShortcutRegex = /^Shortcut\("([^")]*)", ?"([^")]*)", ?"([^")]*)"\) (.*)$/;
+    function delegate(id, description, defaultKeybinding, objectType, callback){
+        var shortcut = Shortcut.registerShortcut({
+            id: id,
+            description: description,
+            keybinding: defaultKeybinding, // TODO: Add support for rebinding
+            object: objectType,
+            action: callback
+        });
+    }
+    function delegateMousetrapEvents(events, objectType) {
+        for (var key in events) {
+            var method = events[key];
+            if (!_.isFunction(method)) method = this[events[key]];
+            if (!method) continue;
+            var match = key.match(ShortcutRegex);
+            delegate.call(this, match[1], match[2], match[3], objectType, _.bind(method, this));
+        }
+        this.shortcutObjects = this.shortcutObjects || [];
+        if (objectType === "global") {
+            Shortcut.globalObject = this.$el[0];
+            this._shortcutObject = Shortcut.globalObject;
+        } else {
+            this._shortcutObject = Shortcut.addObject(this.$el[0], objectType);
+        }
+    }
+    View.delegateEvents = function(events) {
+        if (!(events || (events = _.result(this, 'events')))) return this;
+        var mousetrapEvents = {};
+        var nonMousetrapEvents = {};
+        for (var key in events) {
+            if (key.match(ShortcutRegex)) {
+                mousetrapEvents[key] = events[key];
+            } else {
+                nonMousetrapEvents[key] = events[key];
+            }
+        }
+        delegateEvents.call(this, nonMousetrapEvents);
+        if (_.size(mousetrapEvents) > 0) {
+            var type = this.shortcutObject || this.className;
+            if (!type) {
+                throw "Mousetrap events need to specify a type on the view object via .className or .shortcutObject. 'global' will catch all keystrokes rather than binding to the selector";
+            }
+            delegateMousetrapEvents.call(this, mousetrapEvents, type);
+        }
+        return this;
+    };
+    View.undelegateEvents = function() {
+        if (this._shortcutObject) Shortcut.removeObject(this._shortcutObject);
+        // Also undelegate mousetrap events
+        undelegateEvents.apply(this, arguments);
+    };
+})(Backbone.View.prototype, Shortcut);
+
 /**
  * @depend ../library/cursorToEnd.js
+ * @depend ../library/viewShortcuts.js
  */
 
 
@@ -68,21 +315,18 @@ var TodoView = Backbone.View.extend({
   tagName: 'div',
   className: 'todo',
   events: {
-    //"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": "keydown",
+    'Shortcut("toggleComplete", "Mark an item as complete or not", "ctrl+enter") > .text': "toggleComplete",
+    'Shortcut("backspace", "Combine an item with the previous item", "backspace") > .text': "backspace",
+    'Shortcut("delete", "Combine an item with the next item", "del") > .text': "delete",
   },
   initialize: function() {
     this.childViewPositions = [];
     this.listenTo(this.model, "change", this.render);
     this.listenTo(this.model, 'destroy', this.remove);
   },
-  toggleComplete: function(e) {
-    this.model.toggleComplete();
-    e.stopPropagation();
-    return this;
-  },
   startEditingText: function(options) {
     options = options || {};
     if (options.atEnd) {
@@ -109,16 +353,7 @@ var TodoView = Backbone.View.extend({
   decodeText: function(encodedText) {
     return $("<div/>").html(encodedText).text();
   },
-  keydown: function(e) {
-    if (e.keyCode == 46 /* backspace */ || e.keyCode == 8 /* delete */) {
-        return this.backspace(e);
-    } else if (e.keyCode == 17) {
-        return this.control(e);
-    } else {
-        console.log(e.keyCode + " key pressed");
-    }
-  },
-  control: function(e) {
+  toggleComplete: function(e) {
     this.stopEditingText();
     this.model.toggleComplete();
     var next = this.model.nextNode(this.model.collection, {"childrenAllowed":false});
@@ -136,7 +371,7 @@ var TodoView = Backbone.View.extend({
         e.preventDefault();
         this.model.remove(this.model.collection);
         previousNode.getView().startEditingText({"atEnd":true});
-    } else if (this.isFocusAtBeginning() && e.keyCode !== 46 /* backspace only */) {
+    } else if (this.isFocusAtBeginning()) {
         e.preventDefault();
         var text = this.model.get("text");
         this.model.remove(this.model.collection);
@@ -145,6 +380,20 @@ var TodoView = Backbone.View.extend({
         previousNode.getView().startEditingText({"atMarker": ".focus"});
     }
   },
+  "delete": function(e) {
+    if (this.model.hasChildren()) {
+        return;
+    }
+    var previousNode = this.model.previousNode(this.model.collection);
+    if (!previousNode) {
+        return;
+    }
+    if (this.model.get("text") === "") {
+        e.preventDefault();
+        this.model.remove(this.model.collection);
+        previousNode.getView().startEditingText({"atEnd":true});
+    }
+  },
   textChange: function(e) {
     var collection = this.model.collection;
     var lines = $(e.target).html().split(/<br\\?>/);
@@ -192,7 +441,7 @@ var TodoView = Backbone.View.extend({
     }
     return this;
   },
-  template: "<div class=\"text\" contenteditable=\"plaintext-only\">{{{text}}}</div><div class=\"bullets\"></div>",
+  template: "<div class=\"text mousetrap\" contenteditable=\"plaintext-only\">{{{text}}}</div><div class=\"bullets\"></div>",
   addChild: function(el, position) {
      if(typeof position === 'undefined' || position === -1) {
         console.log("TodoView:addChild called without a position");
@@ -259,12 +508,14 @@ var TodoModel = Backbone.Model.extend({
     },
     getPreviousSibling: function(collection) {
         var parent = this.getParent(collection);
+        if (!parent) return undefined;
         var index = parent.findChild(this.id);
         if (index < 0 || index === 0) return undefined;
         return parent.getChild(collection, index - 1);
     },
     getNextSibling: function(collection) {
         var parent = this.getParent(collection);
+        if (!parent) return undefined;
         var index = parent.findChild(this.id);
         var numChildren = parent.getChildrenCount();
         if (index < 0 || index === numChildren - 1) return undefined;
@@ -376,181 +627,11 @@ var FlowyDocModel = Backbone.Collection.extend({
     comparator: 'id',
 });
 
-// TODO: More documentation
-// TODO: Removing objects/shortcuts/rebinding won't work until this bug is fixed in mousetrap: https://github.com/ccampbell/mousetrap/issues/267
-
-// firstShortcut = Shortcut.registerShortcut("Control + k", function() { console.log("Shortcut pressed."); });
-// firstShortcut.rebind("Control + m");
-// Shortcut.registerShortcut({keybinding: "L", objectType: "textbox", action: function() { console.log("L pressed while inside the textbox."); }});
-// o = Shortcut.addObject(aTextbox, "textbox"); // Can come before or after keyboard binding
-// o.remove() // Once we lose textbox focus, stop keeping track of it, for example
-
-var Shortcut = (function(document, _) {
-    var globalMousetrap = new Mousetrap();
-    var globalObject = {element: document, type: "global", isGlobal: true, mousetrap: globalMousetrap};
-    var Shortcut = {
-        _globalObject: globalObject, // The object passed to callbacks when a global keybinding is invoked.
-        globalMousetrap: globalMousetrap,
-        boundObjects: [globalObject],
-        shortcuts: [],
-        registerShortcut: function(shortcut) {
-            if (arguments.length > 1) {
-                shortcut = {
-                    keybinding: arguments[0],
-                    action: arguments[1],
-                    object: arguments[2],
-                };
-            }
-            var manager = this;
-            if (!shortcut.ownedByUs) shortcut = _.clone(shortcut);
-            _.defaults(shortcut, {
-                register: function() { manager.registerShortcut(this); },
-                unregister: function() { manager.unregisterShortcut(this); },
-                bind: function(object) { manager.bindShortcut(this, object); },
-                unbind: function(object) { manager.unbindShortcut(this, object); },
-                rebind: function(keybinding) { manager.rebindShortcut(this, keybinding); },
-                object: "global",
-
-                _keybinding: shortcut.keybinding || "",
-                action: function() {},
-                boundObjects: [],
-                ownedByUs: true
-            });
-            Object.defineProperty(shortcut, "keybinding", {
-                set: function (newKeybinding) { manager.rebindShortcut(shortcut, newKeybinding); },
-                get: function () { return shortcut._keybinding; },
-            });
-            this.shortcuts.push(shortcut);
-            _.chain(this.boundObjects).where({'type':shortcut.object}).each(_.partial(this.bindShortcut, shortcut, _), this).value();
-            return shortcut;
-        },
-        unregisterShortcut: function(shortcut) {
-            _.each(_.clone(shortcut.boundObjects), _.partial(this.unbindShortcut, shortcut, _), this);
-        },
-        bindShortcut: function(shortcut, object) { // Do not call directly
-            if (!_.contains(shortcut.boundObjects, object)) {
-                object.mousetrap.bind(shortcut.keybinding, _.bind(this.shortcutPressed, this, shortcut, object));
-                shortcut.boundObjects.push(object);
-            }
-        },
-        unbindShortcut: function(shortcut, object) { // Do not call directly with an object
-            if (!object){
-                _.each(shortcut.boundObjects, _.partial(this.unbindShortcut, shortcut, _), this);
-            }
-            object.mousetrap.unbind(shortcut.keybinding);
-            shortcut.boundObjects = _.without(shortcut.boundObjects, object);
-        },
-        rebindShortcut: function(shortcut, keybinding) {
-            if (keybinding === shortcut.keybinding) return;
-            var oldKeybinding = shortcut.keybinding;
-            var objects = shortcut.boundObjects;
-            shortcut.unbind();
-            shorcut._keybinding = keybinding;
-            _.each(objects, shortcut.bind, shortcut);
-            if (this.onRebindShortcut) this.onRebindShortcut(shortcut, keybinding, oldKeybinding);
-        },
-        userRebindShortcut: function(shortcut, options) {
-            // Cancels on focus loss or if the user presses the escape key
-            options = _.defaults({}, options, {
-                element: undefined, // If the user clicks outside this element, cancel
-                onCancel: function() {},
-            });
-            var manager = this;
-            var cancelled = false;
-            var cancel = function() {
-                if (!cancelled) {
-                    cancelled = true;
-                    if (options.onCancel) options.onCancel();
-                }
-            };
-            if (element) element.addEventListener("blur", cancel);
-            Mousetrap.record(function (keybinding) { // TODO: requires plugin
-                if (_.equal(keybinding, ["ESCAPE"])) { // TODO: Test
-                    cancel();
-                } else {
-                    manager.rebindShortcut(shortcut, keybinding);
-                    if (element) element.removeEventListener("blur", cancel);
-                }
-            });
-        },
-        shortcutPressed: function(shortcut, object) {
-            if (shortcut.action) shortcut.action(object.element, shortcut, object.type);
-        },
-        onRebindShortcut: function(shortcut, keybinding, oldKeybinding) {
-            // NOTE: You may want to hook into this if you want to save/load user preferences
-            // shortcut.id is probably what you want and not the whole shortcut
-        },
-        addObject: function(element, type) {
-            var manager = this;
-            object = {
-                element:element,
-                type:type,
-                mousetrap: new Mousetrap(element),
-                remove: function() { manager.removeObject(this); },
-                add: function() { manager.addObject(this.element, this.type); },
-            };
-            _.chain(this.shortcuts).where({'object': type}).each(_.partial(this.bindShortcut, _, object), this).value();
-            if (!_.contains(this.boundObjects)) this.boundObjects.push(object);
-            return object;
-        },
-        removeObject: function(object) {
-            _.each(this.shortcuts, _.partial(this.unbindShortcut, _, object), this);
-            this.boundObjects = _.without(this.boundObjects, object);
-        },
-        get globalObject() {
-            return this._globalObject;
-        },
-        set globalObject(newElement) {
-            this.removeObject(this.globalObject, "global");
-            this._globalObject = {
-                element: newElement,
-                type: "global",
-                isGlobal: true,
-                mousetrap: this.globalMousetrap,
-            };
-            _.chain(this.shortcuts).where({'object': "global"}).each(_.partial(this.bindShortcut, _, this.globalObject), this).value();
-            if (!_.contains(this.boundObjects)) this.boundObjects.push(this.globalObject);
-            return this.globalObject;
-        },
-        pause: function() { // TODO: These require plugin, test
-            this.boundObjects.each(function(object) {
-                object.mousetrap.pause();
-            });
-        },
-        unpause: function() {
-            this.boundObjects.each(function(object) {
-                object.mousetrap.unpause();
-            });
-        },
-        displayShortcuts: function(shortcuts, options) {
-            options = _.defaults({}, options, {
-                objectGrouping: "default", // Display one list, or group under headers by object type? "true" "false", or "default"
-                displayMultiple: true, // Display multiple shortcuts or just the canonical one?
-                allowRebind: false, // Insert javascript to allow rebinding shortcuts
-                highlightRepeats: true, // Warn about shortcuts which are bound to multiple actions (if user can remap shortcuts)
-            });
-            if (!shortcuts) shortcuts = this.shortcuts;
-            if (options.objectGrouping === "default") {
-                var multipleObjectTypes = _.chain(shortcuts).pluck('object').unique().count().value() > 1;
-                options.objectGrouping = multipleObjectTypes;
-            }
-            // TODO: Return HTML
-            return "";
-        },
-        _displayKeybinding: function(keybinding, options) {
-            options = _.defaults({}, options, { displayMultiple: true });
-            // TODO: Return HTML
-            return "";
-        },
-    };
-    return Shortcut;
-})(document, _);
-
 /**
  * @depend ../views/todo.js
  * @depend ../models/flowyDoc.js
  * @depend ../models/todo.js
- * @depend ../library/shortcut.js
+ * @depend ../library/viewShortcuts.js
  */
 
 var testTodos = [
@@ -609,6 +690,7 @@ var todos = new FlowyDocModel({
 var appDefaults = { list: todos };
 var AppView = Backbone.View.extend({
     el: $("#todo-app"),
+    shortcutObject: "global",
     initialize: function(options) {
         options = _.defaults({}, options, appDefaults);
         this.list = options.list || new FlowyDocModel();
diff --git a/src/library/viewShortcuts.js b/src/library/viewShortcuts.js
new file mode 100644 (file)
index 0000000..e4f90ed
--- /dev/null
@@ -0,0 +1,63 @@
+/**
+ * @depend ../library/shortcut.js
+ */
+
+
+// Integrates "Shortcuts" library with backbone views, to allow events in the form "Shortcut(id, description, default keycombo)": action
+// Shortcut selectors are not supported
+(function (View, Shortcut) {
+    var delegateEvents = View.delegateEvents;
+    var undelegateEvents = View.undelegateEvents;
+    var ShortcutRegex = /^Shortcut\("([^")]*)", ?"([^")]*)", ?"([^")]*)"\) (.*)$/;
+    function delegate(id, description, defaultKeybinding, objectType, callback){
+        var shortcut = Shortcut.registerShortcut({
+            id: id,
+            description: description,
+            keybinding: defaultKeybinding, // TODO: Add support for rebinding
+            object: objectType,
+            action: callback
+        });
+    }
+    function delegateMousetrapEvents(events, objectType) {
+        for (var key in events) {
+            var method = events[key];
+            if (!_.isFunction(method)) method = this[events[key]];
+            if (!method) continue;
+            var match = key.match(ShortcutRegex);
+            delegate.call(this, match[1], match[2], match[3], objectType, _.bind(method, this));
+        }
+        this.shortcutObjects = this.shortcutObjects || [];
+        if (objectType === "global") {
+            Shortcut.globalObject = this.$el[0];
+            this._shortcutObject = Shortcut.globalObject;
+        } else {
+            this._shortcutObject = Shortcut.addObject(this.$el[0], objectType);
+        }
+    }
+    View.delegateEvents = function(events) {
+        if (!(events || (events = _.result(this, 'events')))) return this;
+        var mousetrapEvents = {};
+        var nonMousetrapEvents = {};
+        for (var key in events) {
+            if (key.match(ShortcutRegex)) {
+                mousetrapEvents[key] = events[key];
+            } else {
+                nonMousetrapEvents[key] = events[key];
+            }
+        }
+        delegateEvents.call(this, nonMousetrapEvents);
+        if (_.size(mousetrapEvents) > 0) {
+            var type = this.shortcutObject || this.className;
+            if (!type) {
+                throw "Mousetrap events need to specify a type on the view object via .className or .shortcutObject. 'global' will catch all keystrokes rather than binding to the selector";
+            }
+            delegateMousetrapEvents.call(this, mousetrapEvents, type);
+        }
+        return this;
+    };
+    View.undelegateEvents = function() {
+        if (this._shortcutObject) Shortcut.removeObject(this._shortcutObject);
+        // Also undelegate mousetrap events
+        undelegateEvents.apply(this, arguments);
+    };
+})(Backbone.View.prototype, Shortcut);
index 8c031099b041d41a77e28366a4c8968d1c3517e5..08b0410bd957d0963d108f714e29eea9537032f4 100644 (file)
@@ -36,12 +36,14 @@ var TodoModel = Backbone.Model.extend({
     },
     getPreviousSibling: function(collection) {
         var parent = this.getParent(collection);
+        if (!parent) return undefined;
         var index = parent.findChild(this.id);
         if (index < 0 || index === 0) return undefined;
         return parent.getChild(collection, index - 1);
     },
     getNextSibling: function(collection) {
         var parent = this.getParent(collection);
+        if (!parent) return undefined;
         var index = parent.findChild(this.id);
         var numChildren = parent.getChildrenCount();
         if (index < 0 || index === numChildren - 1) return undefined;
index fba645158f21da9f4d3a43a3d181441d94fb6849..1bb884a2b24de156cf0c544b85f3249cb6fd0530 100644 (file)
@@ -2,7 +2,7 @@
  * @depend ../views/todo.js
  * @depend ../models/flowyDoc.js
  * @depend ../models/todo.js
- * @depend ../library/shortcut.js
+ * @depend ../library/viewShortcuts.js
  */
 
 var testTodos = [
@@ -61,6 +61,7 @@ var todos = new FlowyDocModel({
 var appDefaults = { list: todos };
 var AppView = Backbone.View.extend({
     el: $("#todo-app"),
+    shortcutObject: "global",
     initialize: function(options) {
         options = _.defaults({}, options, appDefaults);
         this.list = options.list || new FlowyDocModel();
index 81cbb5c02e83fef944b1da9cec1d2d9cea2f0f29..dc9451f9a39068254f9a4e989d8c53a6df242db3 100644 (file)
@@ -1,5 +1,6 @@
 /**
  * @depend ../library/cursorToEnd.js
+ * @depend ../library/viewShortcuts.js
  */
 
 
@@ -7,21 +8,18 @@ var TodoView = Backbone.View.extend({
   tagName: 'div',
   className: 'todo',
   events: {
-    //"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": "keydown",
+    'Shortcut("toggleComplete", "Mark an item as complete or not", "ctrl+enter") > .text': "toggleComplete",
+    'Shortcut("backspace", "Combine an item with the previous item", "backspace") > .text': "backspace",
+    'Shortcut("delete", "Combine an item with the next item", "del") > .text': "delete",
   },
   initialize: function() {
     this.childViewPositions = [];
     this.listenTo(this.model, "change", this.render);
     this.listenTo(this.model, 'destroy', this.remove);
   },
-  toggleComplete: function(e) {
-    this.model.toggleComplete();
-    e.stopPropagation();
-    return this;
-  },
   startEditingText: function(options) {
     options = options || {};
     if (options.atEnd) {
@@ -48,16 +46,7 @@ var TodoView = Backbone.View.extend({
   decodeText: function(encodedText) {
     return $("<div/>").html(encodedText).text();
   },
-  keydown: function(e) {
-    if (e.keyCode == 46 /* backspace */ || e.keyCode == 8 /* delete */) {
-        return this.backspace(e);
-    } else if (e.keyCode == 17) {
-        return this.control(e);
-    } else {
-        console.log(e.keyCode + " key pressed");
-    }
-  },
-  control: function(e) {
+  toggleComplete: function(e) {
     this.stopEditingText();
     this.model.toggleComplete();
     var next = this.model.nextNode(this.model.collection, {"childrenAllowed":false});
@@ -75,7 +64,7 @@ var TodoView = Backbone.View.extend({
         e.preventDefault();
         this.model.remove(this.model.collection);
         previousNode.getView().startEditingText({"atEnd":true});
-    } else if (this.isFocusAtBeginning() && e.keyCode !== 46 /* backspace only */) {
+    } else if (this.isFocusAtBeginning()) {
         e.preventDefault();
         var text = this.model.get("text");
         this.model.remove(this.model.collection);
@@ -84,6 +73,20 @@ var TodoView = Backbone.View.extend({
         previousNode.getView().startEditingText({"atMarker": ".focus"});
     }
   },
+  "delete": function(e) {
+    if (this.model.hasChildren()) {
+        return;
+    }
+    var previousNode = this.model.previousNode(this.model.collection);
+    if (!previousNode) {
+        return;
+    }
+    if (this.model.get("text") === "") {
+        e.preventDefault();
+        this.model.remove(this.model.collection);
+        previousNode.getView().startEditingText({"atEnd":true});
+    }
+  },
   textChange: function(e) {
     var collection = this.model.collection;
     var lines = $(e.target).html().split(/<br\\?>/);
@@ -131,7 +134,7 @@ var TodoView = Backbone.View.extend({
     }
     return this;
   },
-  template: "<div class=\"text\" contenteditable=\"plaintext-only\">{{{text}}}</div><div class=\"bullets\"></div>",
+  template: "<div class=\"text mousetrap\" contenteditable=\"plaintext-only\">{{{text}}}</div><div class=\"bullets\"></div>",
   addChild: function(el, position) {
      if(typeof position === 'undefined' || position === -1) {
         console.log("TodoView:addChild called without a position");