accessibleTextarea.js (5743B)
1 /* 2 * Copyright (C) 2012 Google Inc. All rights reserved. 3 * 4 * Redistribution and use in source and binary forms, with or without 5 * modification, are permitted provided that the following conditions are 6 * met: 7 * 8 * * Redistributions of source code must retain the above copyright 9 * notice, this list of conditions and the following disclaimer. 10 * * Redistributions in binary form must reproduce the above 11 * copyright notice, this list of conditions and the following disclaimer 12 * in the documentation and/or other materials provided with the 13 * distribution. 14 * * Neither the name of Google Inc. nor the names of its 15 * contributors may be used to endorse or promote products derived from 16 * this software without specific prior written permission. 17 * 18 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 24 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 25 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 26 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 */ 30 (function(mod) { 31 mod(require("resource://devtools/client/shared/sourceeditor/codemirror/lib/codemirror.js")); 32 })(function(CodeMirror) { 33 // CodeMirror uses an offscreen <textarea> to detect input. 34 // Due to inconsistencies in the many browsers it supports, it simplifies things by 35 // regularly checking if something is in the textarea, adding those characters to the 36 // document, and then clearing the textarea. 37 // This breaks assistive technology that wants to read from CodeMirror, because the 38 // <textarea> that they interact with is constantly empty. 39 // Because we target up-to-date Firefox, we can guarantee consistent input events. 40 // This lets us leave the current line from the editor in our <textarea>. 41 // CodeMirror still expects a mostly empty <textarea>, so we pass CodeMirror a fake 42 // <textarea> that only contains the users input. 43 CodeMirror.inputStyles.accessibleTextArea = class extends CodeMirror.inputStyles.textarea { 44 /** 45 * @override 46 * @param {!Object} display 47 */ 48 init(display) { 49 super.init(display); 50 this.textarea.addEventListener("compositionstart", 51 this._onCompositionStart.bind(this)); 52 } 53 54 _onCompositionStart() { 55 if (this.textarea.selectionEnd === this.textarea.value.length) { 56 return; 57 } 58 59 // CodeMirror always expects the caret to be at the end of the textarea 60 // When in IME composition mode, clip the textarea to how CodeMirror expects it, 61 // and then let CodeMirror do its thing. 62 this.textarea.value = this.textarea.value.substring(0, this.textarea.selectionEnd); 63 const length = this.textarea.value.length; 64 this.textarea.setSelectionRange(length, length); 65 this.prevInput = this.textarea.value; 66 } 67 68 /** 69 * @override 70 * @param {Boolean} typing 71 */ 72 reset(typing) { 73 if ( 74 typing || 75 this.contextMenuPending || 76 this.composing || 77 this.cm.somethingSelected() 78 ) { 79 super.reset(typing); 80 return; 81 } 82 83 // When navigating around the document, keep the current visual line in the textarea. 84 const cursor = this.cm.getCursor(); 85 let start, end; 86 if (this.cm.options.lineWrapping) { 87 // To get the visual line, compute the leftmost and rightmost character positions. 88 const top = this.cm.charCoords(cursor, "page").top; 89 start = this.cm.coordsChar({left: -Infinity, top}); 90 end = this.cm.coordsChar({left: Infinity, top}); 91 } else { 92 // Limit the line to 1000 characters to prevent lag. 93 const offset = Math.floor(cursor.ch / 1000) * 1000; 94 start = {ch: offset, line: cursor.line}; 95 end = {ch: offset + 1000, line: cursor.line}; 96 } 97 98 this.textarea.value = this.cm.getRange(start, end); 99 const caretPosition = cursor.ch - start.ch; 100 this.textarea.setSelectionRange(caretPosition, caretPosition); 101 this.prevInput = this.textarea.value; 102 } 103 104 /** 105 * @override 106 * @return {boolean} 107 */ 108 poll() { 109 if (this.contextMenuPending || this.composing) { 110 return super.poll(); 111 } 112 113 const text = this.textarea.value; 114 let start = 0; 115 const length = Math.min(this.prevInput.length, text.length); 116 117 while (start < length && this.prevInput[start] === text[start]) { 118 ++start; 119 } 120 121 let end = 0; 122 123 while ( 124 end < length - start && 125 this.prevInput[this.prevInput.length - end - 1] === text[text.length - end - 1] 126 ) { 127 ++end; 128 } 129 130 // CodeMirror expects the user to be typing into a blank <textarea>. 131 // Pass a fake textarea into super.poll that only contains the users input. 132 const placeholder = this.textarea; 133 this.textarea = document.createElement("textarea"); 134 this.textarea.value = text.substring(start, text.length - end); 135 this.textarea.setSelectionRange( 136 placeholder.selectionStart - start, 137 placeholder.selectionEnd - start 138 ); 139 this.prevInput = ""; 140 const result = super.poll(); 141 this.prevInput = text; 142 this.textarea = placeholder; 143 return result; 144 } 145 }; 146 });