├── .gitignore ├── bililiteRange.js ├── index.js ├── package.json └── readme.md /.gitignore: -------------------------------------------------------------------------------- 1 | .*.sw? 2 | -------------------------------------------------------------------------------- /bililiteRange.js: -------------------------------------------------------------------------------- 1 | // Cross-broswer implementation of text ranges and selections 2 | // documentation: http://bililite.com/blog/2011/01/17/cross-browser-text-ranges-and-selections/ 3 | // Version: 2.6 4 | // Copyright (c) 2013 Daniel Wachsstock 5 | // MIT license: 6 | // Permission is hereby granted, free of charge, to any person 7 | // obtaining a copy of this software and associated documentation 8 | // files (the "Software"), to deal in the Software without 9 | // restriction, including without limitation the rights to use, 10 | // copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the 12 | // Software is furnished to do so, subject to the following 13 | // conditions: 14 | 15 | // The above copyright notice and this permission notice shall be 16 | // included in all copies or substantial portions of the Software. 17 | 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 19 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 20 | // OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 21 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 22 | // HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 23 | // WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 24 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 25 | // OTHER DEALINGS IN THE SOFTWARE. 26 | 27 | (function(){ 28 | 29 | // a bit of weirdness with IE11: using 'focus' is flaky, even if I'm not bubbling, as far as I can tell. 30 | var focusEvent = 'onfocusin' in document.createElement('input') ? 'focusin' : 'focus'; 31 | 32 | // IE11 normalize is buggy (http://connect.microsoft.com/IE/feedback/details/809424/node-normalize-removes-text-if-dashes-are-present) 33 | var n = document.createElement('div'); 34 | n.appendChild(document.createTextNode('x-')); 35 | n.appendChild(document.createTextNode('x')); 36 | n.normalize(); 37 | var canNormalize = n.firstChild.length == 3; 38 | 39 | 40 | var bililiteRange = module.exports = function(el, debug){ 41 | var ret; 42 | if (debug){ 43 | ret = new NothingRange(); // Easier to force it to use the no-selection type than to try to find an old browser 44 | }else if (window.getSelection && el.setSelectionRange){ 45 | // Standards. Element is an input or textarea 46 | // note that some input elements do not allow selections 47 | try{ 48 | el.selectionStart; // even getting the selection in such an element will throw 49 | ret = new InputRange(); 50 | }catch(e){ 51 | ret = new NothingRange(); 52 | } 53 | }else if (window.getSelection){ 54 | // Standards, with any other kind of element 55 | ret = new W3CRange(); 56 | }else if (document.selection){ 57 | // Internet Explorer 58 | ret = new IERange(); 59 | }else{ 60 | // doesn't support selection 61 | ret = new NothingRange(); 62 | } 63 | ret._el = el; 64 | // determine parent document, as implemented by John McLear 65 | ret._doc = el.ownerDocument; 66 | ret._win = 'defaultView' in ret._doc ? ret._doc.defaultView : ret._doc.parentWindow; 67 | ret._textProp = textProp(el); 68 | ret._bounds = [0, ret.length()]; 69 | // There's no way to detect whether a focus event happened as a result of a click (which should change the selection) 70 | // or as a result of a keyboard event (a tab in) or a script action (el.focus()). So we track it globally, which is a hack, and is likely to fail 71 | // in edge cases (right-clicks, drag-n-drop), and is vulnerable to a lower-down handler preventing bubbling. 72 | // I just don't know a better way. 73 | // I'll hack my event-listening code below, rather than create an entire new bilililiteRange, potentially before the DOM has loaded 74 | if (!('bililiteRangeMouseDown' in ret._doc)){ 75 | var _doc = {_el: ret._doc}; 76 | ret._doc.bililiteRangeMouseDown = false; 77 | bililiteRange.fn.listen.call(_doc, 'mousedown', function() { 78 | ret._doc.bililiteRangeMouseDown = true; 79 | }); 80 | bililiteRange.fn.listen.call(_doc, 'mouseup', function() { 81 | ret._doc.bililiteRangeMouseDown = false; 82 | }); 83 | } 84 | // note that bililiteRangeSelection is an array, which means that copying it only copies the address, which points to the original. 85 | // make sure that we never let it (always do return [bililiteRangeSelection[0], bililiteRangeSelection[1]]), which means never returning 86 | // this._bounds directly 87 | if (!('bililiteRangeSelection' in el)){ 88 | // start tracking the selection 89 | function trackSelection(evt){ 90 | if (evt && evt.which == 9){ 91 | // do tabs my way, by restoring the selection 92 | // there's a flash of the browser's selection, but I don't see a way of avoiding that 93 | ret._nativeSelect(ret._nativeRange(el.bililiteRangeSelection)); 94 | }else{ 95 | el.bililiteRangeSelection = ret._nativeSelection(); 96 | } 97 | } 98 | trackSelection(); 99 | // only IE does this right and allows us to grab the selection before blurring 100 | if ('onbeforedeactivate' in el){ 101 | ret.listen('beforedeactivate', trackSelection); 102 | }else{ 103 | // with standards-based browsers, have to listen for every user interaction 104 | ret.listen('mouseup', trackSelection).listen('keyup', trackSelection); 105 | } 106 | ret.listen(focusEvent, function(){ 107 | // restore the correct selection when the element comes into focus (mouse clicks change the position of the selection) 108 | // Note that Firefox will not fire the focus event until the window/tab is active even if el.focus() is called 109 | // https://bugzilla.mozilla.org/show_bug.cgi?id=566671 110 | if (!ret._doc.bililiteRangeMouseDown){ 111 | ret._nativeSelect(ret._nativeRange(el.bililiteRangeSelection)); 112 | } 113 | }); 114 | } 115 | if (!('oninput' in el)){ 116 | // give IE8 a chance. Note that this still fails in IE11, which has has oninput on contenteditable elements but does not 117 | // dispatch input events. See http://connect.microsoft.com/IE/feedback/details/794285/ie10-11-input-event-does-not-fire-on-div-with-contenteditable-set 118 | // TODO: revisit this when I have IE11 running on my development machine 119 | var inputhack = function() {ret.dispatch({type: 'input', bubbles: true}) }; 120 | ret.listen('keyup', inputhack); 121 | ret.listen('cut', inputhack); 122 | ret.listen('paste', inputhack); 123 | ret.listen('drop', inputhack); 124 | el.oninput = 'patched'; 125 | } 126 | return ret; 127 | } 128 | 129 | function textProp(el){ 130 | // returns the property that contains the text of the element 131 | // note that for elements the text attribute represents the obsolete text color, not the textContent. 132 | // we document that these routines do not work for elements so that should not be relevant 133 | if (typeof el.value != 'undefined') return 'value'; 134 | if (typeof el.text != 'undefined') return 'text'; 135 | if (typeof el.textContent != 'undefined') return 'textContent'; 136 | return 'innerText'; 137 | } 138 | 139 | // base class 140 | function Range(){} 141 | Range.prototype = { 142 | length: function() { 143 | return this._el[this._textProp].replace(/\r/g, '').length; // need to correct for IE's CrLf weirdness 144 | }, 145 | bounds: function(s){ 146 | if (bililiteRange.bounds[s]){ 147 | this._bounds = bililiteRange.bounds[s].apply(this); 148 | }else if (s){ 149 | this._bounds = s; // don't do error checking now; things may change at a moment's notice 150 | }else{ 151 | var b = [ 152 | Math.max(0, Math.min (this.length(), this._bounds[0])), 153 | Math.max(0, Math.min (this.length(), this._bounds[1])) 154 | ]; 155 | b[1] = Math.max(b[0], b[1]); 156 | return b; // need to constrain it to fit 157 | } 158 | return this; // allow for chaining 159 | }, 160 | select: function(){ 161 | var b = this._el.bililiteRangeSelection = this.bounds(); 162 | if (this._el === this._doc.activeElement){ 163 | // only actually select if this element is active! 164 | this._nativeSelect(this._nativeRange(b)); 165 | } 166 | this.dispatch({type: 'select', bubbles: true}); 167 | return this; // allow for chaining 168 | }, 169 | text: function(text, select){ 170 | if (arguments.length){ 171 | var bounds = this.bounds(), el = this._el; 172 | // signal the input per DOM 3 input events, http://www.w3.org/TR/DOM-Level-3-Events/#h4_events-inputevents 173 | // we add another field, bounds, which are the bounds of the original text before being changed. 174 | this.dispatch({type: 'beforeinput', bubbles: true, 175 | data: text, bounds: bounds}); 176 | this._nativeSetText(text, this._nativeRange(bounds)); 177 | if (select == 'start'){ 178 | this.bounds ([bounds[0], bounds[0]]); 179 | }else if (select == 'end'){ 180 | this.bounds ([bounds[0]+text.length, bounds[0]+text.length]); 181 | }else if (select == 'all'){ 182 | this.bounds ([bounds[0], bounds[0]+text.length]); 183 | } 184 | this.dispatch({type: 'input', bubbles: true, 185 | data: text, bounds: bounds}); 186 | return this; // allow for chaining 187 | }else{ 188 | return this._nativeGetText(this._nativeRange(this.bounds())).replace(/\r/g, ''); // need to correct for IE's CrLf weirdness 189 | } 190 | }, 191 | insertEOL: function (){ 192 | this._nativeEOL(); 193 | this._bounds = [this._bounds[0]+1, this._bounds[0]+1]; // move past the EOL marker 194 | return this; 195 | }, 196 | sendkeys: function (text){ 197 | var self = this; 198 | this.data().sendkeysOriginalText = this.text(); 199 | this.data().sendkeysBounds = undefined; 200 | function simplechar (rng, c){ 201 | if (/^{[^}]*}$/.test(c)) c = c.slice(1,-1); // deal with unknown {key}s 202 | for (var i =0; i < c.length; ++i){ 203 | var x = c.charCodeAt(i); 204 | rng.dispatch({type: 'keypress', bubbles: true, keyCode: x, which: x, charCode: x}); 205 | } 206 | rng.text(c, 'end'); 207 | } 208 | text.replace(/{[^}]*}|[^{]+|{/g, function(part){ 209 | (bililiteRange.sendkeys[part] || simplechar)(self, part, simplechar); 210 | }); 211 | this.bounds(this.data().sendkeysBounds); 212 | this.dispatch({type: 'sendkeys', which: text}); 213 | return this; 214 | }, 215 | top: function(){ 216 | return this._nativeTop(this._nativeRange(this.bounds())); 217 | }, 218 | scrollIntoView: function(scroller){ 219 | var top = this.top(); 220 | // scroll into position if necessary 221 | if (this._el.scrollTop > top || this._el.scrollTop+this._el.clientHeight < top){ 222 | if (scroller){ 223 | scroller.call(this._el, top); 224 | }else{ 225 | this._el.scrollTop = top; 226 | } 227 | } 228 | return this; 229 | }, 230 | wrap: function (n){ 231 | this._nativeWrap(n, this._nativeRange(this.bounds())); 232 | return this; 233 | }, 234 | selection: function(text){ 235 | if (arguments.length){ 236 | return this.bounds('selection').text(text, 'end').select(); 237 | }else{ 238 | return this.bounds('selection').text(); 239 | } 240 | }, 241 | clone: function(){ 242 | return bililiteRange(this._el).bounds(this.bounds()); 243 | }, 244 | all: function(text){ 245 | if (arguments.length){ 246 | this.dispatch ({type: 'beforeinput', bubbles: true, data: text}); 247 | this._el[this._textProp] = text; 248 | this.dispatch ({type: 'input', bubbles: true, data: text}); 249 | return this; 250 | }else{ 251 | return this._el[this._textProp].replace(/\r/g, ''); // need to correct for IE's CrLf weirdness 252 | } 253 | }, 254 | element: function() { return this._el }, 255 | // includes a quickie polyfill for CustomEvent for IE that isn't perfect but works for me 256 | // IE10 allows custom events but not "new CustomEvent"; have to do it the old-fashioned way 257 | dispatch: function(opts){ 258 | opts = opts || {}; 259 | var event = document.createEvent ? document.createEvent('CustomEvent') : this._doc.createEventObject(); 260 | event.initCustomEvent && event.initCustomEvent(opts.type, !!opts.bubbles, !!opts.cancelable, opts.detail); 261 | for (var key in opts) event[key] = opts[key]; 262 | // dispatch event asynchronously (in the sense of on the next turn of the event loop; still should be fired in order of dispatch 263 | var el = this._el; 264 | setTimeout(function(){ 265 | try { 266 | el.dispatchEvent ? el.dispatchEvent(event) : el.fireEvent("on" + opts.type, document.createEventObject()); 267 | }catch(e){ 268 | // IE8 will not let me fire custom events at all. Call them directly 269 | var listeners = el['listen'+opts.type]; 270 | if (listeners) for (var i = 0; i < listeners.length; ++i){ 271 | listeners[i].call(el, event); 272 | } 273 | } 274 | }, 0); 275 | return this; 276 | }, 277 | listen: function (type, func){ 278 | var el = this._el; 279 | if (el.addEventListener){ 280 | el.addEventListener(type, func); 281 | }else{ 282 | el.attachEvent("on" + type, func); 283 | // IE8 can't even handle custom events created with createEventObject (though it permits attachEvent), so we have to make our own 284 | var listeners = el['listen'+type] = el['listen'+type] || []; 285 | listeners.push(func); 286 | } 287 | return this; 288 | }, 289 | dontlisten: function (type, func){ 290 | var el = this._el; 291 | if (el.removeEventListener){ 292 | el.removeEventListener(type, func); 293 | }else try{ 294 | el.detachEvent("on" + type, func); 295 | }catch(e){ 296 | var listeners = el['listen'+type]; 297 | if (listeners) for (var i = 0; i < listeners.length; ++i){ 298 | if (listeners[i] === func) listeners[i] = function(){}; // replace with a noop 299 | } 300 | } 301 | return this; 302 | } 303 | }; 304 | 305 | // allow extensions ala jQuery 306 | bililiteRange.fn = Range.prototype; // to allow monkey patching 307 | bililiteRange.extend = function(fns){ 308 | for (fn in fns) Range.prototype[fn] = fns[fn]; 309 | }; 310 | 311 | //bounds functions 312 | bililiteRange.bounds = { 313 | all: function() { return [0, this.length()] }, 314 | start: function () { return [0,0] }, 315 | end: function () { return [this.length(), this.length()] }, 316 | selection: function(){ 317 | if (this._el === this._doc.activeElement){ 318 | this.bounds ('all'); // first select the whole thing for constraining 319 | return this._nativeSelection(); 320 | }else{ 321 | return this._el.bililiteRangeSelection; 322 | } 323 | } 324 | }; 325 | 326 | // sendkeys functions 327 | bililiteRange.sendkeys = { 328 | '{enter}': function (rng){ 329 | rng.dispatch({type: 'keypress', bubbles: true, keyCode: '\n', which: '\n', charCode: '\n'}); 330 | rng.insertEOL(); 331 | }, 332 | '{tab}': function (rng, c, simplechar){ 333 | simplechar(rng, '\t'); // useful for inserting what would be whitespace 334 | }, 335 | '{newline}': function (rng, c, simplechar){ 336 | simplechar(rng, '\n'); // useful for inserting what would be whitespace (and if I don't want to use insertEOL, which does some fancy things) 337 | }, 338 | '{backspace}': function (rng){ 339 | var b = rng.bounds(); 340 | if (b[0] == b[1]) rng.bounds([b[0]-1, b[0]]); // no characters selected; it's just an insertion point. Remove the previous character 341 | rng.text('', 'end'); // delete the characters and update the selection 342 | }, 343 | '{del}': function (rng){ 344 | var b = rng.bounds(); 345 | if (b[0] == b[1]) rng.bounds([b[0], b[0]+1]); // no characters selected; it's just an insertion point. Remove the next character 346 | rng.text('', 'end'); // delete the characters and update the selection 347 | }, 348 | '{rightarrow}': function (rng){ 349 | var b = rng.bounds(); 350 | if (b[0] == b[1]) ++b[1]; // no characters selected; it's just an insertion point. Move to the right 351 | rng.bounds([b[1], b[1]]); 352 | }, 353 | '{leftarrow}': function (rng){ 354 | var b = rng.bounds(); 355 | if (b[0] == b[1]) --b[0]; // no characters selected; it's just an insertion point. Move to the left 356 | rng.bounds([b[0], b[0]]); 357 | }, 358 | '{selectall}' : function (rng){ 359 | rng.bounds('all'); 360 | }, 361 | '{selection}': function (rng){ 362 | // insert the characters without the sendkeys processing 363 | var s = rng.data().sendkeysOriginalText; 364 | for (var i =0; i < s.length; ++i){ 365 | var x = s.charCodeAt(i); 366 | rng.dispatch({type: 'keypress', bubbles: true, keyCode: x, which: x, charCode: x}); 367 | } 368 | rng.text(s, 'end'); 369 | }, 370 | '{mark}' : function (rng){ 371 | rng.data().sendkeysBounds = rng.bounds(); 372 | } 373 | }; 374 | // Synonyms from the proposed DOM standard (http://www.w3.org/TR/DOM-Level-3-Events-key/) 375 | bililiteRange.sendkeys['{Enter}'] = bililiteRange.sendkeys['{enter}']; 376 | bililiteRange.sendkeys['{Backspace}'] = bililiteRange.sendkeys['{backspace}']; 377 | bililiteRange.sendkeys['{Delete}'] = bililiteRange.sendkeys['{del}']; 378 | bililiteRange.sendkeys['{ArrowRight}'] = bililiteRange.sendkeys['{rightarrow}']; 379 | bililiteRange.sendkeys['{ArrowLeft}'] = bililiteRange.sendkeys['{leftarrow}']; 380 | 381 | function IERange(){} 382 | IERange.prototype = new Range(); 383 | IERange.prototype._nativeRange = function (bounds){ 384 | var rng; 385 | if (this._el.tagName == 'INPUT'){ 386 | // IE 8 is very inconsistent; textareas have createTextRange but it doesn't work 387 | rng = this._el.createTextRange(); 388 | }else{ 389 | rng = this._doc.body.createTextRange (); 390 | rng.moveToElementText(this._el); 391 | } 392 | if (bounds){ 393 | if (bounds[1] < 0) bounds[1] = 0; // IE tends to run elements out of bounds 394 | if (bounds[0] > this.length()) bounds[0] = this.length(); 395 | if (bounds[1] < rng.text.replace(/\r/g, '').length){ // correct for IE's CrLf weirdness 396 | // block-display elements have an invisible, uncounted end of element marker, so we move an extra one and use the current length of the range 397 | rng.moveEnd ('character', -1); 398 | rng.moveEnd ('character', bounds[1]-rng.text.replace(/\r/g, '').length); 399 | } 400 | if (bounds[0] > 0) rng.moveStart('character', bounds[0]); 401 | } 402 | return rng; 403 | }; 404 | IERange.prototype._nativeSelect = function (rng){ 405 | rng.select(); 406 | }; 407 | IERange.prototype._nativeSelection = function (){ 408 | // returns [start, end] for the selection constrained to be in element 409 | var rng = this._nativeRange(); // range of the element to constrain to 410 | var len = this.length(); 411 | var sel = this._doc.selection.createRange(); 412 | try{ 413 | return [ 414 | iestart(sel, rng), 415 | ieend (sel, rng) 416 | ]; 417 | }catch (e){ 418 | // TODO: determine if this is still necessary, since we only call _nativeSelection if _el is active 419 | // IE gets upset sometimes about comparing text to input elements, but the selections cannot overlap, so make a best guess 420 | return (sel.parentElement().sourceIndex < this._el.sourceIndex) ? [0,0] : [len, len]; 421 | } 422 | }; 423 | IERange.prototype._nativeGetText = function (rng){ 424 | return rng.text; 425 | }; 426 | IERange.prototype._nativeSetText = function (text, rng){ 427 | rng.text = text; 428 | }; 429 | IERange.prototype._nativeEOL = function(){ 430 | if ('value' in this._el){ 431 | this.text('\n'); // for input and textarea, insert it straight 432 | }else{ 433 | this._nativeRange(this.bounds()).pasteHTML('\n
'); 434 | } 435 | }; 436 | IERange.prototype._nativeTop = function(rng){ 437 | var startrng = this._nativeRange([0,0]); 438 | return rng.boundingTop - startrng.boundingTop; 439 | } 440 | IERange.prototype._nativeWrap = function(n, rng) { 441 | // hacky to use string manipulation but I don't see another way to do it. 442 | var div = document.createElement('div'); 443 | div.appendChild(n); 444 | // insert the existing range HTML after the first tag 445 | var html = div.innerHTML.replace('><', '>'+rng.htmlText+'<'); 446 | rng.pasteHTML(html); 447 | }; 448 | 449 | // IE internals 450 | function iestart(rng, constraint){ 451 | // returns the position (in character) of the start of rng within constraint. If it's not in constraint, returns 0 if it's before, length if it's after 452 | var len = constraint.text.replace(/\r/g, '').length; // correct for IE's CrLf weirdness 453 | if (rng.compareEndPoints ('StartToStart', constraint) <= 0) return 0; // at or before the beginning 454 | if (rng.compareEndPoints ('StartToEnd', constraint) >= 0) return len; 455 | for (var i = 0; rng.compareEndPoints ('StartToStart', constraint) > 0; ++i, rng.moveStart('character', -1)); 456 | return i; 457 | } 458 | function ieend (rng, constraint){ 459 | // returns the position (in character) of the end of rng within constraint. If it's not in constraint, returns 0 if it's before, length if it's after 460 | var len = constraint.text.replace(/\r/g, '').length; // correct for IE's CrLf weirdness 461 | if (rng.compareEndPoints ('EndToEnd', constraint) >= 0) return len; // at or after the end 462 | if (rng.compareEndPoints ('EndToStart', constraint) <= 0) return 0; 463 | for (var i = 0; rng.compareEndPoints ('EndToStart', constraint) > 0; ++i, rng.moveEnd('character', -1)); 464 | return i; 465 | } 466 | 467 | // an input element in a standards document. "Native Range" is just the bounds array 468 | function InputRange(){} 469 | InputRange.prototype = new Range(); 470 | InputRange.prototype._nativeRange = function(bounds) { 471 | return bounds || [0, this.length()]; 472 | }; 473 | InputRange.prototype._nativeSelect = function (rng){ 474 | this._el.setSelectionRange(rng[0], rng[1]); 475 | }; 476 | InputRange.prototype._nativeSelection = function(){ 477 | return [this._el.selectionStart, this._el.selectionEnd]; 478 | }; 479 | InputRange.prototype._nativeGetText = function(rng){ 480 | return this._el.value.substring(rng[0], rng[1]); 481 | }; 482 | InputRange.prototype._nativeSetText = function(text, rng){ 483 | var val = this._el.value; 484 | this._el.value = val.substring(0, rng[0]) + text + val.substring(rng[1]); 485 | }; 486 | InputRange.prototype._nativeEOL = function(){ 487 | this.text('\n'); 488 | }; 489 | InputRange.prototype._nativeTop = function(rng){ 490 | // I can't remember where I found this clever hack to find the location of text in a text area 491 | var clone = this._el.cloneNode(true); 492 | clone.style.visibility = 'hidden'; 493 | clone.style.position = 'absolute'; 494 | this._el.parentNode.insertBefore(clone, this._el); 495 | clone.style.height = '1px'; 496 | clone.value = this._el.value.slice(0, rng[0]); 497 | var top = clone.scrollHeight; 498 | // this gives the bottom of the text, so we have to subtract the height of a single line 499 | clone.value = 'X'; 500 | top -= clone.scrollHeight; 501 | clone.parentNode.removeChild(clone); 502 | return top; 503 | } 504 | InputRange.prototype._nativeWrap = function() {throw new Error("Cannot wrap in a text element")}; 505 | 506 | function W3CRange(){} 507 | W3CRange.prototype = new Range(); 508 | W3CRange.prototype._nativeRange = function (bounds){ 509 | var rng = this._doc.createRange(); 510 | rng.selectNodeContents(this._el); 511 | if (bounds){ 512 | w3cmoveBoundary (rng, bounds[0], true, this._el); 513 | rng.collapse (true); 514 | w3cmoveBoundary (rng, bounds[1]-bounds[0], false, this._el); 515 | } 516 | return rng; 517 | }; 518 | W3CRange.prototype._nativeSelect = function (rng){ 519 | this._win.getSelection().removeAllRanges(); 520 | this._win.getSelection().addRange (rng); 521 | }; 522 | W3CRange.prototype._nativeSelection = function (){ 523 | // returns [start, end] for the selection constrained to be in element 524 | var rng = this._nativeRange(); // range of the element to constrain to 525 | if (this._win.getSelection().rangeCount == 0) return [this.length(), this.length()]; // append to the end 526 | var sel = this._win.getSelection().getRangeAt(0); 527 | return [ 528 | w3cstart(sel, rng), 529 | w3cend (sel, rng) 530 | ]; 531 | } 532 | W3CRange.prototype._nativeGetText = function (rng){ 533 | return String.prototype.slice.apply(this._el.textContent, this.bounds()); 534 | // return rng.toString(); // this fails in IE11 since it insists on inserting \r's before \n's in Ranges. node.textContent works as expected 535 | }; 536 | W3CRange.prototype._nativeSetText = function (text, rng){ 537 | rng.deleteContents(); 538 | rng.insertNode (this._doc.createTextNode(text)); 539 | if (canNormalize) this._el.normalize(); // merge the text with the surrounding text 540 | }; 541 | W3CRange.prototype._nativeEOL = function(){ 542 | var rng = this._nativeRange(this.bounds()); 543 | rng.deleteContents(); 544 | var br = this._doc.createElement('br'); 545 | br.setAttribute ('_moz_dirty', ''); // for Firefox 546 | rng.insertNode (br); 547 | rng.insertNode (this._doc.createTextNode('\n')); 548 | rng.collapse (false); 549 | }; 550 | W3CRange.prototype._nativeTop = function(rng){ 551 | if (this.length == 0) return 0; // no text, no scrolling 552 | if (rng.toString() == ''){ 553 | var textnode = this._doc.createTextNode('X'); 554 | rng.insertNode (textnode); 555 | } 556 | var startrng = this._nativeRange([0,1]); 557 | var top = rng.getBoundingClientRect().top - startrng.getBoundingClientRect().top; 558 | if (textnode) textnode.parentNode.removeChild(textnode); 559 | return top; 560 | } 561 | W3CRange.prototype._nativeWrap = function(n, rng) { 562 | rng.surroundContents(n); 563 | }; 564 | 565 | // W3C internals 566 | function nextnode (node, root){ 567 | // in-order traversal 568 | // we've already visited node, so get kids then siblings 569 | if (node.firstChild) return node.firstChild; 570 | if (node.nextSibling) return node.nextSibling; 571 | if (node===root) return null; 572 | while (node.parentNode){ 573 | // get uncles 574 | node = node.parentNode; 575 | if (node == root) return null; 576 | if (node.nextSibling) return node.nextSibling; 577 | } 578 | return null; 579 | } 580 | function w3cmoveBoundary (rng, n, bStart, el){ 581 | // move the boundary (bStart == true ? start : end) n characters forward, up to the end of element el. Forward only! 582 | // if the start is moved after the end, then an exception is raised 583 | if (n <= 0) return; 584 | var node = rng[bStart ? 'startContainer' : 'endContainer']; 585 | if (node.nodeType == 3){ 586 | // we may be starting somewhere into the text 587 | n += rng[bStart ? 'startOffset' : 'endOffset']; 588 | } 589 | while (node){ 590 | if (node.nodeType == 3){ 591 | var length = node.nodeValue.length; 592 | if (n <= length){ 593 | rng[bStart ? 'setStart' : 'setEnd'](node, n); 594 | // special case: if we end next to a
, include that node. 595 | if (n == length){ 596 | // skip past zero-length text nodes 597 | for (var next = nextnode (node, el); next && next.nodeType==3 && next.nodeValue.length == 0; next = nextnode(next, el)){ 598 | rng[bStart ? 'setStartAfter' : 'setEndAfter'](next); 599 | } 600 | if (next && next.nodeType == 1 && next.nodeName == "BR") rng[bStart ? 'setStartAfter' : 'setEndAfter'](next); 601 | } 602 | return; 603 | }else{ 604 | rng[bStart ? 'setStartAfter' : 'setEndAfter'](node); // skip past this one 605 | n -= length; // and eat these characters 606 | } 607 | } 608 | node = nextnode (node, el); 609 | } 610 | } 611 | var START_TO_START = 0; // from the w3c definitions 612 | var START_TO_END = 1; 613 | var END_TO_END = 2; 614 | var END_TO_START = 3; 615 | // from the Mozilla documentation, for range.compareBoundaryPoints(how, sourceRange) 616 | // -1, 0, or 1, indicating whether the corresponding boundary-point of range is respectively before, equal to, or after the corresponding boundary-point of sourceRange. 617 | // * Range.END_TO_END compares the end boundary-point of sourceRange to the end boundary-point of range. 618 | // * Range.END_TO_START compares the end boundary-point of sourceRange to the start boundary-point of range. 619 | // * Range.START_TO_END compares the start boundary-point of sourceRange to the end boundary-point of range. 620 | // * Range.START_TO_START compares the start boundary-point of sourceRange to the start boundary-point of range. 621 | function w3cstart(rng, constraint){ 622 | if (rng.compareBoundaryPoints (START_TO_START, constraint) <= 0) return 0; // at or before the beginning 623 | if (rng.compareBoundaryPoints (END_TO_START, constraint) >= 0) return constraint.toString().length; 624 | rng = rng.cloneRange(); // don't change the original 625 | rng.setEnd (constraint.endContainer, constraint.endOffset); // they now end at the same place 626 | return constraint.toString().replace(/\r/g, '').length - rng.toString().replace(/\r/g, '').length; 627 | } 628 | function w3cend (rng, constraint){ 629 | if (rng.compareBoundaryPoints (END_TO_END, constraint) >= 0) return constraint.toString().length; // at or after the end 630 | if (rng.compareBoundaryPoints (START_TO_END, constraint) <= 0) return 0; 631 | rng = rng.cloneRange(); // don't change the original 632 | rng.setStart (constraint.startContainer, constraint.startOffset); // they now start at the same place 633 | return rng.toString().replace(/\r/g, '').length; 634 | } 635 | 636 | function NothingRange(){} 637 | NothingRange.prototype = new Range(); 638 | NothingRange.prototype._nativeRange = function(bounds) { 639 | return bounds || [0,this.length()]; 640 | }; 641 | NothingRange.prototype._nativeSelect = function (rng){ // do nothing 642 | }; 643 | NothingRange.prototype._nativeSelection = function(){ 644 | return [0,0]; 645 | }; 646 | NothingRange.prototype._nativeGetText = function (rng){ 647 | return this._el[this._textProp].substring(rng[0], rng[1]); 648 | }; 649 | NothingRange.prototype._nativeSetText = function (text, rng){ 650 | var val = this._el[this._textProp]; 651 | this._el[this._textProp] = val.substring(0, rng[0]) + text + val.substring(rng[1]); 652 | }; 653 | NothingRange.prototype._nativeEOL = function(){ 654 | this.text('\n'); 655 | }; 656 | NothingRange.prototype._nativeTop = function(){ 657 | return 0; 658 | }; 659 | NothingRange.prototype._nativeWrap = function() {throw new Error("Wrapping not implemented")}; 660 | 661 | 662 | // data for elements, similar to jQuery data, but allows for monitoring with custom events 663 | var data = []; // to avoid attaching javascript objects to DOM elements, to avoid memory leaks 664 | bililiteRange.fn.data = function(){ 665 | var index = this.element().bililiteRangeData; 666 | if (index == undefined){ 667 | index = this.element().bililiteRangeData = data.length; 668 | data[index] = new Data(this); 669 | } 670 | return data[index]; 671 | } 672 | try { 673 | Object.defineProperty({},'foo',{}); // IE8 will throw an error 674 | var Data = function(rng) { 675 | // we use JSON.stringify to display the data values. To make some of those non-enumerable, we have to use properties 676 | Object.defineProperty(this, 'values', { 677 | value: {} 678 | }); 679 | Object.defineProperty(this, 'sourceRange', { 680 | value: rng 681 | }); 682 | Object.defineProperty(this, 'toJSON', { 683 | value: function(){ 684 | var ret = {}; 685 | for (var i in Data.prototype) if (i in this.values) ret[i] = this.values[i]; 686 | return ret; 687 | } 688 | }); 689 | // to display all the properties (not just those changed), use JSON.stringify(state.all) 690 | Object.defineProperty(this, 'all', { 691 | get: function(){ 692 | var ret = {}; 693 | for (var i in Data.prototype) ret[i] = this[i]; 694 | return ret; 695 | } 696 | }); 697 | } 698 | 699 | Data.prototype = {}; 700 | Object.defineProperty(Data.prototype, 'values', { 701 | value: {} 702 | }); 703 | Object.defineProperty(Data.prototype, 'monitored', { 704 | value: {} 705 | }); 706 | 707 | bililiteRange.data = function (name, newdesc){ 708 | newdesc = newdesc || {}; 709 | var desc = Object.getOwnPropertyDescriptor(Data.prototype, name) || {}; 710 | if ('enumerable' in newdesc) desc.enumerable = !!newdesc.enumerable; 711 | if (!('enumerable' in desc)) desc.enumerable = true; // default 712 | if ('value' in newdesc) Data.prototype.values[name] = newdesc.value; 713 | if ('monitored' in newdesc) Data.prototype.monitored[name] = newdesc.monitored; 714 | desc.configurable = true; 715 | desc.get = function (){ 716 | if (name in this.values) return this.values[name]; 717 | return Data.prototype.values[name]; 718 | }; 719 | desc.set = function (value){ 720 | this.values[name] = value; 721 | if (Data.prototype.monitored[name]) this.sourceRange.dispatch({ 722 | type: 'bililiteRangeData', 723 | bubbles: true, 724 | detail: {name: name, value: value} 725 | }); 726 | } 727 | Object.defineProperty(Data.prototype, name, desc); 728 | } 729 | }catch(err){ 730 | // if we can't set object property properties, just use old-fashioned properties 731 | Data = function(rng){ this.sourceRange = rng }; 732 | Data.prototype = {}; 733 | bililiteRange.data = function(name, newdesc){ 734 | if ('value' in newdesc) Data.prototype[name] = newdesc.value; 735 | } 736 | } 737 | 738 | })(); 739 | 740 | // Polyfill for forEach, per Mozilla documentation. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/forEach#Polyfill 741 | if (!Array.prototype.forEach) 742 | { 743 | Array.prototype.forEach = function(fun /*, thisArg */) 744 | { 745 | "use strict"; 746 | 747 | if (this === void 0 || this === null) 748 | throw new TypeError(); 749 | 750 | var t = Object(this); 751 | var len = t.length >>> 0; 752 | if (typeof fun !== "function") 753 | throw new TypeError(); 754 | 755 | var thisArg = arguments.length >= 2 ? arguments[1] : void 0; 756 | for (var i = 0; i < len; i++) 757 | { 758 | if (i in t) 759 | fun.call(thisArg, t[i], i, t); 760 | } 761 | }; 762 | } 763 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | // insert characters in a textarea or text input field 2 | // special characters are enclosed in {}; use {{} for the { character itself 3 | // documentation: http://bililite.com/blog/2008/08/20/the-fnsendkeys-plugin/ 4 | // Version: 4 5 | // Copyright (c) 2013 Daniel Wachsstock 6 | // MIT license: 7 | // Permission is hereby granted, free of charge, to any person 8 | // obtaining a copy of this software and associated documentation 9 | // files (the "Software"), to deal in the Software without 10 | // restriction, including without limitation the rights to use, 11 | // copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the 13 | // Software is furnished to do so, subject to the following 14 | // conditions: 15 | 16 | // The above copyright notice and this permission notice shall be 17 | // included in all copies or substantial portions of the Software. 18 | 19 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 20 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 21 | // OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 22 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 23 | // HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 24 | // WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 25 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 26 | // OTHER DEALINGS IN THE SOFTWARE. 27 | 28 | var jQuery = require('jquery'); 29 | var bililiteRange = require('./bililiteRange'); 30 | 31 | (function($){ 32 | 33 | $.fn.sendkeys = function (x){ 34 | x = x.replace(/([^{])\n/g, '$1{enter}'); // turn line feeds into explicit break insertions, but not if escaped 35 | return this.each( function(){ 36 | bililiteRange(this).bounds('selection').sendkeys(x).select(); 37 | this.focus(); 38 | }); 39 | }; // sendkeys 40 | 41 | // add a default handler for keydowns so that we can send keystrokes, even though code-generated events 42 | // are untrusted (http://www.w3.org/TR/DOM-Level-3-Events/#trusted-events) 43 | // documentation of special event handlers is at http://learn.jquery.com/events/event-extensions/ 44 | $.event.special.keydown = $.event.special.keydown || {}; 45 | $.event.special.keydown._default = function (evt){ 46 | if (evt.isTrusted) return false; 47 | if (evt.ctrlKey || evt.altKey || evt.metaKey) return false; // only deal with printable characters. This may be a false assumption 48 | if (evt.key == null) return false; // nothing to print. Use the keymap plugin to set this 49 | var target = evt.target; 50 | if (target.isContentEditable || target.nodeName == 'INPUT' || target.nodeName == 'TEXTAREA') { 51 | // only insert into editable elements 52 | var key = evt.key; 53 | if (key.length > 1 && key.charAt(0) != '{') key = '{'+key+'}'; // sendkeys notation 54 | $(target).sendkeys(key); 55 | return true; 56 | } 57 | return false; 58 | } 59 | })(jQuery) 60 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jquery-sendkeys", 3 | "version": "4.0.0", 4 | "description": "jQuery plugin for simulating key presses on HTML inputs", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "peerDependencies": { 10 | "jquery": "*" 11 | }, 12 | "keywords": [ 13 | "jquery", 14 | "input", 15 | "sendkeys", 16 | "keypress", 17 | "keyup", 18 | "keydown", 19 | "keyboard", 20 | "testing" 21 | ], 22 | "author": "Tim Macfarlane ", 23 | "license": "MIT", 24 | "repository": { 25 | "type": "git", 26 | "url": "https://github.com/featurist/jquery-sendkeys.git" 27 | }, 28 | "bugs": { 29 | "url": "https://github.com/featurist/jquery-sendkeys/issues" 30 | }, 31 | "homepage": "https://github.com/featurist/jquery-sendkeys" 32 | } 33 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # jQuery sendkeys 2 | 3 | This is an NPM and browserify compatible version of [jQuery.sendkeys](http://bililite.com/blog/2011/01/23/improved-sendkeys/). It's very useful for testing HTML pages by simulating key presses on text inputs and textareas. 4 | 5 | # how to use? 6 | 7 | var $ = require('jquery'); 8 | require('jquery-sendkeys'); 9 | 10 | $('input.search').sendkeys('something'); 11 | 12 | See full documentation [here](http://bililite.com/blog/2011/01/23/improved-sendkeys/). Original repo here: [https://github.com/dwachss/bililiteRange](https://github.com/dwachss/bililiteRange), v4 taken from commit [254894b971a73c80e12fab9be416c8ad1c689452](https://github.com/dwachss/bililiteRange/commit/254894b971a73c80e12fab9be416c8ad1c689452) 13 | --------------------------------------------------------------------------------