Keine Beschreibung
Du kannst nicht mehr als 25 Themen auswählen Themen müssen mit entweder einem Buchstaben oder einer Ziffer beginnen. Sie können Bindestriche („-“) enthalten und bis zu 35 Zeichen lang sein.

ext-chromevox.js 13KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541
  1. ace.define("ace/ext/chromevox",["require","exports","module","ace/editor","ace/config"], function(require, exports, module) {
  2. var cvoxAce = {};
  3. cvoxAce.SpeechProperty;
  4. cvoxAce.Cursor;
  5. cvoxAce.Token;
  6. cvoxAce.Annotation;
  7. var CONSTANT_PROP = {
  8. 'rate': 0.8,
  9. 'pitch': 0.4,
  10. 'volume': 0.9
  11. };
  12. var DEFAULT_PROP = {
  13. 'rate': 1,
  14. 'pitch': 0.5,
  15. 'volume': 0.9
  16. };
  17. var ENTITY_PROP = {
  18. 'rate': 0.8,
  19. 'pitch': 0.8,
  20. 'volume': 0.9
  21. };
  22. var KEYWORD_PROP = {
  23. 'rate': 0.8,
  24. 'pitch': 0.3,
  25. 'volume': 0.9
  26. };
  27. var STORAGE_PROP = {
  28. 'rate': 0.8,
  29. 'pitch': 0.7,
  30. 'volume': 0.9
  31. };
  32. var VARIABLE_PROP = {
  33. 'rate': 0.8,
  34. 'pitch': 0.8,
  35. 'volume': 0.9
  36. };
  37. var DELETED_PROP = {
  38. 'punctuationEcho': 'none',
  39. 'relativePitch': -0.6
  40. };
  41. var ERROR_EARCON = 'ALERT_NONMODAL';
  42. var MODE_SWITCH_EARCON = 'ALERT_MODAL';
  43. var NO_MATCH_EARCON = 'INVALID_KEYPRESS';
  44. var INSERT_MODE_STATE = 'insertMode';
  45. var COMMAND_MODE_STATE = 'start';
  46. var REPLACE_LIST = [
  47. {
  48. substr: ';',
  49. newSubstr: ' semicolon '
  50. },
  51. {
  52. substr: ':',
  53. newSubstr: ' colon '
  54. }
  55. ];
  56. var Command = {
  57. SPEAK_ANNOT: 'annots',
  58. SPEAK_ALL_ANNOTS: 'all_annots',
  59. TOGGLE_LOCATION: 'toggle_location',
  60. SPEAK_MODE: 'mode',
  61. SPEAK_ROW_COL: 'row_col',
  62. TOGGLE_DISPLACEMENT: 'toggle_displacement',
  63. FOCUS_TEXT: 'focus_text'
  64. };
  65. var KEY_PREFIX = 'CONTROL + SHIFT ';
  66. cvoxAce.editor = null;
  67. var lastCursor = null;
  68. var annotTable = {};
  69. var shouldSpeakRowLocation = false;
  70. var shouldSpeakDisplacement = false;
  71. var changed = false;
  72. var vimState = null;
  73. var keyCodeToShortcutMap = {};
  74. var cmdToShortcutMap = {};
  75. var getKeyShortcutString = function(keyCode) {
  76. return KEY_PREFIX + String.fromCharCode(keyCode);
  77. };
  78. var isVimMode = function() {
  79. var keyboardHandler = cvoxAce.editor.keyBinding.getKeyboardHandler();
  80. return keyboardHandler.$id === 'ace/keyboard/vim';
  81. };
  82. var getCurrentToken = function(cursor) {
  83. return cvoxAce.editor.getSession().getTokenAt(cursor.row, cursor.column + 1);
  84. };
  85. var getCurrentLine = function(cursor) {
  86. return cvoxAce.editor.getSession().getLine(cursor.row);
  87. };
  88. var onRowChange = function(currCursor) {
  89. if (annotTable[currCursor.row]) {
  90. cvox.Api.playEarcon(ERROR_EARCON);
  91. }
  92. if (shouldSpeakRowLocation) {
  93. cvox.Api.stop();
  94. speakChar(currCursor);
  95. speakTokenQueue(getCurrentToken(currCursor));
  96. speakLine(currCursor.row, 1);
  97. } else {
  98. speakLine(currCursor.row, 0);
  99. }
  100. };
  101. var isWord = function(cursor) {
  102. var line = getCurrentLine(cursor);
  103. var lineSuffix = line.substr(cursor.column - 1);
  104. if (cursor.column === 0) {
  105. lineSuffix = ' ' + line;
  106. }
  107. var firstWordRegExp = /^\W(\w+)/;
  108. var words = firstWordRegExp.exec(lineSuffix);
  109. return words !== null;
  110. };
  111. var rules = {
  112. 'constant': {
  113. prop: CONSTANT_PROP
  114. },
  115. 'entity': {
  116. prop: ENTITY_PROP
  117. },
  118. 'keyword': {
  119. prop: KEYWORD_PROP
  120. },
  121. 'storage': {
  122. prop: STORAGE_PROP
  123. },
  124. 'variable': {
  125. prop: VARIABLE_PROP
  126. },
  127. 'meta': {
  128. prop: DEFAULT_PROP,
  129. replace: [
  130. {
  131. substr: '</',
  132. newSubstr: ' closing tag '
  133. },
  134. {
  135. substr: '/>',
  136. newSubstr: ' close tag '
  137. },
  138. {
  139. substr: '<',
  140. newSubstr: ' tag start '
  141. },
  142. {
  143. substr: '>',
  144. newSubstr: ' tag end '
  145. }
  146. ]
  147. }
  148. };
  149. var DEFAULT_RULE = {
  150. prop: DEFAULT_RULE
  151. };
  152. var expand = function(value, replaceRules) {
  153. var newValue = value;
  154. for (var i = 0; i < replaceRules.length; i++) {
  155. var replaceRule = replaceRules[i];
  156. var regexp = new RegExp(replaceRule.substr, 'g');
  157. newValue = newValue.replace(regexp, replaceRule.newSubstr);
  158. }
  159. return newValue;
  160. };
  161. var mergeTokens = function(tokens, start, end) {
  162. var newToken = {};
  163. newToken.value = '';
  164. newToken.type = tokens[start].type;
  165. for (var j = start; j < end; j++) {
  166. newToken.value += tokens[j].value;
  167. }
  168. return newToken;
  169. };
  170. var mergeLikeTokens = function(tokens) {
  171. if (tokens.length <= 1) {
  172. return tokens;
  173. }
  174. var newTokens = [];
  175. var lastLikeIndex = 0;
  176. for (var i = 1; i < tokens.length; i++) {
  177. var lastLikeToken = tokens[lastLikeIndex];
  178. var currToken = tokens[i];
  179. if (getTokenRule(lastLikeToken) !== getTokenRule(currToken)) {
  180. newTokens.push(mergeTokens(tokens, lastLikeIndex, i));
  181. lastLikeIndex = i;
  182. }
  183. }
  184. newTokens.push(mergeTokens(tokens, lastLikeIndex, tokens.length));
  185. return newTokens;
  186. };
  187. var isRowWhiteSpace = function(row) {
  188. var line = cvoxAce.editor.getSession().getLine(row);
  189. var whiteSpaceRegexp = /^\s*$/;
  190. return whiteSpaceRegexp.exec(line) !== null;
  191. };
  192. var speakLine = function(row, queue) {
  193. var tokens = cvoxAce.editor.getSession().getTokens(row);
  194. if (tokens.length === 0 || isRowWhiteSpace(row)) {
  195. cvox.Api.playEarcon('EDITABLE_TEXT');
  196. return;
  197. }
  198. tokens = mergeLikeTokens(tokens);
  199. var firstToken = tokens[0];
  200. tokens = tokens.filter(function(token) {
  201. return token !== firstToken;
  202. });
  203. speakToken_(firstToken, queue);
  204. tokens.forEach(speakTokenQueue);
  205. };
  206. var speakTokenFlush = function(token) {
  207. speakToken_(token, 0);
  208. };
  209. var speakTokenQueue = function(token) {
  210. speakToken_(token, 1);
  211. };
  212. var getTokenRule = function(token) {
  213. if (!token || !token.type) {
  214. return;
  215. }
  216. var split = token.type.split('.');
  217. if (split.length === 0) {
  218. return;
  219. }
  220. var type = split[0];
  221. var rule = rules[type];
  222. if (!rule) {
  223. return DEFAULT_RULE;
  224. }
  225. return rule;
  226. };
  227. var speakToken_ = function(token, queue) {
  228. var rule = getTokenRule(token);
  229. var value = expand(token.value, REPLACE_LIST);
  230. if (rule.replace) {
  231. value = expand(value, rule.replace);
  232. }
  233. cvox.Api.speak(value, queue, rule.prop);
  234. };
  235. var speakChar = function(cursor) {
  236. var line = getCurrentLine(cursor);
  237. cvox.Api.speak(line[cursor.column], 1);
  238. };
  239. var speakDisplacement = function(lastCursor, currCursor) {
  240. var line = getCurrentLine(currCursor);
  241. var displace = line.substring(lastCursor.column, currCursor.column);
  242. displace = displace.replace(/ /g, ' space ');
  243. cvox.Api.speak(displace);
  244. };
  245. var speakCharOrWordOrLine = function(lastCursor, currCursor) {
  246. if (Math.abs(lastCursor.column - currCursor.column) !== 1) {
  247. var currLineLength = getCurrentLine(currCursor).length;
  248. if (currCursor.column === 0 || currCursor.column === currLineLength) {
  249. speakLine(currCursor.row, 0);
  250. return;
  251. }
  252. if (isWord(currCursor)) {
  253. cvox.Api.stop();
  254. speakTokenQueue(getCurrentToken(currCursor));
  255. return;
  256. }
  257. }
  258. speakChar(currCursor);
  259. };
  260. var onColumnChange = function(lastCursor, currCursor) {
  261. if (!cvoxAce.editor.selection.isEmpty()) {
  262. speakDisplacement(lastCursor, currCursor);
  263. cvox.Api.speak('selected', 1);
  264. }
  265. else if (shouldSpeakDisplacement) {
  266. speakDisplacement(lastCursor, currCursor);
  267. } else {
  268. speakCharOrWordOrLine(lastCursor, currCursor);
  269. }
  270. };
  271. var onCursorChange = function(evt) {
  272. if (changed) {
  273. changed = false;
  274. return;
  275. }
  276. var currCursor = cvoxAce.editor.selection.getCursor();
  277. if (currCursor.row !== lastCursor.row) {
  278. onRowChange(currCursor);
  279. } else {
  280. onColumnChange(lastCursor, currCursor);
  281. }
  282. lastCursor = currCursor;
  283. };
  284. var onSelectionChange = function(evt) {
  285. if (cvoxAce.editor.selection.isEmpty()) {
  286. cvox.Api.speak('unselected');
  287. }
  288. };
  289. var onChange = function(evt) {
  290. var data = evt.data;
  291. switch (data.action) {
  292. case 'removeText':
  293. cvox.Api.speak(data.text, 0, DELETED_PROP);
  294. changed = true;
  295. break;
  296. case 'insertText':
  297. cvox.Api.speak(data.text, 0);
  298. changed = true;
  299. break;
  300. }
  301. };
  302. var isNewAnnotation = function(annot) {
  303. var row = annot.row;
  304. var col = annot.column;
  305. return !annotTable[row] || !annotTable[row][col];
  306. };
  307. var populateAnnotations = function(annotations) {
  308. annotTable = {};
  309. for (var i = 0; i < annotations.length; i++) {
  310. var annotation = annotations[i];
  311. var row = annotation.row;
  312. var col = annotation.column;
  313. if (!annotTable[row]) {
  314. annotTable[row] = {};
  315. }
  316. annotTable[row][col] = annotation;
  317. }
  318. };
  319. var onAnnotationChange = function(evt) {
  320. var annotations = cvoxAce.editor.getSession().getAnnotations();
  321. var newAnnotations = annotations.filter(isNewAnnotation);
  322. if (newAnnotations.length > 0) {
  323. cvox.Api.playEarcon(ERROR_EARCON);
  324. }
  325. populateAnnotations(annotations);
  326. };
  327. var speakAnnot = function(annot) {
  328. var annotText = annot.type + ' ' + annot.text + ' on ' +
  329. rowColToString(annot.row, annot.column);
  330. annotText = annotText.replace(';', 'semicolon');
  331. cvox.Api.speak(annotText, 1);
  332. };
  333. var speakAnnotsByRow = function(row) {
  334. var annots = annotTable[row];
  335. for (var col in annots) {
  336. speakAnnot(annots[col]);
  337. }
  338. };
  339. var rowColToString = function(row, col) {
  340. return 'row ' + (row + 1) + ' column ' + (col + 1);
  341. };
  342. var speakCurrRowAndCol = function() {
  343. cvox.Api.speak(rowColToString(lastCursor.row, lastCursor.column));
  344. };
  345. var speakAllAnnots = function() {
  346. for (var row in annotTable) {
  347. speakAnnotsByRow(row);
  348. }
  349. };
  350. var speakMode = function() {
  351. if (!isVimMode()) {
  352. return;
  353. }
  354. switch (cvoxAce.editor.keyBinding.$data.state) {
  355. case INSERT_MODE_STATE:
  356. cvox.Api.speak('Insert mode');
  357. break;
  358. case COMMAND_MODE_STATE:
  359. cvox.Api.speak('Command mode');
  360. break;
  361. }
  362. };
  363. var toggleSpeakRowLocation = function() {
  364. shouldSpeakRowLocation = !shouldSpeakRowLocation;
  365. if (shouldSpeakRowLocation) {
  366. cvox.Api.speak('Speak location on row change enabled.');
  367. } else {
  368. cvox.Api.speak('Speak location on row change disabled.');
  369. }
  370. };
  371. var toggleSpeakDisplacement = function() {
  372. shouldSpeakDisplacement = !shouldSpeakDisplacement;
  373. if (shouldSpeakDisplacement) {
  374. cvox.Api.speak('Speak displacement on column changes.');
  375. } else {
  376. cvox.Api.speak('Speak current character or word on column changes.');
  377. }
  378. };
  379. var onKeyDown = function(evt) {
  380. if (evt.ctrlKey && evt.shiftKey) {
  381. var shortcut = keyCodeToShortcutMap[evt.keyCode];
  382. if (shortcut) {
  383. shortcut.func();
  384. }
  385. }
  386. };
  387. var onChangeStatus = function(evt, editor) {
  388. if (!isVimMode()) {
  389. return;
  390. }
  391. var state = editor.keyBinding.$data.state;
  392. if (state === vimState) {
  393. return;
  394. }
  395. switch (state) {
  396. case INSERT_MODE_STATE:
  397. cvox.Api.playEarcon(MODE_SWITCH_EARCON);
  398. cvox.Api.setKeyEcho(true);
  399. break;
  400. case COMMAND_MODE_STATE:
  401. cvox.Api.playEarcon(MODE_SWITCH_EARCON);
  402. cvox.Api.setKeyEcho(false);
  403. break;
  404. }
  405. vimState = state;
  406. };
  407. var contextMenuHandler = function(evt) {
  408. var cmd = evt.detail['customCommand'];
  409. var shortcut = cmdToShortcutMap[cmd];
  410. if (shortcut) {
  411. shortcut.func();
  412. cvoxAce.editor.focus();
  413. }
  414. };
  415. var initContextMenu = function() {
  416. var ACTIONS = SHORTCUTS.map(function(shortcut) {
  417. return {
  418. desc: shortcut.desc + getKeyShortcutString(shortcut.keyCode),
  419. cmd: shortcut.cmd
  420. };
  421. });
  422. var body = document.querySelector('body');
  423. body.setAttribute('contextMenuActions', JSON.stringify(ACTIONS));
  424. body.addEventListener('ATCustomEvent', contextMenuHandler, true);
  425. };
  426. var onFindSearchbox = function(evt) {
  427. if (evt.match) {
  428. speakLine(lastCursor.row, 0);
  429. } else {
  430. cvox.Api.playEarcon(NO_MATCH_EARCON);
  431. }
  432. };
  433. var focus = function() {
  434. cvoxAce.editor.focus();
  435. };
  436. var SHORTCUTS = [
  437. {
  438. keyCode: 49,
  439. func: function() {
  440. speakAnnotsByRow(lastCursor.row);
  441. },
  442. cmd: Command.SPEAK_ANNOT,
  443. desc: 'Speak annotations on line'
  444. },
  445. {
  446. keyCode: 50,
  447. func: speakAllAnnots,
  448. cmd: Command.SPEAK_ALL_ANNOTS,
  449. desc: 'Speak all annotations'
  450. },
  451. {
  452. keyCode: 51,
  453. func: speakMode,
  454. cmd: Command.SPEAK_MODE,
  455. desc: 'Speak Vim mode'
  456. },
  457. {
  458. keyCode: 52,
  459. func: toggleSpeakRowLocation,
  460. cmd: Command.TOGGLE_LOCATION,
  461. desc: 'Toggle speak row location'
  462. },
  463. {
  464. keyCode: 53,
  465. func: speakCurrRowAndCol,
  466. cmd: Command.SPEAK_ROW_COL,
  467. desc: 'Speak row and column'
  468. },
  469. {
  470. keyCode: 54,
  471. func: toggleSpeakDisplacement,
  472. cmd: Command.TOGGLE_DISPLACEMENT,
  473. desc: 'Toggle speak displacement'
  474. },
  475. {
  476. keyCode: 55,
  477. func: focus,
  478. cmd: Command.FOCUS_TEXT,
  479. desc: 'Focus text'
  480. }
  481. ];
  482. var onFocus = function() {
  483. cvoxAce.editor = editor;
  484. editor.getSession().selection.on('changeCursor', onCursorChange);
  485. editor.getSession().selection.on('changeSelection', onSelectionChange);
  486. editor.getSession().on('change', onChange);
  487. editor.getSession().on('changeAnnotation', onAnnotationChange);
  488. editor.on('changeStatus', onChangeStatus);
  489. editor.on('findSearchBox', onFindSearchbox);
  490. editor.container.addEventListener('keydown', onKeyDown);
  491. lastCursor = editor.selection.getCursor();
  492. };
  493. var init = function(editor) {
  494. onFocus();
  495. SHORTCUTS.forEach(function(shortcut) {
  496. keyCodeToShortcutMap[shortcut.keyCode] = shortcut;
  497. cmdToShortcutMap[shortcut.cmd] = shortcut;
  498. });
  499. editor.on('focus', onFocus);
  500. if (isVimMode()) {
  501. cvox.Api.setKeyEcho(false);
  502. }
  503. initContextMenu();
  504. };
  505. function cvoxApiExists() {
  506. return (typeof(cvox) !== 'undefined') && cvox && cvox.Api;
  507. }
  508. var tries = 0;
  509. var MAX_TRIES = 15;
  510. function watchForCvoxLoad(editor) {
  511. if (cvoxApiExists()) {
  512. init(editor);
  513. } else {
  514. tries++;
  515. if (tries >= MAX_TRIES) {
  516. return;
  517. }
  518. window.setTimeout(watchForCvoxLoad, 500, editor);
  519. }
  520. }
  521. var Editor = require('../editor').Editor;
  522. require('../config').defineOptions(Editor.prototype, 'editor', {
  523. enableChromevoxEnhancements: {
  524. set: function(val) {
  525. if (val) {
  526. watchForCvoxLoad(this);
  527. }
  528. },
  529. value: true // turn it on by default or check for window.cvox
  530. }
  531. });
  532. });
  533. (function() {
  534. ace.require(["ace/ext/chromevox"], function() {});
  535. })();