blob: a53cb5a4dcb39c2b94889ea07f5bbec574973184 [file] [log] [blame]
/*****************************************
* Deals with the browser DOM events from
* interaction with the typist.
****************************************/
Controller.open(function(_) {
_.keystroke = function(key, evt) {
this.cursor.parent.keystroke(key, evt, this);
};
});
Node.open(function(_) {
_.keystroke = function(key, e, ctrlr) {
var cursor = ctrlr.cursor;
switch (key) {
case 'Ctrl-Shift-Backspace':
case 'Ctrl-Backspace':
ctrlr.ctrlDeleteDir(L);
break;
case 'Shift-Backspace':
case 'Backspace':
ctrlr.backspace();
break;
// Tab or Esc -> go one block right if it exists, else escape right.
case 'Esc':
case 'Tab':
ctrlr.escapeDir(R, key, e);
return;
// Shift-Tab -> go one block left if it exists, else escape left.
case 'Shift-Tab':
case 'Shift-Esc':
ctrlr.escapeDir(L, key, e);
return;
// End -> move to the end of the current block.
case 'End':
ctrlr.notify('move').cursor.insAtRightEnd(cursor.parent);
break;
// Ctrl-End -> move all the way to the end of the root block.
case 'Ctrl-End':
ctrlr.notify('move').cursor.insAtRightEnd(ctrlr.root);
break;
// Shift-End -> select to the end of the current block.
case 'Shift-End':
while (cursor[R]) {
ctrlr.selectRight();
}
break;
// Ctrl-Shift-End -> select to the end of the root block.
case 'Ctrl-Shift-End':
while (cursor[R] || cursor.parent !== ctrlr.root) {
ctrlr.selectRight();
}
break;
// Home -> move to the start of the root block or the current block.
case 'Home':
ctrlr.notify('move').cursor.insAtLeftEnd(cursor.parent);
break;
// Ctrl-Home -> move to the start of the current block.
case 'Ctrl-Home':
ctrlr.notify('move').cursor.insAtLeftEnd(ctrlr.root);
break;
// Shift-Home -> select to the start of the current block.
case 'Shift-Home':
while (cursor[L]) {
ctrlr.selectLeft();
}
break;
// Ctrl-Shift-Home -> move to the start of the root block.
case 'Ctrl-Shift-Home':
while (cursor[L] || cursor.parent !== ctrlr.root) {
ctrlr.selectLeft();
}
break;
case 'Left': ctrlr.moveLeft(); break;
case 'Shift-Left': ctrlr.selectLeft(); break;
case 'Ctrl-Left': break;
case 'Right': ctrlr.moveRight(); break;
case 'Shift-Right': ctrlr.selectRight(); break;
case 'Ctrl-Right': break;
case 'Up': ctrlr.moveUp(); break;
case 'Down': ctrlr.moveDown(); break;
case 'Shift-Up':
if (cursor[L]) {
while (cursor[L]) ctrlr.selectLeft();
} else {
ctrlr.selectLeft();
}
case 'Shift-Down':
if (cursor[R]) {
while (cursor[R]) ctrlr.selectRight();
}
else {
ctrlr.selectRight();
}
case 'Ctrl-Up': break;
case 'Ctrl-Down': break;
case 'Ctrl-Shift-Del':
case 'Ctrl-Del':
ctrlr.ctrlDeleteDir(R);
break;
case 'Shift-Del':
case 'Del':
ctrlr.deleteForward();
break;
case 'Meta-A':
case 'Ctrl-A':
ctrlr.notify('move').cursor.insAtRightEnd(ctrlr.root);
while (cursor[L]) ctrlr.selectLeft();
break;
default:
return;
}
e.preventDefault();
ctrlr.scrollHoriz();
};
_.moveOutOf = // called by Controller::escapeDir, moveDir
_.moveTowards = // called by Controller::moveDir
_.deleteOutOf = // called by Controller::deleteDir
_.deleteTowards = // called by Controller::deleteDir
_.unselectInto = // called by Controller::selectDir
_.selectOutOf = // called by Controller::selectDir
_.selectTowards = // called by Controller::selectDir
function() { pray('overridden or never called on this node'); };
});
Controller.open(function(_) {
this.onNotify(function(e) {
if (e === 'move' || e === 'upDown') this.show().clearSelection();
});
_.escapeDir = function(dir, key, e) {
prayDirection(dir);
var cursor = this.cursor;
// only prevent default of Tab if not in the root editable
if (cursor.parent !== this.root) e.preventDefault();
// want to be a noop if in the root editable (in fact, Tab has an unrelated
// default browser action if so)
if (cursor.parent === this.root) return;
cursor.parent.moveOutOf(dir, cursor);
return this.notify('move');
};
optionProcessors.leftRightIntoCmdGoes = function(updown) {
if (updown && updown !== 'up' && updown !== 'down') {
throw '"up" or "down" required for leftRightIntoCmdGoes option, '
+ 'got "'+updown+'"';
}
return updown;
};
_.moveDir = function(dir) {
prayDirection(dir);
var cursor = this.cursor, updown = cursor.options.leftRightIntoCmdGoes;
if (cursor.selection) {
cursor.insDirOf(dir, cursor.selection.ends[dir]);
}
else if (cursor[dir]) cursor[dir].moveTowards(dir, cursor, updown);
else cursor.parent.moveOutOf(dir, cursor, updown);
return this.notify('move');
};
_.moveLeft = function() { return this.moveDir(L); };
_.moveRight = function() { return this.moveDir(R); };
/**
* moveUp and moveDown have almost identical algorithms:
* - first check left and right, if so insAtLeft/RightEnd of them
* - else check the parent's 'upOutOf'/'downOutOf' property:
* + if it's a function, call it with the cursor as the sole argument and
* use the return value as if it were the value of the property
* + if it's a Node, jump up or down into it:
* - if there is a cached Point in the block, insert there
* - else, seekHoriz within the block to the current x-coordinate (to be
* as close to directly above/below the current position as possible)
* + unless it's exactly `true`, stop bubbling
*/
_.moveUp = function() { return moveUpDown(this, 'up'); };
_.moveDown = function() { return moveUpDown(this, 'down'); };
function moveUpDown(self, dir) {
var cursor = self.notify('upDown').cursor;
var dirInto = dir+'Into', dirOutOf = dir+'OutOf';
if (cursor[R][dirInto]) cursor.insAtLeftEnd(cursor[R][dirInto]);
else if (cursor[L][dirInto]) cursor.insAtRightEnd(cursor[L][dirInto]);
else {
cursor.parent.bubble(function(ancestor) {
var prop = ancestor[dirOutOf];
if (prop) {
if (typeof prop === 'function') prop = ancestor[dirOutOf](cursor);
if (prop instanceof Node) cursor.jumpUpDown(ancestor, prop);
if (prop !== true) return false;
}
});
}
return self;
}
this.onNotify(function(e) { if (e !== 'upDown') this.upDownCache = {}; });
this.onNotify(function(e) { if (e === 'edit') this.show().deleteSelection(); });
_.deleteDir = function(dir) {
prayDirection(dir);
var cursor = this.cursor;
var hadSelection = cursor.selection;
this.notify('edit'); // deletes selection if present
if (!hadSelection) {
if (cursor[dir]) cursor[dir].deleteTowards(dir, cursor);
else cursor.parent.deleteOutOf(dir, cursor);
}
if (cursor[L].siblingDeleted) cursor[L].siblingDeleted(cursor.options, R);
if (cursor[R].siblingDeleted) cursor[R].siblingDeleted(cursor.options, L);
cursor.parent.bubble('reflow');
return this;
};
_.ctrlDeleteDir = function(dir) {
prayDirection(dir);
var cursor = this.cursor;
if (!cursor[dir] || cursor.selection) return this.deleteDir(dir);
this.notify('edit');
if (dir === L) {
Fragment(cursor.parent.ends[L], cursor[L]).remove();
} else {
Fragment(cursor[R], cursor.parent.ends[R]).remove();
};
cursor.insAtDirEnd(dir, cursor.parent);
if (cursor[L].siblingDeleted) cursor[L].siblingDeleted(cursor.options, R);
if (cursor[R].siblingDeleted) cursor[R].siblingDeleted(cursor.options, L);
cursor.parent.bubble('reflow');
return this;
};
_.backspace = function() { return this.deleteDir(L); };
_.deleteForward = function() { return this.deleteDir(R); };
this.onNotify(function(e) { if (e !== 'select') this.endSelection(); });
_.selectDir = function(dir) {
var cursor = this.notify('select').cursor, seln = cursor.selection;
prayDirection(dir);
if (!cursor.anticursor) cursor.startSelection();
var node = cursor[dir];
if (node) {
// "if node we're selecting towards is inside selection (hence retracting)
// and is on the *far side* of the selection (hence is only node selected)
// and the anticursor is *inside* that node, not just on the other side"
if (seln && seln.ends[dir] === node && cursor.anticursor[-dir] !== node) {
node.unselectInto(dir, cursor);
}
else node.selectTowards(dir, cursor);
}
else cursor.parent.selectOutOf(dir, cursor);
cursor.clearSelection();
cursor.select() || cursor.show();
};
_.selectLeft = function() { return this.selectDir(L); };
_.selectRight = function() { return this.selectDir(R); };
});