Chromevox theme now an extension, added demo file, added findSearchBox event.

This commit is contained in:
Peter Xiao 2013-08-12 15:58:09 -07:00
commit 38b42d20d3
4 changed files with 809 additions and 786 deletions

44
demo/chromevox.html Normal file
View file

@ -0,0 +1,44 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>ACE Autocompletion demo</title>
<style type="text/css" media="screen">
body {
overflow: hidden;
}
#editor {
margin: 0;
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
}
</style>
</head>
<body>
<pre id="editor"></pre>
<!-- load ace -->
<script src="../build/src/ace.js"></script>
<!-- load ace language tools -->
<script src="../build/src/ext-chromevox.js"></script>
<script>
// trigger extension
ace.require("ace/ext/chromevox");
var editor = ace.edit("editor");
editor.session.setMode("ace/mode/html");
editor.setTheme("ace/theme/tomorrow");
// enable autocompletion and snippets
editor.setOptions({
enableBasicAutocompletion: true,
enableSnippets: true
});
</script>
<script src="./show_own_source.js"></script>
</body>
</html>

View file

@ -1,9 +1,10 @@
define(function(require, exports, module) { define(function(require, exports, module) {
/* ChromeVox Ace namespace. */
var cvoxAce = {};
/* Typedefs for Closure compiler. */ /* ChromeVox Ace namespace. */
/** var cvoxAce = {};
/* Typedefs for Closure compiler. */
/**
* @typedef {{ * @typedef {{
rate: number, rate: number,
pitch: number, pitch: number,
@ -12,27 +13,27 @@ define(function(require, exports, module) {
punctuationEcho: string punctuationEcho: string
}} }}
*/ */
/* TODO(peterxiao): Export this typedef through cvox.Api. */ /* TODO(peterxiao): Export this typedef through cvox.Api. */
cvoxAce.SpeechProperty; cvoxAce.SpeechProperty;
/** /**
* @typedef {{ * @typedef {{
* row: number, * row: number,
* column: number * column: number
* }} * }}
*/ */
cvoxAce.Cursor; cvoxAce.Cursor;
/** /**
* @typedef {{ * @typedef {{
type: string, type: string,
value: string value: string
}} }}
} }
*/ */
cvoxAce.Token; cvoxAce.Token;
/** /**
* These are errors and information that Ace will display in the gutter. * These are errors and information that Ace will display in the gutter.
* @typedef {{ * @typedef {{
row: number, row: number,
@ -41,91 +42,91 @@ define(function(require, exports, module) {
}} }}
} }
*/ */
cvoxAce.Annotation; cvoxAce.Annotation;
/* Speech Properties. */ /* Speech Properties. */
/** /**
* Speech property for speaking constant tokens. * Speech property for speaking constant tokens.
* @type {cvoxAce.SpeechProperty} * @type {cvoxAce.SpeechProperty}
*/ */
var CONSTANT_PROP = { var CONSTANT_PROP = {
'rate': 0.8, 'rate': 0.8,
'pitch': 0.4, 'pitch': 0.4,
'volume': 0.9 'volume': 0.9
}; };
/** /**
* Default speech property for speaking tokens. * Default speech property for speaking tokens.
* @type {cvoxAce.SpeechProperty} * @type {cvoxAce.SpeechProperty}
*/ */
var DEFAULT_PROP = { var DEFAULT_PROP = {
'rate': 1, 'rate': 1,
'pitch': 0.5, 'pitch': 0.5,
'volume': 0.9 'volume': 0.9
}; };
/** /**
* Speech property for speaking entity tokens. * Speech property for speaking entity tokens.
* @type {cvoxAce.SpeechProperty} * @type {cvoxAce.SpeechProperty}
*/ */
var ENTITY_PROP = { var ENTITY_PROP = {
'rate': 0.8, 'rate': 0.8,
'pitch': 0.8, 'pitch': 0.8,
'volume': 0.9 'volume': 0.9
}; };
/** /**
* Speech property for speaking keywords. * Speech property for speaking keywords.
* @type {cvoxAce.SpeechProperty} * @type {cvoxAce.SpeechProperty}
*/ */
var KEYWORD_PROP = { var KEYWORD_PROP = {
'rate': 0.8, 'rate': 0.8,
'pitch': 0.3, 'pitch': 0.3,
'volume': 0.9 'volume': 0.9
}; };
/** /**
* Speech property for speaking storage tokens. * Speech property for speaking storage tokens.
* @type {cvoxAce.SpeechProperty} * @type {cvoxAce.SpeechProperty}
*/ */
var STORAGE_PROP = { var STORAGE_PROP = {
'rate': 0.8, 'rate': 0.8,
'pitch': 0.7, 'pitch': 0.7,
'volume': 0.9 'volume': 0.9
}; };
/** /**
* Speech property for speaking variable tokens. * Speech property for speaking variable tokens.
* @type {cvoxAce.SpeechProperty} * @type {cvoxAce.SpeechProperty}
*/ */
var VARIABLE_PROP = { var VARIABLE_PROP = {
'rate': 0.8, 'rate': 0.8,
'pitch': 0.8, 'pitch': 0.8,
'volume': 0.9 'volume': 0.9
}; };
/** /**
* Speech property for speaking deleted text. * Speech property for speaking deleted text.
* @type {cvoxAce.SpeechProperty} * @type {cvoxAce.SpeechProperty}
*/ */
var DELETED_PROP = { var DELETED_PROP = {
'punctuationEcho': 'none', 'punctuationEcho': 'none',
'relativePitch': -0.6 'relativePitch': -0.6
}; };
/* Constants for Earcons. */ /* Constants for Earcons. */
var ERROR_EARCON = 'ALERT_NONMODAL'; var ERROR_EARCON = 'ALERT_NONMODAL';
var MODE_SWITCH_EARCON = 'ALERT_MODAL'; var MODE_SWITCH_EARCON = 'ALERT_MODAL';
var NO_MATCH_EARCON = 'INVALID_KEYPRESS'; var NO_MATCH_EARCON = 'INVALID_KEYPRESS';
/* Constants for vim state. */ /* Constants for vim state. */
var INSERT_MODE_STATE = 'insertMode'; var INSERT_MODE_STATE = 'insertMode';
var COMMAND_MODE_STATE = 'start'; var COMMAND_MODE_STATE = 'start';
/** /**
* Context menu commands. * Context menu commands.
*/ */
var Command = { var Command = {
SPEAK_ANNOT: 'annots', SPEAK_ANNOT: 'annots',
SPEAK_ALL_ANNOTS: 'all_annots', SPEAK_ALL_ANNOTS: 'all_annots',
TOGGLE_LOCATION: 'toggle_location', TOGGLE_LOCATION: 'toggle_location',
@ -133,102 +134,104 @@ define(function(require, exports, module) {
SPEAK_ROW_COL: 'row_col', SPEAK_ROW_COL: 'row_col',
TOGGLE_DISPLACEMENT: 'toggle_displacement', TOGGLE_DISPLACEMENT: 'toggle_displacement',
FOCUS_TEXT: 'focus_text' FOCUS_TEXT: 'focus_text'
}; };
/** /**
* Key prefix for each shortcut. * Key prefix for each shortcut.
*/ */
var KEY_PREFIX = 'CONTROL + SHIFT '; var KEY_PREFIX = 'CONTROL + SHIFT ';
/* Globals. */ /* Globals. */
/** cvoxAce.editor = null;
/**
* Last cursor position. * Last cursor position.
* @type {!cvoxAce.Cursor} * @type {cvoxAce.Cursor}
*/ */
var lastCursor = ace.selection.getCursor(); var lastCursor = null;
/** /**
* Table of annotations. * Table of annotations.
* @typedef {!Object.<number, Object<number, cvoxAce.Annotation>>} * @typedef {!Object.<number, Object<number, cvoxAce.Annotation>>}
*/ */
var annotTable = {}; var annotTable = {};
/** /**
* Whether to speak character, word, and then line. This allows blind users * Whether to speak character, word, and then line. This allows blind users
* to know the location of the cursor when they change lines. * to know the location of the cursor when they change lines.
* @typedef {boolean} * @typedef {boolean}
*/ */
var shouldSpeakRowLocation = false; var shouldSpeakRowLocation = false;
/** /**
* Whether to speak displacement. * Whether to speak displacement.
* @typedef {boolean} * @typedef {boolean}
*/ */
var shouldSpeakDisplacement = false; var shouldSpeakDisplacement = false;
/** /**
* Whether text was changed to cause a cursor change event. * Whether text was changed to cause a cursor change event.
* @typedef {boolean} * @typedef {boolean}
*/ */
var changed = false; var changed = false;
/** /**
* Current state vim is in. * Current state vim is in.
*/ */
var vimState = null; var vimState = null;
/** /**
* Mapping from key code to shortcut. * Mapping from key code to shortcut.
*/ */
var keyCodeToShortcutMap = {}; var keyCodeToShortcutMap = {};
/** /**
* Mapping from command to shortcut. * Mapping from command to shortcut.
*/ */
var cmdToShortcutMap = {}; var cmdToShortcutMap = {};
/** /**
* Get shortcut string from keyCode. * Get shortcut string from keyCode.
* @param {number} keyCode Key code of shortcut. * @param {number} keyCode Key code of shortcut.
* @return {string} String representation of shortcut. * @return {string} String representation of shortcut.
*/ */
var getKeyShortcutString = function(keyCode) { var getKeyShortcutString = function(keyCode) {
return KEY_PREFIX + String.fromCharCode(keyCode); return KEY_PREFIX + String.fromCharCode(keyCode);
}; };
/** /**
* Return if in vim mode. * Return if in vim mode.
* @return {boolean} True if in Vim mode. * @return {boolean} True if in Vim mode.
*/ */
var isVimMode = function() { var isVimMode = function() {
return ace.keyBinding.getKeyboardHandler().$id === 'ace/keyboard/vim'; var keyboardHandler = cvoxAce.editor.keyBinding.getKeyboardHandler();
}; return keyboardHandler.$id === 'ace/keyboard/vim';
};
/** /**
* Gets the current token. * Gets the current token.
* @param {!cvoxAce.Cursor} cursor Current position of the cursor. * @param {!cvoxAce.Cursor} cursor Current position of the cursor.
* @return {!cvoxAce.Token} Token at the current position. * @return {!cvoxAce.Token} Token at the current position.
*/ */
var getCurrentToken = function(cursor) { var getCurrentToken = function(cursor) {
return ace.getSession().getTokenAt(cursor.row, cursor.column + 1); return cvoxAce.editor.getSession().getTokenAt(cursor.row, cursor.column + 1);
}; };
/** /**
* Gets the current line the cursor is under. * Gets the current line the cursor is under.
* @param {!cvoxAce.Cursor} cursor Current cursor position. * @param {!cvoxAce.Cursor} cursor Current cursor position.
*/ */
var getCurrentLine = function(cursor) { var getCurrentLine = function(cursor) {
return ace.getSession().getLine(cursor.row); return cvoxAce.editor.getSession().getLine(cursor.row);
}; };
/** /**
* Event handler for row changes. When the user changes rows we want to speak * Event handler for row changes. When the user changes rows we want to speak
* the line so the user can work on this line. If shouldSpeakRowLocation is on * the line so the user can work on this line. If shouldSpeakRowLocation is on
* then we speak the character, then the row, then the line so the user knows * then we speak the character, then the row, then the line so the user knows
* where the cursor is. * where the cursor is.
* @param {!cvoxAce.Cursor} currCursor Current cursor position. * @param {!cvoxAce.Cursor} currCursor Current cursor position.
*/ */
var onRowChange = function(currCursor) { var onRowChange = function(currCursor) {
/* Notify that this line has an annotation. */ /* Notify that this line has an annotation. */
if (annotTable[currCursor.row]) { if (annotTable[currCursor.row]) {
cvox.Api.playEarcon(ERROR_EARCON); cvox.Api.playEarcon(ERROR_EARCON);
@ -241,15 +244,15 @@ define(function(require, exports, module) {
} else { } else {
speakLine(currCursor.row, 0); speakLine(currCursor.row, 0);
} }
}; };
/** /**
* Returns whether the cursor is at the beginning of a word. A word is * Returns whether the cursor is at the beginning of a word. A word is
* a grouping of alphanumeric characters including underscores. * a grouping of alphanumeric characters including underscores.
* @param {!cvoxAce.Cursor} cursor Current cursor position. * @param {!cvoxAce.Cursor} cursor Current cursor position.
* @return {boolean} Whether there is word. * @return {boolean} Whether there is word.
*/ */
var isWord = function(cursor) { var isWord = function(cursor) {
var line = getCurrentLine(cursor); var line = getCurrentLine(cursor);
var lineSuffix = line.substr(cursor.column - 1); var lineSuffix = line.substr(cursor.column - 1);
if (cursor.column === 0) { if (cursor.column === 0) {
@ -259,26 +262,26 @@ define(function(require, exports, module) {
var firstWordRegExp = /^\W(\w+)/; var firstWordRegExp = /^\W(\w+)/;
var words = firstWordRegExp.exec(lineSuffix); var words = firstWordRegExp.exec(lineSuffix);
return words !== null; return words !== null;
}; };
/** /**
* A mapping of syntax type to speech properties. * A mapping of syntax type to speech properties.
*/ */
var rules = { var rules = {
'constant': CONSTANT_PROP, 'constant': CONSTANT_PROP,
'entity': ENTITY_PROP, 'entity': ENTITY_PROP,
'keyword': KEYWORD_PROP, 'keyword': KEYWORD_PROP,
'storage': STORAGE_PROP, 'storage': STORAGE_PROP,
'variable': VARIABLE_PROP 'variable': VARIABLE_PROP
}; };
/** /**
* Speak the line with syntax properties. * Speak the line with syntax properties.
* @param {number} row Row to speak. * @param {number} row Row to speak.
* @param {number} queue Queue mode to speak. * @param {number} queue Queue mode to speak.
*/ */
var speakLine = function(row, queue) { var speakLine = function(row, queue) {
var tokens = ace.getSession().getTokens(row); var tokens = cvoxAce.editor.getSession().getTokens(row);
if (tokens.length === 0) { if (tokens.length === 0) {
return; return;
} }
@ -291,33 +294,33 @@ define(function(require, exports, module) {
speakToken_(firstToken, queue); speakToken_(firstToken, queue);
/* Speak rest of tokens. */ /* Speak rest of tokens. */
tokens.forEach(speakTokenQueue); tokens.forEach(speakTokenQueue);
}; };
/** /**
* Speak the token based on the syntax of the token, flushing. * Speak the token based on the syntax of the token, flushing.
* @param {!cvoxAce.Token} token Token to speak. * @param {!cvoxAce.Token} token Token to speak.
* @param {number} queue Queue mode. * @param {number} queue Queue mode.
*/ */
var speakTokenFlush = function(token) { var speakTokenFlush = function(token) {
speakToken_(token, 0); speakToken_(token, 0);
}; };
/** /**
* Speak the token based on the syntax of the token, queueing. * Speak the token based on the syntax of the token, queueing.
* @param {!cvoxAce.Token} token Token to speak. * @param {!cvoxAce.Token} token Token to speak.
* @param {number} queue Queue mode. * @param {number} queue Queue mode.
*/ */
var speakTokenQueue = function(token) { var speakTokenQueue = function(token) {
speakToken_(token, 1); speakToken_(token, 1);
}; };
/** /**
* Speak the token based on the syntax of the token. * Speak the token based on the syntax of the token.
* @private * @private
* @param {!cvoxAce.Token} token Token to speak. * @param {!cvoxAce.Token} token Token to speak.
* @param {number} queue Queue mode. * @param {number} queue Queue mode.
*/ */
var speakToken_ = function(token, queue) { var speakToken_ = function(token, queue) {
/* Types are period delimited. In this case, we only syntax speak the outer /* Types are period delimited. In this case, we only syntax speak the outer
* most type of token. */ * most type of token. */
if (!token || !token.type) { if (!token || !token.type) {
@ -333,25 +336,25 @@ define(function(require, exports, module) {
prop = DEFAULT_PROP; prop = DEFAULT_PROP;
} }
cvox.Api.speak(token.value, queue, prop); cvox.Api.speak(token.value, queue, prop);
}; };
/** /**
* Speaks the character under the cursor. This is queued. * Speaks the character under the cursor. This is queued.
* @param {!cvoxAce.Cursor} cursor Current cursor position. * @param {!cvoxAce.Cursor} cursor Current cursor position.
* @return {string} Character. * @return {string} Character.
*/ */
var speakChar = function(cursor) { var speakChar = function(cursor) {
var line = getCurrentLine(cursor); var line = getCurrentLine(cursor);
cvox.Api.speak(line[cursor.column], 1); cvox.Api.speak(line[cursor.column], 1);
}; };
/** /**
* Speaks the jump from lastCursor to currCursor. This function assumes the * Speaks the jump from lastCursor to currCursor. This function assumes the
* jump takes place on the current line. * jump takes place on the current line.
* @param {!cvoxAce.Cursor} lastCursor Previous cursor position. * @param {!cvoxAce.Cursor} lastCursor Previous cursor position.
* @param {!cvoxAce.Cursor} currCursor Current cursor position. * @param {!cvoxAce.Cursor} currCursor Current cursor position.
*/ */
var speakDisplacement = function(lastCursor, currCursor) { var speakDisplacement = function(lastCursor, currCursor) {
cvox.Api.stop(); cvox.Api.stop();
var line = getCurrentLine(currCursor); var line = getCurrentLine(currCursor);
@ -364,15 +367,15 @@ define(function(require, exports, module) {
/* Speak out loud spaces. */ /* Speak out loud spaces. */
displace = displace.replace(/ /g, ' space '); displace = displace.replace(/ /g, ' space ');
cvox.Api.speak(displace, 1); cvox.Api.speak(displace, 1);
}; };
/** /**
* Speaks the word if the cursor jumped to a new word or to the beginning * Speaks the word if the cursor jumped to a new word or to the beginning
* of the line. Otherwise speak the charactor. * of the line. Otherwise speak the charactor.
* @param {!cvoxAce.Cursor} lastCursor Previous cursor position. * @param {!cvoxAce.Cursor} lastCursor Previous cursor position.
* @param {!cvoxAce.Cursor} currCursor Current cursor position. * @param {!cvoxAce.Cursor} currCursor Current cursor position.
*/ */
var speakCharOrWordOrLine = function(lastCursor, currCursor) { var speakCharOrWordOrLine = function(lastCursor, currCursor) {
/* Say word only if jump. */ /* Say word only if jump. */
if (Math.abs(lastCursor.column - currCursor.column) !== 1) { if (Math.abs(lastCursor.column - currCursor.column) !== 1) {
var currLineLength = getCurrentLine(currCursor).length; var currLineLength = getCurrentLine(currCursor).length;
@ -388,9 +391,9 @@ define(function(require, exports, module) {
} }
} }
speakChar(currCursor); speakChar(currCursor);
}; };
/** /**
* Event handler for column changes. If shouldSpeakDisplacement is on, then * Event handler for column changes. If shouldSpeakDisplacement is on, then
* we just speak displacements in row changes. Otherwise, we either speak * we just speak displacements in row changes. Otherwise, we either speak
* the character for single character movements, the word when jumping to the * the character for single character movements, the word when jumping to the
@ -398,41 +401,41 @@ define(function(require, exports, module) {
* @param {!cvoxAce.Cursor} lastCursor Previous cursor position. * @param {!cvoxAce.Cursor} lastCursor Previous cursor position.
* @param {!cvoxAce.Cursor} currCursor Current cursor position. * @param {!cvoxAce.Cursor} currCursor Current cursor position.
*/ */
var onColumnChange = function(lastCursor, currCursor) { var onColumnChange = function(lastCursor, currCursor) {
if (shouldSpeakDisplacement) { if (shouldSpeakDisplacement) {
speakDisplacement(lastCursor, currCursor); speakDisplacement(lastCursor, currCursor);
} else { } else {
speakCharOrWordOrLine(lastCursor, currCursor); speakCharOrWordOrLine(lastCursor, currCursor);
} }
}; };
/** /**
* Event handler for cursor changes. Classify cursor changes as either row or * Event handler for cursor changes. Classify cursor changes as either row or
* column changes, then delegate accordingly. * column changes, then delegate accordingly.
* @param {!Event} evt The event. * @param {!Event} evt The event.
*/ */
var onCursorChange = function(evt) { var onCursorChange = function(evt) {
/* Do not speak if cursor change was a result of text insertion. We want to /* Do not speak if cursor change was a result of text insertion. We want to
* speak the text that was inserted and not where the cursor lands. */ * speak the text that was inserted and not where the cursor lands. */
if (changed) { if (changed) {
changed = false; changed = false;
return; return;
} }
var currCursor = ace.selection.getCursor(); var currCursor = cvoxAce.editor.selection.getCursor();
if (currCursor.row !== lastCursor.row) { if (currCursor.row !== lastCursor.row) {
onRowChange(currCursor); onRowChange(currCursor);
} else { } else {
onColumnChange(lastCursor, currCursor); onColumnChange(lastCursor, currCursor);
} }
lastCursor = currCursor; lastCursor = currCursor;
}; };
/** /**
* Event handler for source changes. We want auditory feedback for inserting * Event handler for source changes. We want auditory feedback for inserting
* and deleting text. * and deleting text.
* @param {!Event} evt The event. * @param {!Event} evt The event.
*/ */
var onChange = function(evt) { var onChange = function(evt) {
var data = evt.data; var data = evt.data;
switch (data.action) { switch (data.action) {
case 'removeText': case 'removeText':
@ -446,24 +449,24 @@ define(function(require, exports, module) {
changed = true; changed = true;
break; break;
} }
}; };
/** /**
* Returns whether or not the annotation is new. * Returns whether or not the annotation is new.
* @param {!cvoxAce.Annotation} annot Annotation in question. * @param {!cvoxAce.Annotation} annot Annotation in question.
* @return {boolean} Whether annot is new. * @return {boolean} Whether annot is new.
*/ */
var isNewAnnotation = function(annot) { var isNewAnnotation = function(annot) {
var row = annot.row; var row = annot.row;
var col = annot.column; var col = annot.column;
return !annotTable[row] || !annotTable[row][col]; return !annotTable[row] || !annotTable[row][col];
}; };
/** /**
* Populates the annotation table. * Populates the annotation table.
* @param {!Array.<cvoxAce.Annotation>} annotations Array of annotations. * @param {!Array.<cvoxAce.Annotation>} annotations Array of annotations.
*/ */
var populateAnnotations = function(annotations) { var populateAnnotations = function(annotations) {
annotTable = {}; annotTable = {};
for (var i = 0; i < annotations.length; i++) { for (var i = 0; i < annotations.length; i++) {
var annotation = annotations[i]; var annotation = annotations[i];
@ -474,78 +477,78 @@ define(function(require, exports, module) {
} }
annotTable[row][col] = annotation; annotTable[row][col] = annotation;
} }
}; };
/** /**
* Event handler for annotation changes. We want to notify the user when an * Event handler for annotation changes. We want to notify the user when an
* a new annotation appears. * a new annotation appears.
* @param {!Event} evt Event. * @param {!Event} evt Event.
*/ */
var onAnnotationChange = function(evt) { var onAnnotationChange = function(evt) {
var annotations = ace.getSession().getAnnotations(); var annotations = cvoxAce.editor.getSession().getAnnotations();
var newAnnotations = annotations.filter(isNewAnnotation); var newAnnotations = annotations.filter(isNewAnnotation);
if (newAnnotations.length > 0) { if (newAnnotations.length > 0) {
cvox.Api.playEarcon(ERROR_EARCON); cvox.Api.playEarcon(ERROR_EARCON);
} }
populateAnnotations(annotations); populateAnnotations(annotations);
}; };
/** /**
* Speak annotation. * Speak annotation.
* @param {!cvoxAce.Annotation} annot Annotation to speak. * @param {!cvoxAce.Annotation} annot Annotation to speak.
*/ */
var speakAnnot = function(annot) { var speakAnnot = function(annot) {
var annotText = annot.type + ' ' + annot.text + ' on ' + var annotText = annot.type + ' ' + annot.text + ' on ' +
rowColToString(annot.row, annot.column); rowColToString(annot.row, annot.column);
annotText = annotText.replace(';', 'semicolon'); annotText = annotText.replace(';', 'semicolon');
cvox.Api.speak(annotText, 1); cvox.Api.speak(annotText, 1);
}; };
/** /**
* Speak annotations in a row. * Speak annotations in a row.
* @param {number} row Row of annotations to speak. * @param {number} row Row of annotations to speak.
*/ */
var speakAnnotsByRow = function(row) { var speakAnnotsByRow = function(row) {
var annots = annotTable[row]; var annots = annotTable[row];
for (var col in annots) { for (var col in annots) {
speakAnnot(annots[col]); speakAnnot(annots[col]);
} }
}; };
/** /**
* Get a string representation of a row and column. * Get a string representation of a row and column.
* @param {boolean} row Zero indexed row. * @param {boolean} row Zero indexed row.
* @param {boolean} col Zero indexed column. * @param {boolean} col Zero indexed column.
* @return {string} Row and column to be spoken. * @return {string} Row and column to be spoken.
*/ */
var rowColToString = function(row, col) { var rowColToString = function(row, col) {
return 'row ' + (row + 1) + ' column ' + (col + 1); return 'row ' + (row + 1) + ' column ' + (col + 1);
}; };
/** /**
* Speaks the row and column. * Speaks the row and column.
*/ */
var speakCurrRowAndCol = function() { var speakCurrRowAndCol = function() {
cvox.Api.speak(rowColToString(lastCursor.row, lastCursor.column)); cvox.Api.speak(rowColToString(lastCursor.row, lastCursor.column));
}; };
/** /**
* Speaks all annotations. * Speaks all annotations.
*/ */
var speakAllAnnots = function() { var speakAllAnnots = function() {
for (var row in annotTable) { for (var row in annotTable) {
speakAnnotsByRow(row); speakAnnotsByRow(row);
} }
}; };
/** /**
* Speak the vim mode. If no vim mode, this function does nothing. * Speak the vim mode. If no vim mode, this function does nothing.
*/ */
var speakMode = function() { var speakMode = function() {
if (!isVimMode()) { if (!isVimMode()) {
return; return;
} }
switch (ace.keyBinding.$data.state) { switch (cvoxAce.editor.keyBinding.$data.state) {
case INSERT_MODE_STATE: case INSERT_MODE_STATE:
cvox.Api.speak('Insert mode'); cvox.Api.speak('Insert mode');
break; break;
@ -553,12 +556,12 @@ define(function(require, exports, module) {
cvox.Api.speak('Command mode'); cvox.Api.speak('Command mode');
break; break;
} }
}; };
/** /**
* Toggle speak location. * Toggle speak location.
*/ */
var toggleSpeakRowLocation = function() { var toggleSpeakRowLocation = function() {
shouldSpeakRowLocation = !shouldSpeakRowLocation; shouldSpeakRowLocation = !shouldSpeakRowLocation;
/* Auditory feedback of the change. */ /* Auditory feedback of the change. */
if (shouldSpeakRowLocation) { if (shouldSpeakRowLocation) {
@ -566,12 +569,12 @@ define(function(require, exports, module) {
} else { } else {
cvox.Api.speak('Speak location on row change disabled.'); cvox.Api.speak('Speak location on row change disabled.');
} }
}; };
/** /**
* Toggle speak displacement. * Toggle speak displacement.
*/ */
var toggleSpeakDisplacement = function() { var toggleSpeakDisplacement = function() {
speakDisplacement = !speakDisplacement; speakDisplacement = !speakDisplacement;
/* Auditory feedback of the change. */ /* Auditory feedback of the change. */
if (speakDisplacement) { if (speakDisplacement) {
@ -579,29 +582,29 @@ define(function(require, exports, module) {
} else { } else {
cvox.Api.speak('Speak current character or word on column changes.'); cvox.Api.speak('Speak current character or word on column changes.');
} }
}; };
/** /**
* Event handler for key down events. Gets the right shortcut from the map, * Event handler for key down events. Gets the right shortcut from the map,
* and calls the associated function. * and calls the associated function.
* @param {!Event} evt Keyboard event. * @param {!Event} evt Keyboard event.
*/ */
var onKeyDown = function(evt) { var onKeyDown = function(evt) {
if (evt.ctrlKey && evt.shiftKey) { if (evt.ctrlKey && evt.shiftKey) {
var shortcut = keyCodeToShortcutMap[evt.keyCode]; var shortcut = keyCodeToShortcutMap[evt.keyCode];
if (shortcut) { if (shortcut) {
shortcut.func(); shortcut.func();
} }
} }
}; };
/** /**
* Event handler for status change events. Auditory feedback of changing * Event handler for status change events. Auditory feedback of changing
* between vim states. * between vim states.
* @param {!Event} evt Change status event. * @param {!Event} evt Change status event.
* @param {!Object} editor Editor state. * @param {!Object} editor Editor state.
*/ */
var onChangeStatus = function(evt, editor) { var onChangeStatus = function(evt, editor) {
if (!isVimMode()) { if (!isVimMode()) {
return; return;
} }
@ -624,30 +627,30 @@ define(function(require, exports, module) {
break; break;
} }
vimState = state; vimState = state;
}; };
/** /**
* Handles context menu events. This is a ChromeVox feature where hitting * Handles context menu events. This is a ChromeVox feature where hitting
* the shortcut ChromeVox + comma will open up a search bar where you can * the shortcut ChromeVox + comma will open up a search bar where you can
* type in various commands. All keyboard shortcuts are also commands that * type in various commands. All keyboard shortcuts are also commands that
* can be invoked. This handles the event that ChromeVox sends to the page. * can be invoked. This handles the event that ChromeVox sends to the page.
* @param {Event} evt Event received. * @param {Event} evt Event received.
*/ */
var contextMenuHandler = function(evt) { var contextMenuHandler = function(evt) {
var cmd = evt.detail['customCommand']; var cmd = evt.detail['customCommand'];
var shortcut = cmdToShortcutMap[cmd]; var shortcut = cmdToShortcutMap[cmd];
if (shortcut) { if (shortcut) {
shortcut.func(); shortcut.func();
/* ChromeVox will bring focus to an element near the cursor instead of the /* ChromeVox will bring focus to an element near the cursor instead of the
* text input. */ * text input. */
ace.focus(); cvoxAce.editor.focus();
} }
}; };
/** /**
* Initialize the ChromeVox context menu. * Initialize the ChromeVox context menu.
*/ */
var initContextMenu = function() { var initContextMenu = function() {
var ACTIONS = SHORTCUTS.map(function(shortcut) { var ACTIONS = SHORTCUTS.map(function(shortcut) {
return { return {
desc: shortcut.desc + getKeyShortcutString(shortcut.keyCode), desc: shortcut.desc + getKeyShortcutString(shortcut.keyCode),
@ -661,63 +664,29 @@ define(function(require, exports, module) {
/* Listen for ContextMenu events. */ /* Listen for ContextMenu events. */
body.addEventListener('ATCustomEvent', contextMenuHandler, true); body.addEventListener('ATCustomEvent', contextMenuHandler, true);
}; };
/** var onFindSearchbox = function(evt) {
* Returns a mutations handler where f is applied to each mutation. if (evt.match) {
* @param {function} f Function to be applied to mutations.
*/
var getMutationHandler = function(f) {
return function(mutations) {
mutations.forEach(f);
};
};
/**
* Watches and handles the mutation that is a result of a search.
* @param {Mutation} m Mutation.
*/
var watchForSearch = function(m) {
if (m.attributeName === 'class' &&
m.target.className === 'ace_search_form ace_nomatch') {
/* No match, give auditory feedback! */
cvox.Api.playEarcon(NO_MATCH_EARCON);
} else {
/* There is still a match! Speak the line. */ /* There is still a match! Speak the line. */
speakLine(lastCursor.row, 0); speakLine(lastCursor.row, 0);
} else {
/* No match, give auditory feedback! */
cvox.Api.playEarcon(NO_MATCH_EARCON);
} }
}; };
/** /**
* Configuration for mutation observer.
*/
var MO_CONFIG = { attributes: true, childList: true, characterData: true};
/**
* Watches and handles the mutation that adds the search bar to the DOM.
*/
var watchForStartSearch = function(m) {
for (var i = 0; i < m.addedNodes.length; i++) {
if (m.addedNodes.item(i).className === 'ace_search right') {
var mutationHandler = getMutationHandler(watchForSearch);
var searchObs = new MutationObserver(mutationHandler);
var target = m.addedNodes.item(i).querySelector('.ace_search_form');
searchObs.observe(target, MO_CONFIG);
}
}
};
/**
* Focus to text input. * Focus to text input.
*/ */
var focus = function() { var focus = function() {
ace.focus(); cvoxAce.editor.focus();
}; };
/** /**
* Shortcut definitions. * Shortcut definitions.
*/ */
var SHORTCUTS = [ var SHORTCUTS = [
{ {
/* 1 key. */ /* 1 key. */
keyCode: 49, keyCode: 49,
@ -769,12 +738,14 @@ define(function(require, exports, module) {
cmd: Command.FOCUS_TEXT, cmd: Command.FOCUS_TEXT,
desc: 'Focus text' desc: 'Focus text'
} }
]; ];
/** /**
* Initialize the theme. * Initialize the theme.
*/ */
var init = function() { var init = function(editor) {
cvoxAce.editor = editor;
lastCursor = editor.selection.getCursor();
/* Construct maps. */ /* Construct maps. */
SHORTCUTS.forEach(function(shortcut) { SHORTCUTS.forEach(function(shortcut) {
keyCodeToShortcutMap[shortcut.keyCode] = shortcut; keyCodeToShortcutMap[shortcut.keyCode] = shortcut;
@ -782,10 +753,11 @@ define(function(require, exports, module) {
}); });
/* Set up listeners. */ /* Set up listeners. */
ace.getSession().selection.on('changeCursor', onCursorChange); cvoxAce.editor.getSession().selection.on('changeCursor', onCursorChange);
ace.getSession().on('change', onChange); cvoxAce.editor.getSession().on('change', onChange);
ace.getSession().on('changeAnnotation', onAnnotationChange); cvoxAce.editor.getSession().on('changeAnnotation', onAnnotationChange);
ace.on('changeStatus', onChangeStatus); cvoxAce.editor.on('changeStatus', onChangeStatus);
cvoxAce.editor.on('findSearchBox', onFindSearchbox);
window.addEventListener('keydown', onKeyDown); window.addEventListener('keydown', onKeyDown);
/* Assume we start in command mode if vim. */ /* Assume we start in command mode if vim. */
@ -794,50 +766,55 @@ define(function(require, exports, module) {
} }
initContextMenu(); initContextMenu();
var target = document.querySelector('.ace_editor'); cvoxAce.editor.focus();
var mutationHandler = getMutationHandler(watchForStartSearch); };
var observer = new MutationObserver(mutationHandler);
observer.observe(target, MO_CONFIG); /**
ace.focus();
};
/**
* Returns if cvox exists, and the api exists. * Returns if cvox exists, and the api exists.
* @return {boolean} Whether not Cvox Api exists. * @return {boolean} Whether not Cvox Api exists.
*/ */
function cvoxApiExists() { function cvoxApiExists() {
return (typeof(cvox) !== 'undefined') && cvox && cvox.Api; return (typeof(cvox) !== 'undefined') && cvox && cvox.Api;
} }
/** /**
* Number of tries for Cvox loading. * Number of tries for Cvox loading.
* @type {number} * @type {number}
*/ */
var tries = 0; var tries = 0;
/** /**
* Max number of tries to watch for Cvox loading. * Max number of tries to watch for Cvox loading.
* @type {number} * @type {number}
*/ */
var MAX_TRIES = 15; var MAX_TRIES = 15;
/** /**
* Check for ChromeVox load. * Check for ChromeVox load.
* @param {Object} editor Editor to use.
*/ */
function watchForCvoxLoad() { function watchForCvoxLoad(editor) {
if (cvoxApiExists()) { if (cvoxApiExists()) {
init(); init(editor);
} else { } else {
tries++; tries++;
if (tries >= MAX_TRIES) { if (tries >= MAX_TRIES) {
return; return;
} }
window.setTimeout(watchForCvoxLoad, 500); window.setTimeout(watchForCvoxLoad, 500, editor);
}
} }
}
var Editor = require('../editor').Editor;
require('../config').defineOptions(Editor.prototype, 'editor', {
enableChromevoxEnhancements: {
set: function(val) {
if (val) {
watchForCvoxLoad(this);
}
},
value: true // turn it on by default or check for window.cvox
}
});
/* Initialize everything when ChromeVox has loaded. */
watchForCvoxLoad();
}); });

View file

@ -214,7 +214,9 @@ var SearchBox = function(editor, range, showReplaceForm) {
caseSensitive: this.caseSensitiveOption.checked, caseSensitive: this.caseSensitiveOption.checked,
wholeWord: this.wholeWordOption.checked wholeWord: this.wholeWordOption.checked
}); });
dom.setCssClass(this.searchBox, "ace_nomatch", !range && this.searchInput.value); var noMatch = !range && this.searchInput.value;
dom.setCssClass(this.searchBox, "ace_nomatch", noMatch);
this.editor._emit("findSearchBox", { match: !noMatch });
this.highlight(); this.highlight();
}; };
this.findNext = function() { this.findNext = function() {

View file

@ -16,8 +16,8 @@ module.exports.themes = [
"kr_theme", "kr_theme",
"merbivore", "merbivore",
"merbivore_soft", "merbivore_soft",
"monokai",
"mono_industrial", "mono_industrial",
"monokai",
"pastel_on_dark", "pastel_on_dark",
"solarized_dark", "solarized_dark",
"solarized_light", "solarized_light",