/* * $Id: documentselection.js 413 2008-05-16 21:30:59Z wingedfox $ * $HeadURL: https://svn.debugger.ru/repos/jslibs/BrowserExtensions/tags/BrowserExtensions.018/documentselection.js $ * * Class implements cross-browser work with text selection * * @author Ilya Lebedev * @author $Author: wingedfox $ * @modified $Date: 2008-05-17 01:30:59 +0400 (Сбт, 17 Май 2008) $ * @version $Rev: 413 $ * @license LGPL */ /* * @class DocumentSelection */ DocumentSelection = new function () { var self = this; /* * Stores hash of keys, applied to elements * * @type Object * @scope private */ var keys = { 'prevCalcNode' : '__prevCalcNode' } //--------------------------------------------------------------------------- // PRIVATES //--------------------------------------------------------------------------- /** * Calls specified method with the supplied params * This is done to process only correct requests * * @param {Function} method to call * @param {Array} arguments of [target, param1, paramN] * @return {Object} method call result or false, if any error happened * @scope private */ var callMethod = function (m, arg) { var el = arg[0] ,id ,module = "" if (!el || !el.tagName) return false; switch (arg[0].tagName.toLowerCase()) { case 'input': if (el.type && el.type != 'text' && el.type != 'password') return false; case 'textarea': module = "input"; break; case 'iframe': module = "frame"; arg[0] = el.contentWindow; break; default: return false; } /* * instantiate the module */ if ('function' == typeof self.module[module]) self.module[module] = new self.module[module](keys); /* * throw the exception, is method is not implemented */ if (!self.module[module] || !self.module[module][m]) throw new Error ('Method \''+m+'\' is not implemented for DocumentSelection \''+module+'\' module.'); return self.module[module][m].apply(self,arg); } /** * Keeps scrolling on the place for browsers, those don't support this natively * * @param {HTMLElement} el target element * @param {Number} ot old scrollTop property * @param {Number} ol old scrollLeft property * @scope private */ var keepScroll = function (el,ot,ol) { if (window.getSelection && 'iframe'!=el.tagName.toLowerCase()) { var q = self.getSelectionOffset(el) if (el.contentWindow) el = el.contentWindow.document.body; if (ot>q.y) el.scrollTop = q.y; else if (ot+el.clientHeight>q.y) el.scrollTop = ot; else el.scrollTop = q.y-el.clientHeight/2; if (ol>q.x) el.scrollLeft = q.x; else if (ol+el.clientWidth>q.x) el.scrollLeft = ol; else el.scrollLeft = q.x-el.clientWidth/2; } } //--------------------------------------------------------------------------- // SETTERS //--------------------------------------------------------------------------- /** * getSelectionRange wrapper/emulator * * @param {HTMLElement} * @param {Number} start position * @param {Number} end position * @param {Boolean} related indicates calculation of range relatively to current start point * @return void * @scope public */ self.setRange = function(el, start, end, related) { var ot = el.scrollTop ,ol = el.scrollLeft /* * set range on relative coordinates */ if (related) { var st = self.getStart(el); end = st+end; start = st+start; } if (start < 0) start = 0; if (end < start) end = start; callMethod ('setRange',[el,start,end]); keepScroll(el,ot,ol); } //--------------------------------------------------------------------------- // GETTERS //--------------------------------------------------------------------------- /** * Return contents of the current selection * * @param {HTMLElement} el element to look position on * @return {String} * @scope public */ self.getSelection = function(el) { return callMethod('getSelection',[el]); } /** * getSelectionStart wrapper/emulator * adapted version * * @param {HTMLElement} el element to calculate end position for * @return {Number} start position * @scope public */ self.getStart = function (el) { return callMethod('getPos',[el,true]); } /* * getSelectionEnd wrapper/emulator * adapted version * * @param {HTMLElement} el element to calculate end position for * @return {Number} start position * @scope public */ self.getEnd = function (el) { return callMethod('getPos',[el,false]); } /* * Return cursor position for supplied field * * @param {HTMLElement} element to get cursor position from * @return {Number} position * @scope public */ self.getCursorPosition = function (el) { return self.getStart(el); } //--------------------------------------------------------------------------- // MISC FUNCTIONS //--------------------------------------------------------------------------- /* * Insert text at cursor position * * @param {HTMLElement} text field to insert text * @param {String} text to insert * @scope public */ self.insertAtCursor = function (el, val, keep) { var ot = el.scrollTop ,ol = el.scrollLeft if (!keep) { callMethod('del',[el]); } var pos = callMethod('ins',[el,val]); keepScroll(el,ot,ol); return pos; } /* * Wraps selection with start and end text * * @param {HTMLElement} text field to insert text * @param {String} start text at the beginnging of the selection * @param {String} end text at the end of the selection * @scope public */ self.wrapSelection = function (el, start, end) { var s = self.getCursorPosition(el) ,e = self.getEnd(el) if (s==e) { self.insertAtCursor(el,start+end); } else { self.insertAtCursor(el,start,true); self.setRange(el,e+start.length,e+start.length); self.insertAtCursor(el,end,true); } } /* * Deletes char at cursor position * * @param {HTMLElement} text field to delete text * @param {Boolean} delete text before (backspace) or after (del) cursor * @scope public */ self.deleteAtCursor = function (el, after) { if (!self.getSelection(el)) { if (after) self.setRange(el,0,1,true); else self.setRange(el,-1,0,true); } return self.deleteSelection(el); } /** * Removes the selection, if available * * @param {HTMLElement} el field to delete text from * @scope public */ self.deleteSelection = function (el) { var ol = el.scrollLeft ,ot = el.scrollTop ,ret = callMethod('del',[el]); keepScroll(el,ot,ol); return ret; } /** * Method is used to caclulate pixel offsets for the selection in TextArea (other inputs are not tested yet) * * @param {HTMLTextareaElement} el target to calculate offsets * @return {Object} {x: horizontal offset, y: vertical offset, h: height offset} * @scope public */ self.getSelectionOffset = function (el) { return callMethod('getSelectionOffset',[el],true); } } DocumentSelection.module = { /** * Module processing selection in the 'input' and 'textarea' fields * * @param {Object} keys properties, registered for use in DS * @scope protected */ 'input' : function (keys) { var self=this; /** * Special document node, used to calculate range offsets in Mozilla * * @type HtmlDivElement * @scope private */ var offsetCalculator = null; /** * Returns selection start or end position in absolute chars from the field start * * @param {HTMLInputElement, HTMLTextareaElement} el input or textarea to get position from * @param {Boolean} start get start or end selection position * @return {Number} offset from the beginning * @scope private */ self.getPos = function (el, start) { var off; try { el.setActive(); if (start) off = Math.abs(el.document.selection.createRange().moveStart("character", -100000000)); else off = Math.abs(el.document.selection.createRange().moveEnd("character", -100000000)); /* * test for the TEXTAREA's dumb behavior */ if (el.tagName.toLowerCase() != 'input') { /* * calculate node offset */ var r = el.document.body.createTextRange(); r.moveToElementText(el); var sTest = Math.abs(r.moveStart("character", -100000000)); off -= sTest; } } catch (e) { try { off = (start?el.selectionStart:el.selectionEnd); } catch (e) { off = 0; } } return off; } /** * Removes the selection, if available * * @param {HTMLElement} el field to delete text from * @return {String} deleted substring * @scope public */ self.del = function (el) { var ret = "" ,s = self.getPos(el,true) ,e = self.getPos(el,false) if (s!=e) { /* * check for IE, because Opera does use \r\n sequence, but calculate positions correctly */ var tmp = document.selection&&!window.opera?el.value.replace(/\r/g,""):el.value; ret = tmp.substring(s,e); el.value = tmp.substring(0, s)+tmp.substring(e,tmp.length); self.setRange(el,s,s); } return ret; } /** * Inserts text to the textarea * * @param {HTMLElement} text field to insert text * @param {String} text to insert * @return {Number} new cursor position * @scope public */ self.ins = function (el,val) { var ret = "" ,s = self.getPos(el,true) /* * check for IE, because Opera does use \r\n sequence, but calculate positions correctly */ var tmp = document.selection&&!window.opera?el.value.replace(/\r/g,""):el.value; el.value = tmp.substring(0,s)+val+tmp.substring(s,tmp.length); s += val.length; self.setRange(el,s,s); return s; } /** * Return contents of the current selection * * @param {HTMLElement} el element to look position on * @param {Number} s start position * @param {Number} e end position * @return {String} * @scope public */ self.getSelection = function (el) { var s = self.getPos(el,true), e = self.getPos(el,false) /* * w/o this check content might be duplicated on delete */ if (e