Created
April 24, 2020 01:54
-
-
Save nopium/542430a6cb75b63e3d4ec849a84cc7d1 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // Cross-broswer implementation of text ranges and selections | |
| // documentation: http://bililite.com/blog/2011/01/17/cross-browser-text-ranges-and-selections/ | |
| // Version: 2.6 | |
| // Copyright (c) 2013 Daniel Wachsstock | |
| // MIT license: | |
| // Permission is hereby granted, free of charge, to any person | |
| // obtaining a copy of this software and associated documentation | |
| // files (the "Software"), to deal in the Software without | |
| // restriction, including without limitation the rights to use, | |
| // copy, modify, merge, publish, distribute, sublicense, and/or sell | |
| // copies of the Software, and to permit persons to whom the | |
| // Software is furnished to do so, subject to the following | |
| // conditions: | |
| // The above copyright notice and this permission notice shall be | |
| // included in all copies or substantial portions of the Software. | |
| // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, | |
| // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES | |
| // OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND | |
| // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT | |
| // HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, | |
| // WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING | |
| // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR | |
| // OTHER DEALINGS IN THE SOFTWARE. | |
| (function(){ | |
| // a bit of weirdness with IE11: using 'focus' is flaky, even if I'm not bubbling, as far as I can tell. | |
| var focusEvent = 'onfocusin' in document.createElement('input') ? 'focusin' : 'focus'; | |
| // IE11 normalize is buggy (http://connect.microsoft.com/IE/feedback/details/809424/node-normalize-removes-text-if-dashes-are-present) | |
| var n = document.createElement('div'); | |
| n.appendChild(document.createTextNode('x-')); | |
| n.appendChild(document.createTextNode('x')); | |
| n.normalize(); | |
| var canNormalize = n.firstChild.length == 3; | |
| bililiteRange = function(el, debug){ | |
| var ret; | |
| if (debug){ | |
| ret = new NothingRange(); // Easier to force it to use the no-selection type than to try to find an old browser | |
| }else if (window.getSelection && el.setSelectionRange){ | |
| // Standards. Element is an input or textarea | |
| // note that some input elements do not allow selections | |
| try{ | |
| el.selectionStart; // even getting the selection in such an element will throw | |
| ret = new InputRange(); | |
| }catch(e){ | |
| ret = new NothingRange(); | |
| } | |
| }else if (window.getSelection){ | |
| // Standards, with any other kind of element | |
| ret = new W3CRange(); | |
| }else if (document.selection){ | |
| // Internet Explorer | |
| ret = new IERange(); | |
| }else{ | |
| // doesn't support selection | |
| ret = new NothingRange(); | |
| } | |
| ret._el = el; | |
| // determine parent document, as implemented by John McLear <[email protected]> | |
| ret._doc = el.ownerDocument; | |
| ret._win = 'defaultView' in ret._doc ? ret._doc.defaultView : ret._doc.parentWindow; | |
| ret._textProp = textProp(el); | |
| ret._bounds = [0, ret.length()]; | |
| // There's no way to detect whether a focus event happened as a result of a click (which should change the selection) | |
| // 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 | |
| // in edge cases (right-clicks, drag-n-drop), and is vulnerable to a lower-down handler preventing bubbling. | |
| // I just don't know a better way. | |
| // I'll hack my event-listening code below, rather than create an entire new bilililiteRange, potentially before the DOM has loaded | |
| if (!('bililiteRangeMouseDown' in ret._doc)){ | |
| var _doc = {_el: ret._doc}; | |
| ret._doc.bililiteRangeMouseDown = false; | |
| bililiteRange.fn.listen.call(_doc, 'mousedown', function() { | |
| ret._doc.bililiteRangeMouseDown = true; | |
| }); | |
| bililiteRange.fn.listen.call(_doc, 'mouseup', function() { | |
| ret._doc.bililiteRangeMouseDown = false; | |
| }); | |
| } | |
| // note that bililiteRangeSelection is an array, which means that copying it only copies the address, which points to the original. | |
| // make sure that we never let it (always do return [bililiteRangeSelection[0], bililiteRangeSelection[1]]), which means never returning | |
| // this._bounds directly | |
| if (!('bililiteRangeSelection' in el)){ | |
| // start tracking the selection | |
| function trackSelection(evt){ | |
| if (evt && evt.which == 9){ | |
| // do tabs my way, by restoring the selection | |
| // there's a flash of the browser's selection, but I don't see a way of avoiding that | |
| ret._nativeSelect(ret._nativeRange(el.bililiteRangeSelection)); | |
| }else{ | |
| el.bililiteRangeSelection = ret._nativeSelection(); | |
| } | |
| } | |
| trackSelection(); | |
| // only IE does this right and allows us to grab the selection before blurring | |
| if ('onbeforedeactivate' in el){ | |
| ret.listen('beforedeactivate', trackSelection); | |
| }else{ | |
| // with standards-based browsers, have to listen for every user interaction | |
| ret.listen('mouseup', trackSelection).listen('keyup', trackSelection); | |
| } | |
| ret.listen(focusEvent, function(){ | |
| // restore the correct selection when the element comes into focus (mouse clicks change the position of the selection) | |
| // Note that Firefox will not fire the focus event until the window/tab is active even if el.focus() is called | |
| // https://bugzilla.mozilla.org/show_bug.cgi?id=566671 | |
| if (!ret._doc.bililiteRangeMouseDown){ | |
| ret._nativeSelect(ret._nativeRange(el.bililiteRangeSelection)); | |
| } | |
| }); | |
| } | |
| if (!('oninput' in el)){ | |
| // give IE8 a chance. Note that this still fails in IE11, which has has oninput on contenteditable elements but does not | |
| // dispatch input events. See http://connect.microsoft.com/IE/feedback/details/794285/ie10-11-input-event-does-not-fire-on-div-with-contenteditable-set | |
| // TODO: revisit this when I have IE11 running on my development machine | |
| // TODO: FIXED | |
| var inputhack = function() {ret.dispatch({type: 'input', bubbles: true}) }; | |
| if(typeof window.setTimeout == 'object'){ /* IE 8 sees `setTimeout` as an `object` and not a `function` */ | |
| ret.listen('keyup', inputhack); | |
| ret.listen('cut', inputhack); | |
| ret.listen('paste', inputhack); | |
| ret.listen('drop', inputhack); | |
| el.oninput = 'patched'; | |
| } | |
| }else{ | |
| /* | |
| IE9/IE11 supports the `textinput` event (even on contenteditable elements) | |
| See http://help.dottoro.com/ljhiwalm.php | |
| */ | |
| /* Detect IE 9/11, See: https://stackoverflow.com/questions/21825157/internet-explorer-11-detection */ | |
| if((!(window.FileReader) || !!window.MSInputMethodContext) && !!document.documentMode){ | |
| ret.listen('textinput', function(){ ret.dispatch({type: 'input', bubbles: true}); }); | |
| } | |
| } | |
| return ret; | |
| } | |
| function textProp(el){ | |
| // returns the property that contains the text of the element | |
| // note that for <body> elements the text attribute represents the obsolete text color, not the textContent. | |
| // we document that these routines do not work for <body> elements so that should not be relevant | |
| // Bugfix for https://github.com/dwachss/bililiteRange/issues/18 | |
| // Adding typeof check of string for el.value in case for li elements | |
| if (typeof el.value === 'string') return 'value'; | |
| if (typeof el.text != 'undefined') return 'text'; | |
| if (typeof el.textContent != 'undefined') return 'textContent'; | |
| return 'innerText'; | |
| } | |
| // base class | |
| function Range(){} | |
| Range.prototype = { | |
| length: function() { | |
| return this._el[this._textProp].replace(/\r/g, '').length; // need to correct for IE's CrLf weirdness | |
| }, | |
| bounds: function(s){ | |
| if (bililiteRange.bounds[s]){ | |
| this._bounds = bililiteRange.bounds[s].apply(this); | |
| }else if (s){ | |
| this._bounds = s; // don't do error checking now; things may change at a moment's notice | |
| }else{ | |
| var b = [ | |
| Math.max(0, Math.min (this.length(), this._bounds[0])), | |
| Math.max(0, Math.min (this.length(), this._bounds[1])) | |
| ]; | |
| b[1] = Math.max(b[0], b[1]); | |
| return b; // need to constrain it to fit | |
| } | |
| return this; // allow for chaining | |
| }, | |
| select: function(){ | |
| var b = this._el.bililiteRangeSelection = this.bounds(); | |
| if (this._el === this._doc.activeElement){ | |
| // only actually select if this element is active! | |
| this._nativeSelect(this._nativeRange(b)); | |
| } | |
| this.dispatch({type: 'select', bubbles: true}); | |
| return this; // allow for chaining | |
| }, | |
| text: function(text, select){ | |
| if (arguments.length){ | |
| var bounds = this.bounds(), el = this._el; | |
| // signal the input per DOM 3 input events, http://www.w3.org/TR/DOM-Level-3-Events/#h4_events-inputevents | |
| // we add another field, bounds, which are the bounds of the original text before being changed. | |
| this.dispatch({type: 'beforeinput', bubbles: true, | |
| data: text, bounds: bounds}); | |
| this._nativeSetText(text, this._nativeRange(bounds)); | |
| if (select == 'start'){ | |
| this.bounds ([bounds[0], bounds[0]]); | |
| }else if (select == 'end'){ | |
| this.bounds ([bounds[0]+text.length, bounds[0]+text.length]); | |
| }else if (select == 'all'){ | |
| this.bounds ([bounds[0], bounds[0]+text.length]); | |
| } | |
| this.dispatch({type: 'input', bubbles: true, | |
| data: text, bounds: bounds}); | |
| return this; // allow for chaining | |
| }else{ | |
| return this._nativeGetText(this._nativeRange(this.bounds())).replace(/\r/g, ''); // need to correct for IE's CrLf weirdness | |
| } | |
| }, | |
| insertEOL: function (){ | |
| this._nativeEOL(); | |
| this._bounds = [this._bounds[0]+1, this._bounds[0]+1]; // move past the EOL marker | |
| return this; | |
| }, | |
| sendkeys: function (text){ | |
| var self = this; | |
| this.data().sendkeysOriginalText = this.text(); | |
| this.data().sendkeysBounds = undefined; | |
| function simplechar (rng, c){ | |
| if (/^{[^}]*}$/.test(c)) c = c.slice(1,-1); // deal with unknown {key}s | |
| for (var i =0; i < c.length; ++i){ | |
| var x = c.charCodeAt(i); | |
| rng.dispatch({type: 'keypress', bubbles: true, keyCode: x, which: x, charCode: x}); | |
| } | |
| rng.text(c, 'end'); | |
| } | |
| text.replace(/{[^}]*}|[^{]+|{/g, function(part){ | |
| (bililiteRange.sendkeys[part] || simplechar)(self, part, simplechar); | |
| }); | |
| this.bounds(this.data().sendkeysBounds); | |
| this.dispatch({type: 'sendkeys', which: text}); | |
| return this; | |
| }, | |
| top: function(){ | |
| return this._nativeTop(this._nativeRange(this.bounds())); | |
| }, | |
| scrollIntoView: function(scroller){ | |
| var top = this.top(); | |
| // scroll into position if necessary | |
| if (this._el.scrollTop > top || this._el.scrollTop+this._el.clientHeight < top){ | |
| if (scroller){ | |
| scroller.call(this._el, top); | |
| }else{ | |
| this._el.scrollTop = top; | |
| } | |
| } | |
| return this; | |
| }, | |
| wrap: function (n){ | |
| this._nativeWrap(n, this._nativeRange(this.bounds())); | |
| return this; | |
| }, | |
| selection: function(text){ | |
| if (arguments.length){ | |
| return this.bounds('selection').text(text, 'end').select(); | |
| }else{ | |
| return this.bounds('selection').text(); | |
| } | |
| }, | |
| clone: function(){ | |
| return bililiteRange(this._el).bounds(this.bounds()); | |
| }, | |
| all: function(text){ | |
| if (arguments.length){ | |
| this.dispatch ({type: 'beforeinput', bubbles: true, data: text}); | |
| this._el[this._textProp] = text; | |
| this.dispatch ({type: 'input', bubbles: true, data: text}); | |
| return this; | |
| }else{ | |
| return this._el[this._textProp].replace(/\r/g, ''); // need to correct for IE's CrLf weirdness | |
| } | |
| }, | |
| element: function() { return this._el }, | |
| // includes a quickie polyfill for CustomEvent for IE that isn't perfect but works for me | |
| // IE10 allows custom events but not "new CustomEvent"; have to do it the old-fashioned way | |
| dispatch: function(opts){ | |
| opts = opts || {}; | |
| var event = document.createEvent ? document.createEvent('CustomEvent') : this._doc.createEventObject(); | |
| event.initCustomEvent && event.initCustomEvent(opts.type, !!opts.bubbles, !!opts.cancelable, opts.detail); | |
| for (var key in opts) event[key] = opts[key]; | |
| // dispatch event asynchronously (in the sense of on the next turn of the event loop; still should be fired in order of dispatch | |
| var el = this._el; | |
| setTimeout(function(){ | |
| try { | |
| el.dispatchEvent ? el.dispatchEvent(event) : el.fireEvent("on" + opts.type, document.createEventObject()); | |
| }catch(e){ | |
| // IE8 will not let me fire custom events at all. Call them directly | |
| var listeners = el['listen'+opts.type]; | |
| if (listeners) for (var i = 0; i < listeners.length; ++i){ | |
| listeners[i].call(el, event); | |
| } | |
| } | |
| }, 0); | |
| return this; | |
| }, | |
| listen: function (type, func){ | |
| var el = this._el; | |
| if (el.addEventListener){ | |
| el.addEventListener(type, func); | |
| }else{ | |
| el.attachEvent("on" + type, func); | |
| // IE8 can't even handle custom events created with createEventObject (though it permits attachEvent), so we have to make our own | |
| var listeners = el['listen'+type] = el['listen'+type] || []; | |
| listeners.push(func); | |
| } | |
| return this; | |
| }, | |
| dontlisten: function (type, func){ | |
| var el = this._el; | |
| if (el.removeEventListener){ | |
| el.removeEventListener(type, func); | |
| }else try{ | |
| el.detachEvent("on" + type, func); | |
| }catch(e){ | |
| var listeners = el['listen'+type]; | |
| if (listeners) for (var i = 0; i < listeners.length; ++i){ | |
| if (listeners[i] === func) listeners[i] = function(){}; // replace with a noop | |
| } | |
| } | |
| return this; | |
| } | |
| }; | |
| // allow extensions ala jQuery | |
| bililiteRange.fn = Range.prototype; // to allow monkey patching | |
| bililiteRange.extend = function(fns){ | |
| for (fn in fns) Range.prototype[fn] = fns[fn]; | |
| }; | |
| //bounds functions | |
| bililiteRange.bounds = { | |
| all: function() { return [0, this.length()] }, | |
| start: function () { return [0,0] }, | |
| end: function () { return [this.length(), this.length()] }, | |
| selection: function(){ | |
| if (this._el === this._doc.activeElement){ | |
| this.bounds ('all'); // first select the whole thing for constraining | |
| return this._nativeSelection(); | |
| }else{ | |
| return this._el.bililiteRangeSelection; | |
| } | |
| } | |
| }; | |
| // sendkeys functions | |
| bililiteRange.sendkeys = { | |
| '{enter}': function (rng){ | |
| rng.dispatch({type: 'keypress', bubbles: true, keyCode: '\n', which: '\n', charCode: '\n'}); | |
| rng.insertEOL(); | |
| }, | |
| '{tab}': function (rng, c, simplechar){ | |
| simplechar(rng, '\t'); // useful for inserting what would be whitespace | |
| }, | |
| '{newline}': function (rng, c, simplechar){ | |
| simplechar(rng, '\n'); // useful for inserting what would be whitespace (and if I don't want to use insertEOL, which does some fancy things) | |
| }, | |
| '{backspace}': function (rng){ | |
| var b = rng.bounds(); | |
| 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 | |
| rng.text('', 'end'); // delete the characters and update the selection | |
| }, | |
| '{del}': function (rng){ | |
| var b = rng.bounds(); | |
| 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 | |
| rng.text('', 'end'); // delete the characters and update the selection | |
| }, | |
| '{rightarrow}': function (rng){ | |
| var b = rng.bounds(); | |
| if (b[0] == b[1]) ++b[1]; // no characters selected; it's just an insertion point. Move to the right | |
| rng.bounds([b[1], b[1]]); | |
| }, | |
| '{leftarrow}': function (rng){ | |
| var b = rng.bounds(); | |
| if (b[0] == b[1]) --b[0]; // no characters selected; it's just an insertion point. Move to the left | |
| rng.bounds([b[0], b[0]]); | |
| }, | |
| '{selectall}' : function (rng){ | |
| rng.bounds('all'); | |
| }, | |
| '{selection}': function (rng){ | |
| // insert the characters without the sendkeys processing | |
| var s = rng.data().sendkeysOriginalText; | |
| for (var i =0; i < s.length; ++i){ | |
| var x = s.charCodeAt(i); | |
| rng.dispatch({type: 'keypress', bubbles: true, keyCode: x, which: x, charCode: x}); | |
| } | |
| rng.text(s, 'end'); | |
| }, | |
| '{mark}' : function (rng){ | |
| rng.data().sendkeysBounds = rng.bounds(); | |
| } | |
| }; | |
| // Synonyms from the proposed DOM standard (http://www.w3.org/TR/DOM-Level-3-Events-key/) | |
| bililiteRange.sendkeys['{Enter}'] = bililiteRange.sendkeys['{enter}']; | |
| bililiteRange.sendkeys['{Backspace}'] = bililiteRange.sendkeys['{backspace}']; | |
| bililiteRange.sendkeys['{Delete}'] = bililiteRange.sendkeys['{del}']; | |
| bililiteRange.sendkeys['{ArrowRight}'] = bililiteRange.sendkeys['{rightarrow}']; | |
| bililiteRange.sendkeys['{ArrowLeft}'] = bililiteRange.sendkeys['{leftarrow}']; | |
| function IERange(){} | |
| IERange.prototype = new Range(); | |
| IERange.prototype._nativeRange = function (bounds){ | |
| var rng; | |
| if (this._el.tagName == 'INPUT'){ | |
| // IE 8 is very inconsistent; textareas have createTextRange but it doesn't work | |
| rng = this._el.createTextRange(); | |
| }else{ | |
| rng = this._doc.body.createTextRange (); | |
| rng.moveToElementText(this._el); | |
| } | |
| if (bounds){ | |
| if (bounds[1] < 0) bounds[1] = 0; // IE tends to run elements out of bounds | |
| if (bounds[0] > this.length()) bounds[0] = this.length(); | |
| if (bounds[1] < rng.text.replace(/\r/g, '').length){ // correct for IE's CrLf weirdness | |
| // 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 | |
| rng.moveEnd ('character', -1); | |
| rng.moveEnd ('character', bounds[1]-rng.text.replace(/\r/g, '').length); | |
| } | |
| if (bounds[0] > 0) rng.moveStart('character', bounds[0]); | |
| } | |
| return rng; | |
| }; | |
| IERange.prototype._nativeSelect = function (rng){ | |
| rng.select(); | |
| }; | |
| IERange.prototype._nativeSelection = function (){ | |
| // returns [start, end] for the selection constrained to be in element | |
| var rng = this._nativeRange(); // range of the element to constrain to | |
| var len = this.length(); | |
| var sel = this._doc.selection.createRange(); | |
| try{ | |
| return [ | |
| iestart(sel, rng), | |
| ieend (sel, rng) | |
| ]; | |
| }catch (e){ | |
| // TODO: determine if this is still necessary, since we only call _nativeSelection if _el is active | |
| // IE gets upset sometimes about comparing text to input elements, but the selections cannot overlap, so make a best guess | |
| return (sel.parentElement().sourceIndex < this._el.sourceIndex) ? [0,0] : [len, len]; | |
| } | |
| }; | |
| IERange.prototype._nativeGetText = function (rng){ | |
| return rng.text; | |
| }; | |
| IERange.prototype._nativeSetText = function (text, rng){ | |
| rng.text = text; | |
| }; | |
| IERange.prototype._nativeEOL = function(){ | |
| if ('value' in this._el){ | |
| this.text('\n'); // for input and textarea, insert it straight | |
| }else{ | |
| this._nativeRange(this.bounds()).pasteHTML('\n<br/>'); | |
| } | |
| }; | |
| IERange.prototype._nativeTop = function(rng){ | |
| var startrng = this._nativeRange([0,0]); | |
| return rng.boundingTop - startrng.boundingTop; | |
| } | |
| IERange.prototype._nativeWrap = function(n, rng) { | |
| // hacky to use string manipulation but I don't see another way to do it. | |
| var div = document.createElement('div'); | |
| div.appendChild(n); | |
| // insert the existing range HTML after the first tag | |
| var html = div.innerHTML.replace('><', '>'+rng.htmlText+'<'); | |
| rng.pasteHTML(html); | |
| }; | |
| // IE internals | |
| function iestart(rng, constraint){ | |
| // 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 | |
| var len = constraint.text.replace(/\r/g, '').length; // correct for IE's CrLf weirdness | |
| if (rng.compareEndPoints ('StartToStart', constraint) <= 0) return 0; // at or before the beginning | |
| if (rng.compareEndPoints ('StartToEnd', constraint) >= 0) return len; | |
| for (var i = 0; rng.compareEndPoints ('StartToStart', constraint) > 0; ++i, rng.moveStart('character', -1)); | |
| return i; | |
| } | |
| function ieend (rng, constraint){ | |
| // 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 | |
| var len = constraint.text.replace(/\r/g, '').length; // correct for IE's CrLf weirdness | |
| if (rng.compareEndPoints ('EndToEnd', constraint) >= 0) return len; // at or after the end | |
| if (rng.compareEndPoints ('EndToStart', constraint) <= 0) return 0; | |
| for (var i = 0; rng.compareEndPoints ('EndToStart', constraint) > 0; ++i, rng.moveEnd('character', -1)); | |
| return i; | |
| } | |
| // an input element in a standards document. "Native Range" is just the bounds array | |
| function InputRange(){} | |
| InputRange.prototype = new Range(); | |
| InputRange.prototype._nativeRange = function(bounds) { | |
| return bounds || [0, this.length()]; | |
| }; | |
| InputRange.prototype._nativeSelect = function (rng){ | |
| this._el.setSelectionRange(rng[0], rng[1]); | |
| }; | |
| InputRange.prototype._nativeSelection = function(){ | |
| return [this._el.selectionStart, this._el.selectionEnd]; | |
| }; | |
| InputRange.prototype._nativeGetText = function(rng){ | |
| return this._el.value.substring(rng[0], rng[1]); | |
| }; | |
| InputRange.prototype._nativeSetText = function(text, rng){ | |
| var val = this._el.value; | |
| this._el.value = val.substring(0, rng[0]) + text + val.substring(rng[1]); | |
| }; | |
| InputRange.prototype._nativeEOL = function(){ | |
| this.text('\n'); | |
| }; | |
| InputRange.prototype._nativeTop = function(rng){ | |
| // I can't remember where I found this clever hack to find the location of text in a text area | |
| var clone = this._el.cloneNode(true); | |
| clone.style.visibility = 'hidden'; | |
| clone.style.position = 'absolute'; | |
| this._el.parentNode.insertBefore(clone, this._el); | |
| clone.style.height = '1px'; | |
| clone.value = this._el.value.slice(0, rng[0]); | |
| var top = clone.scrollHeight; | |
| // this gives the bottom of the text, so we have to subtract the height of a single line | |
| clone.value = 'X'; | |
| top -= clone.scrollHeight; | |
| clone.parentNode.removeChild(clone); | |
| return top; | |
| } | |
| InputRange.prototype._nativeWrap = function() {throw new Error("Cannot wrap in a text element")}; | |
| function W3CRange(){} | |
| W3CRange.prototype = new Range(); | |
| W3CRange.prototype._nativeRange = function (bounds){ | |
| var rng = this._doc.createRange(); | |
| rng.selectNodeContents(this._el); | |
| if (bounds){ | |
| w3cmoveBoundary (rng, bounds[0], true, this._el); | |
| rng.collapse (true); | |
| w3cmoveBoundary (rng, bounds[1]-bounds[0], false, this._el); | |
| } | |
| return rng; | |
| }; | |
| W3CRange.prototype._nativeSelect = function (rng){ | |
| this._win.getSelection().removeAllRanges(); | |
| this._win.getSelection().addRange (rng); | |
| }; | |
| W3CRange.prototype._nativeSelection = function (){ | |
| // returns [start, end] for the selection constrained to be in element | |
| var rng = this._nativeRange(); // range of the element to constrain to | |
| if (this._win.getSelection().rangeCount == 0) return [this.length(), this.length()]; // append to the end | |
| var sel = this._win.getSelection().getRangeAt(0); | |
| return [ | |
| w3cstart(sel, rng), | |
| w3cend (sel, rng) | |
| ]; | |
| } | |
| W3CRange.prototype._nativeGetText = function (rng){ | |
| return String.prototype.slice.apply(this._el.textContent, this.bounds()); | |
| // return rng.toString(); // this fails in IE11 since it insists on inserting \r's before \n's in Ranges. node.textContent works as expected | |
| }; | |
| W3CRange.prototype._nativeSetText = function (text, rng){ | |
| rng.deleteContents(); | |
| rng.insertNode (this._doc.createTextNode(text)); | |
| if (canNormalize) this._el.normalize(); // merge the text with the surrounding text | |
| }; | |
| W3CRange.prototype._nativeEOL = function(){ | |
| var rng = this._nativeRange(this.bounds()); | |
| rng.deleteContents(); | |
| var br = this._doc.createElement('br'); | |
| br.setAttribute ('_moz_dirty', ''); // for Firefox | |
| rng.insertNode (br); | |
| rng.insertNode (this._doc.createTextNode('\n')); | |
| rng.collapse (false); | |
| }; | |
| W3CRange.prototype._nativeTop = function(rng){ | |
| if (this.length == 0) return 0; // no text, no scrolling | |
| if (rng.toString() == ''){ | |
| var textnode = this._doc.createTextNode('X'); | |
| rng.insertNode (textnode); | |
| } | |
| var startrng = this._nativeRange([0,1]); | |
| var top = rng.getBoundingClientRect().top - startrng.getBoundingClientRect().top; | |
| if (textnode) textnode.parentNode.removeChild(textnode); | |
| return top; | |
| } | |
| W3CRange.prototype._nativeWrap = function(n, rng) { | |
| rng.surroundContents(n); | |
| }; | |
| // W3C internals | |
| function nextnode (node, root){ | |
| // in-order traversal | |
| // we've already visited node, so get kids then siblings | |
| if (node.firstChild) return node.firstChild; | |
| if (node.nextSibling) return node.nextSibling; | |
| if (node===root) return null; | |
| while (node.parentNode){ | |
| // get uncles | |
| node = node.parentNode; | |
| if (node == root) return null; | |
| if (node.nextSibling) return node.nextSibling; | |
| } | |
| return null; | |
| } | |
| function w3cmoveBoundary (rng, n, bStart, el){ | |
| // move the boundary (bStart == true ? start : end) n characters forward, up to the end of element el. Forward only! | |
| // if the start is moved after the end, then an exception is raised | |
| if (n <= 0) return; | |
| var node = rng[bStart ? 'startContainer' : 'endContainer']; | |
| if (node.nodeType == 3){ | |
| // we may be starting somewhere into the text | |
| n += rng[bStart ? 'startOffset' : 'endOffset']; | |
| } | |
| while (node){ | |
| if (node.nodeType == 3){ | |
| var length = node.nodeValue.length; | |
| if (n <= length){ | |
| rng[bStart ? 'setStart' : 'setEnd'](node, n); | |
| // special case: if we end next to a <br>, include that node. | |
| if (n == length){ | |
| // skip past zero-length text nodes | |
| for (var next = nextnode (node, el); next && next.nodeType==3 && next.nodeValue.length == 0; next = nextnode(next, el)){ | |
| rng[bStart ? 'setStartAfter' : 'setEndAfter'](next); | |
| } | |
| if (next && next.nodeType == 1 && next.nodeName == "BR") rng[bStart ? 'setStartAfter' : 'setEndAfter'](next); | |
| } | |
| return; | |
| }else{ | |
| rng[bStart ? 'setStartAfter' : 'setEndAfter'](node); // skip past this one | |
| n -= length; // and eat these characters | |
| } | |
| } | |
| node = nextnode (node, el); | |
| } | |
| } | |
| var START_TO_START = 0; // from the w3c definitions | |
| var START_TO_END = 1; | |
| var END_TO_END = 2; | |
| var END_TO_START = 3; | |
| // from the Mozilla documentation, for range.compareBoundaryPoints(how, sourceRange) | |
| // -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. | |
| // * Range.END_TO_END compares the end boundary-point of sourceRange to the end boundary-point of range. | |
| // * Range.END_TO_START compares the end boundary-point of sourceRange to the start boundary-point of range. | |
| // * Range.START_TO_END compares the start boundary-point of sourceRange to the end boundary-point of range. | |
| // * Range.START_TO_START compares the start boundary-point of sourceRange to the start boundary-point of range. | |
| function w3cstart(rng, constraint){ | |
| if (rng.compareBoundaryPoints (START_TO_START, constraint) <= 0) return 0; // at or before the beginning | |
| if (rng.compareBoundaryPoints (END_TO_START, constraint) >= 0) return constraint.toString().length; | |
| rng = rng.cloneRange(); // don't change the original | |
| rng.setEnd (constraint.endContainer, constraint.endOffset); // they now end at the same place | |
| return constraint.toString().replace(/\r/g, '').length - rng.toString().replace(/\r/g, '').length; | |
| } | |
| function w3cend (rng, constraint){ | |
| if (rng.compareBoundaryPoints (END_TO_END, constraint) >= 0) return constraint.toString().length; // at or after the end | |
| if (rng.compareBoundaryPoints (START_TO_END, constraint) <= 0) return 0; | |
| rng = rng.cloneRange(); // don't change the original | |
| rng.setStart (constraint.startContainer, constraint.startOffset); // they now start at the same place | |
| return rng.toString().replace(/\r/g, '').length; | |
| } | |
| function NothingRange(){} | |
| NothingRange.prototype = new Range(); | |
| NothingRange.prototype._nativeRange = function(bounds) { | |
| return bounds || [0,this.length()]; | |
| }; | |
| NothingRange.prototype._nativeSelect = function (rng){ // do nothing | |
| }; | |
| NothingRange.prototype._nativeSelection = function(){ | |
| return [0,0]; | |
| }; | |
| NothingRange.prototype._nativeGetText = function (rng){ | |
| return this._el[this._textProp].substring(rng[0], rng[1]); | |
| }; | |
| NothingRange.prototype._nativeSetText = function (text, rng){ | |
| var val = this._el[this._textProp]; | |
| this._el[this._textProp] = val.substring(0, rng[0]) + text + val.substring(rng[1]); | |
| }; | |
| NothingRange.prototype._nativeEOL = function(){ | |
| this.text('\n'); | |
| }; | |
| NothingRange.prototype._nativeTop = function(){ | |
| return 0; | |
| }; | |
| NothingRange.prototype._nativeWrap = function() {throw new Error("Wrapping not implemented")}; | |
| // data for elements, similar to jQuery data, but allows for monitoring with custom events | |
| var data = []; // to avoid attaching javascript objects to DOM elements, to avoid memory leaks | |
| bililiteRange.fn.data = function(){ | |
| var index = this.element().bililiteRangeData; | |
| if (index == undefined){ | |
| index = this.element().bililiteRangeData = data.length; | |
| data[index] = new Data(this); | |
| } | |
| return data[index]; | |
| } | |
| try { | |
| Object.defineProperty({},'foo',{}); // IE8 will throw an error | |
| var Data = function(rng) { | |
| // we use JSON.stringify to display the data values. To make some of those non-enumerable, we have to use properties | |
| Object.defineProperty(this, 'values', { | |
| value: {} | |
| }); | |
| Object.defineProperty(this, 'sourceRange', { | |
| value: rng | |
| }); | |
| Object.defineProperty(this, 'toJSON', { | |
| value: function(){ | |
| var ret = {}; | |
| for (var i in Data.prototype) if (i in this.values) ret[i] = this.values[i]; | |
| return ret; | |
| } | |
| }); | |
| // to display all the properties (not just those changed), use JSON.stringify(state.all) | |
| Object.defineProperty(this, 'all', { | |
| get: function(){ | |
| var ret = {}; | |
| for (var i in Data.prototype) ret[i] = this[i]; | |
| return ret; | |
| } | |
| }); | |
| } | |
| Data.prototype = {}; | |
| Object.defineProperty(Data.prototype, 'values', { | |
| value: {} | |
| }); | |
| Object.defineProperty(Data.prototype, 'monitored', { | |
| value: {} | |
| }); | |
| bililiteRange.data = function (name, newdesc){ | |
| newdesc = newdesc || {}; | |
| var desc = Object.getOwnPropertyDescriptor(Data.prototype, name) || {}; | |
| if ('enumerable' in newdesc) desc.enumerable = !!newdesc.enumerable; | |
| if (!('enumerable' in desc)) desc.enumerable = true; // default | |
| if ('value' in newdesc) Data.prototype.values[name] = newdesc.value; | |
| if ('monitored' in newdesc) Data.prototype.monitored[name] = newdesc.monitored; | |
| desc.configurable = true; | |
| desc.get = function (){ | |
| if (name in this.values) return this.values[name]; | |
| return Data.prototype.values[name]; | |
| }; | |
| desc.set = function (value){ | |
| this.values[name] = value; | |
| if (Data.prototype.monitored[name]) this.sourceRange.dispatch({ | |
| type: 'bililiteRangeData', | |
| bubbles: true, | |
| detail: {name: name, value: value} | |
| }); | |
| } | |
| Object.defineProperty(Data.prototype, name, desc); | |
| } | |
| }catch(err){ | |
| // if we can't set object property properties, just use old-fashioned properties | |
| Data = function(rng){ this.sourceRange = rng }; | |
| Data.prototype = {}; | |
| bililiteRange.data = function(name, newdesc){ | |
| if ('value' in newdesc) Data.prototype[name] = newdesc.value; | |
| } | |
| } | |
| })(); | |
| // Polyfill for forEach, per Mozilla documentation. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/forEach#Polyfill | |
| if (!Array.prototype.forEach) | |
| { | |
| Array.prototype.forEach = function(fun /*, thisArg */) | |
| { | |
| "use strict"; | |
| if (this === void 0 || this === null) | |
| throw new TypeError(); | |
| var t = Object(this); | |
| var len = t.length >>> 0; | |
| if (typeof fun !== "function") | |
| throw new TypeError(); | |
| var thisArg = arguments.length >= 2 ? arguments[1] : void 0; | |
| for (var i = 0; i < len; i++) | |
| { | |
| if (i in t) | |
| fun.call(thisArg, t[i], i, t); | |
| } | |
| }; | |
| } | |
| // insert characters in a textarea or text input field | |
| // special characters are enclosed in {}; use {{} for the { character itself | |
| // documentation: http://bililite.com/blog/2008/08/20/the-fnsendkeys-plugin/ | |
| // Version: 4 | |
| // Copyright (c) 2013 Daniel Wachsstock | |
| // MIT license: | |
| // Permission is hereby granted, free of charge, to any person | |
| // obtaining a copy of this software and associated documentation | |
| // files (the "Software"), to deal in the Software without | |
| // restriction, including without limitation the rights to use, | |
| // copy, modify, merge, publish, distribute, sublicense, and/or sell | |
| // copies of the Software, and to permit persons to whom the | |
| // Software is furnished to do so, subject to the following | |
| // conditions: | |
| // The above copyright notice and this permission notice shall be | |
| // included in all copies or substantial portions of the Software. | |
| // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, | |
| // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES | |
| // OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND | |
| // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT | |
| // HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, | |
| // WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING | |
| // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR | |
| // OTHER DEALINGS IN THE SOFTWARE. | |
| initSendKeys = function($){ | |
| $.fn.sendkeys = function (x){ | |
| x = x.replace(/([^{])\n/g, '$1{enter}'); // turn line feeds into explicit break insertions, but not if escaped | |
| return this.each( function(){ | |
| bililiteRange(this).bounds('selection').sendkeys(x).select(); | |
| this.focus(); | |
| }); | |
| }; // sendkeys | |
| // add a default handler for keydowns so that we can send keystrokes, even though code-generated events | |
| // are untrusted (http://www.w3.org/TR/DOM-Level-3-Events/#trusted-events) | |
| // documentation of special event handlers is at http://learn.jquery.com/events/event-extensions/ | |
| $.event.special.keydown = $.event.special.keydown || {}; | |
| $.event.special.keydown._default = function (evt){ | |
| if (evt.isTrusted) return false; | |
| if (evt.ctrlKey || evt.altKey || evt.metaKey) return false; // only deal with printable characters. This may be a false assumption | |
| if (evt.key == null) return false; // nothing to print. Use the keymap plugin to set this | |
| var target = evt.target; | |
| if (target.isContentEditable || target.nodeName == 'INPUT' || target.nodeName == 'TEXTAREA') { | |
| // only insert into editable elements | |
| var key = evt.key; | |
| if (key.length > 1 && key.charAt(0) != '{') key = '{'+key+'}'; // sendkeys notation | |
| $(target).sendkeys(key); | |
| return true; | |
| } | |
| return false; | |
| } | |
| }; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment