| /***************************************** | |
| * 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[L] || cursor.selection) return this.deleteDir(); | |
| this.notify('edit'); | |
| Fragment(cursor.parent.ends[L], cursor[L]).remove(); | |
| cursor.insAtDirEnd(L, 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); }; | |
| }); |