codekasten/fotokiosk/fotokiste/static/virtual_keyboard/extensions/documentselection.js
2008-08-23 12:31:08 +00:00

695 lines
26 KiB
JavaScript

/*
* $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<s) e = s;
/*
* 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;
return tmp.substring(s,e);
}
/**
* Sets the selection range
*
* @param {HTMLElement}
* @param {Number} start position
* @param {Number} end position
* @return void
* @scope public
*/
self.setRange = function (el,start,end) {
if ('function' == typeof el.setSelectionRange) {
/*
* for Mozilla
*/
try {el.setSelectionRange(start, end)} catch (e) {}
} else {
/*
* for IE
*/
var range;
/*
* just try to create a range....
*/
try {
range = el.createTextRange();
} catch(e) {
try {
range = el.document.body.createTextRange();
range.moveToElementText(el);
} catch(e) {
return false;
}
}
el.focus();
range.collapse(true);
range.moveStart("character", start);
range.moveEnd("character", end - start);
range.select();
}
}
/**
* 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) {
var range;
if ('function' == typeof el.setSelectionRange) {
/*
* For Mozilla
*/
if (!offsetCalculator) {
/*
* create hidden div, which will 'emulate' the textarea
* it's put 'below the ground', because toggling block/none is too expensive
*/
offsetCalculator = document.createElement('td');
document.body.appendChild(offsetCalculator);
}
/*
* store the reference to last-checked object, to prevent recalculation of styles
*/
if (offsetCalculator[keys.prevCalcNode] != el) {
offsetCalculator[keys.prevCalcNode] = el;
var cs = document.defaultView.getComputedStyle(el, null);
for (var i in cs) {
try {if (cs[i]) offsetCalculator.style[i] = cs[i];}catch(e){}
}
offsetCalculator.style.overflow = 'auto';
offsetCalculator.style.position = 'absolute';
offsetCalculator.style.visibility = 'hidden';
offsetCalculator.style.zIndex = '-10';
offsetCalculator.style.left="-10000px";
offsetCalculator.style.top="-10000px";
offsetCalculator.style.backgroundColor = 'yellow';
}
/*
* caclulate offsets to target and move div right below it
*/
var range = document.createRange()
,val = el.value || " ";
if ('input'==el.tagName.toLowerCase()) {
offsetCalculator.style.width = 'auto'
offsetCalculator.style.whiteSpace = 'nowrap';
} else {
offsetCalculator.style.whiteSpace = 'off'==el.getAttribute('wrap')?"pre":"";
}
val = val.replace(/\x20\x20/g,"\x20\xa0").replace(/</g,"&lt;").replace(/>/g,"&gt");
offsetCalculator.innerHTML = ( val.substring(0,el.selectionStart-1)+"<span>"+val.substring(el.selectionStart-1,el.selectionStart)+"</span>"
+val.substring(el.selectionStart)).replace(/\n/g,"<br />")
.replace(/\t/g,"<em style=\"white-space:pre\">\t</em>")
/*
* span is used to find the offsets
*/
var span = offsetCalculator.getElementsByTagName('span')[0];
span.style.border = '1px solid red';
range.offsetLeft = span.offsetLeft// - el.scrollLeft + span.clientWidth;
range.offsetTop = span.offsetTop// - el.scrollTop;
range.offsetHeight = span.offsetHeight;
if ("\n"==val.charAt(el.selectionStart-1)) range.offsetTop += range.offsetHeight*2;
span = null;
} else if (document.selection && document.selection.createRange) {
/*
* For IE
*/
range = document.selection.createRange();
/*
* IE does not allow to calculate lineHeight, but this check is easy
*/
range.offsetHeight = Math.round(range.boundingHeight/(range.text.replace(/[^\n]/g,"").length+1));
if (el.tagName && 'textarea'==el.tagName.toLowerCase()) {
var xy = DOM.getOffset(el)
range = {
'offsetTop' : range.offsetTop-xy.y+DOM.getBodyScrollTop()
,'offsetLeft' : range.offsetLeft-xy.x+DOM.getBodyScrollLeft()
,'offsetHeight' : range.offsetHeight
}
}
}
if (range) {
return {'x': range.offsetLeft, 'y': range.offsetTop, 'h': range.offsetHeight};
}
return {'x': 0, 'y': 0, 'h': 0};
}
}
,'frame' : function () {
var self=this;
/**
* 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 pos = 0
if ('function' == typeof el.getSelection) {
/*
* we need to calculate both start and end points, because range could be reversed
* but we can't move selection end point before start one
*/
var sel = el.getSelection()
,sn = sel.anchorNode
,so = sel.anchorOffset
,en = sel.focusNode
,eo = sel.focusOffset
,ss = false
,es = false
,sc = 0
,ec = 0
,cn
,tw=document.createTreeWalker(el.document.body,NodeFilter.SHOW_TEXT,null,false)
while (sn && sn.nodeType != 3) {
sn = sn.childNodes[so]
so = 0;
}
while (en && en.nodeType != 3) {
en = en.childNodes[eo]
eo = 0;
}
while (cn=tw.nextNode()) {
if (cn == en) {
ec += eo
es = true
}
if (cn == sn) {
sc += so
ss = true
}
if (!es) ec += cn.nodeValue.length
if (!ss) sc += cn.nodeValue.length
if (es && ss) break;
}
pos = start?Math.min(ec,sc):Math.max(ec,sc)
} else {
el.document.body.setActive();
pos = Math.abs(el.document.selection.createRange()[start?"moveStart":"moveEnd"]("character", -100000000));
}
return pos;
}
/**
* Removes the selection, if available
*
* @param {HTMLElement} el field to delete text from
* @return {String} deleted substring
* @scope public
*/
self.del = function (el) {
if ('function' == typeof el.getSelection) {
var s = el.getSelection()
,i = s.rangeCount
while (--i>-1) s.getRangeAt(i).deleteContents();
} else if (el.document && el.document.selection) {
el.document.selection.createRange().text = "";
el.document.selection.createRange().select();
}
}
/**
* Inserts text to the textarea
*
* @param {HTMLElement} text field to insert text
* @param {String} text to insert
* @scope public
*/
self.ins = function (el,val) {
var p = self.getPos(el,true)+val.length;
if ('function' == typeof el.getSelection) {
var n = el.document.createTextNode(val)
,s = el.getSelection()
s.getRangeAt(0).insertNode(n);
n.parentNode.normalize();
} else if (el.document && el.document.selection) {
el.document.body.setActive();
el.document.selection.createRange().text = val;
}
self.setRange(el,p,p)
return p;
}
/**
* 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,s,e) {
if ('function' == typeof el.getSelection) {
var s = el.getSelection();
return s?s.toString():"";
} else if (el.document && el.document.selection) {
return el.document.selection.createRange().text;
}
}
/**
* Sets the selection range
*
* @param {HTMLElement}
* @param {Number} start position
* @param {Number} end position
* @return void
* @scope public
*/
self.setRange = function (el,start,end) {
if ('function' == typeof el.getSelection) {
var sel = el.getSelection();
sel.removeAllRanges();
var r = el.document.createRange()
,cnt = 0
,cl = 0
,cn
,pn
,tw=document.createTreeWalker(el.document.body,NodeFilter.SHOW_TEXT,null,false);
while ((cn=tw.nextNode())&&(!cn.nodeValue.length||(cnt+cn.nodeValue.length < start))) {
pn = cn;
cnt += cn.nodeValue.length;
}
/*
* explicitly set range borders
*/
if (cn||(cn=pn)) {
r.setStart(cn,start-cnt);
r.setEnd(cn,start-cnt);
}
if (cn) {
do {
if (cn.nodeType != 3) continue;
if (cnt+cn.nodeValue.length < end) {
cnt += cn.nodeValue.length;
} else {
r.setEnd(cn,end-cnt);
break;
}
} while (cn=tw.nextNode())
}
sel.addRange(r);
} else if (el.document && el.document.selection) {
el.document.body.setActive();
var r = el.document.selection.createRange()
r.moveToElementText(el.document.body);
r.move("character",start);
r.moveEnd("character",end-start);
r.select();
}
}
/**
* Method is used to calculate 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) {
var off = {'x':0, 'y':0, 'h':0};
if ('function' == typeof el.getSelection) {
var r = el.getSelection().getRangeAt(0)
,e = r.endOffset
,s = el.document.createElement('span')
,n = s;
s.style.borderLeft='1px solid red';
r.insertNode(s);
off.h = n.offsetHeight;
while (n.offsetParent) {
off.x += n.offsetLeft;
off.y += n.offsetTop;
n = n.offsetParent
}
s.parentNode.removeChild(s);
if (e-r.endOffset) {
r.setEnd(r.endContainer.nextSibling,e-r.endOffset);
el.getSelection().addRange(r)
}
} else if (el.document && el.document.selection) {
var r = el.document.selection.createRange()
off.h = r.boundingHeight
off.x = r.offsetLeft;
off.y = r.offsetTop;
}
return off;
}
}
}