}
}
+// 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
*/
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) {
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});
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);
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\\?>/);
}
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");
},
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;
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 = [
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();
}
}
+// 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
*/
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) {
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});
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);
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\\?>/);
}
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");
},
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;
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 = [
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();
--- /dev/null
+/**
+ * @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);
},
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;
* @depend ../views/todo.js
* @depend ../models/flowyDoc.js
* @depend ../models/todo.js
- * @depend ../library/shortcut.js
+ * @depend ../library/viewShortcuts.js
*/
var testTodos = [
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();
/**
* @depend ../library/cursorToEnd.js
+ * @depend ../library/viewShortcuts.js
*/
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) {
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});
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);
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\\?>/);
}
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");