Index: openacs-4/packages/acs-templating/www/resources/xinha-nightly/modules/Opera/Opera.js =================================================================== RCS file: /usr/local/cvsroot/openacs-4/packages/acs-templating/www/resources/xinha-nightly/modules/Opera/Opera.js,v diff -u -r1.3 -r1.4 --- openacs-4/packages/acs-templating/www/resources/xinha-nightly/modules/Opera/Opera.js 27 Mar 2009 08:20:43 -0000 1.3 +++ openacs-4/packages/acs-templating/www/resources/xinha-nightly/modules/Opera/Opera.js 23 May 2010 11:58:33 -0000 1.4 @@ -1,3 +1,902 @@ -/* This compressed file is part of Xinha. For uncompressed sources, forum, and bug reports, go to xinha.org */ -/* This file is part of version 0.96beta2 released Fri, 20 Mar 2009 11:01:14 +0100 */ -Opera._pluginInfo={name:"Opera",origin:"Xinha Core",version:"$LastChangedRevision: 1084 $".replace(/^[^:]*:\s*(.*)\s*\$$/,"$1"),developer:"The Xinha Core Developer Team",developer_url:"$HeadURL: http://svn.xinha.org/trunk/modules/Opera/Opera.js $".replace(/^[^:]*:\s*(.*)\s*\$$/,"$1"),sponsor:"Gogo Internet Services Limited",sponsor_url:"http://www.gogo.co.nz/",license:"htmlArea"};function Opera(a){this.editor=a;a.Opera=this}Opera.prototype.onKeyPress=function(u){var d=this.editor;var j=d.getSelection();if(d.isShortCut(u)){switch(d.getKey(u).toLowerCase()){case"z":if(d._unLink&&d._unlinkOnUndo){Xinha._stopEvent(u);d._unLink();d.updateToolbar();return true}break;case"a":sel=d.getSelection();sel.removeAllRanges();range=d.createRange();range.selectNodeContents(d._doc.body);sel.addRange(range);Xinha._stopEvent(u);return true;break;case"v":if(!d.config.htmlareaPaste){return true}break}}switch(d.getKey(u)){case" ":var g=function(y,m){var x=y.nextSibling;if(typeof m=="string"){m=d._doc.createElement(m)}var s=y.parentNode.insertBefore(m,x);Xinha.removeFromParent(y);s.appendChild(y);x.data=" "+x.data;j.collapse(x,1);d._unLink=function(){var a=s.firstChild;s.removeChild(a);s.parentNode.insertBefore(a,s);Xinha.removeFromParent(s);d._unLink=null;d._unlinkOnUndo=false};d._unlinkOnUndo=true;return s};if(d.config.convertUrlsToLinks&&j&&j.isCollapsed&&j.anchorNode.nodeType==3&&j.anchorNode.data.length>3&&j.anchorNode.data.indexOf(".")>=0){var t=j.anchorNode.data.substring(0,j.anchorOffset).search(/\S{4,}$/);if(t==-1){break}if(d._getFirstAncestor(j,"a")){break}var h=j.anchorNode.data.substring(0,j.anchorOffset).replace(/^.*?(\S*)$/,"$1");var e=h.match(Xinha.RE_email);if(e){var v=j.anchorNode;var f=v.splitText(j.anchorOffset);var k=v.splitText(t);g(k,"a").href="mailto:"+e[0];break}RE_date=/([0-9]+\.)+/;RE_ip=/(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)/;var p=h.match(Xinha.RE_url);if(p){if(RE_date.test(h)){break}var i=j.anchorNode;var b=i.splitText(j.anchorOffset);var q=i.splitText(t);g(q,"a").href=(p[1]?p[1]:"http://")+p[2];break}}break}switch(u.keyCode){case 27:if(d._unLink){d._unLink();Xinha._stopEvent(u)}break;break;case 8:case 46:if(!u.shiftKey&&this.handleBackspace()){Xinha._stopEvent(u)}default:d._unlinkOnUndo=false;if(j.anchorNode&&j.anchorNode.nodeType==3){var w=d._getFirstAncestor(j,"a");if(!w){break}if(!w._updateAnchTimeout){if(j.anchorNode.data.match(Xinha.RE_email)&&w.href.match("mailto:"+j.anchorNode.data.trim())){var l=j.anchorNode;var c=function(){w.href="mailto:"+l.data.trim();w._updateAnchTimeout=setTimeout(c,250)};w._updateAnchTimeout=setTimeout(c,1000);break}var n=j.anchorNode.data.match(Xinha.RE_url);if(n&&w.href.match(new RegExp("http(s)?://"+Xinha.escapeStringForRegExp(j.anchorNode.data.trim())))){var o=j.anchorNode;var r=function(){n=o.data.match(Xinha.RE_url);if(n){w.href=(n[1]?n[1]:"http://")+n[2]}w._updateAnchTimeout=setTimeout(r,250)};w._updateAnchTimeout=setTimeout(r,1000)}}}break}return false};Opera.prototype.handleBackspace=function(){var a=this.editor;setTimeout(function(){var e=a.getSelection();var g=a.createRange(e);var f=g.startContainer;var i=g.startOffset;var c=g.endContainer;var h=g.endOffset;var j=f.nextSibling;if(f.nodeType==3){f=f.parentNode}if(!(/\S/.test(f.tagName))){var d=document.createElement("p");while(f.firstChild){d.appendChild(f.firstChild)}f.parentNode.insertBefore(d,f);Xinha.removeFromParent(f);var b=g.cloneRange();b.setStartBefore(j);b.setEndAfter(j);b.extractContents();e.removeAllRanges();e.addRange(b)}},10)};Opera.prototype.inwardHtml=function(a){a=a.replace(/<(\/?)del(\s|>|\/)/ig,"<$1strike$2");return a};Opera.prototype.outwardHtml=function(a){return a};Opera.prototype.onExecCommand=function(f,e,d){switch(f){case"removeformat":var k=this.editor;var c=k.getSelection();var l=k.saveSelection(c);var j=k.createRange(c);var g=k._doc.body.getElementsByTagName("*");var a=(j.startContainer.nodeType==1)?j.startContainer:j.startContainer.parentNode;var h,b;if(c.isCollapsed){j.selectNodeContents(k._doc.body)}for(h=0;ha.anchorOffset&&a.anchorNode.childNodes[a.anchorOffset].nodeType==1){return a.anchorNode.childNodes[a.anchorOffset]}else{if(a.anchorNode.nodeType==1){return a.anchorNode}else{return null}}}return null};Xinha.prototype.selectionEmpty=function(a){if(!a){return true}if(typeof a.isCollapsed!="undefined"){return a.isCollapsed}return true};Xinha.prototype.saveSelection=function(){return this.createRange(this.getSelection()).cloneRange()};Xinha.prototype.restoreSelection=function(a){var b=this.getSelection();b.removeAllRanges();b.addRange(a)};Xinha.prototype.selectNodeContents=function(b,d){this.focusEditor();this.forceRedraw();var a;var e=typeof d=="undefined"?true:false;var c=this.getSelection();a=this._doc.createRange();if(e&&b.tagName&&b.tagName.toLowerCase().match(/table|img|input|textarea|select/)){a.selectNode(b)}else{a.selectNodeContents(b)}c.removeAllRanges();c.addRange(a);if(typeof d!="undefined"){if(d){c.collapse(a.startContainer,a.startOffset)}else{c.collapse(a.endContainer,a.endOffset)}}};Xinha.prototype.insertHTML=function(c){var e=this.getSelection();var a=this.createRange(e);this.focusEditor();var b=this._doc.createDocumentFragment();var f=this._doc.createElement("div");f.innerHTML=c;while(f.firstChild){b.appendChild(f.firstChild)}var d=this.insertNodeAtSelection(b)};Xinha.prototype.getSelectedHTML=function(){var b=this.getSelection();if(b.isCollapsed){return""}var a=this.createRange(b);return Xinha.getHTML(a.cloneContents(),false,this)};Xinha.prototype.getSelection=function(){var c=this._iframe.contentWindow.getSelection();if(c&&c.focusNode&&c.focusNode.tagName&&c.focusNode.tagName=="HTML"){var b=this._doc.getElementsByTagName("body")[0];var a=this.createRange();a.selectNodeContents(b);c.removeAllRanges();c.addRange(a);c.collapseToEnd()}return c};Xinha.prototype.createRange=function(b){this.activateEditor();if(typeof b!="undefined"){try{return b.getRangeAt(0)}catch(a){return this._doc.createRange()}}else{return this._doc.createRange()}};Xinha.prototype.isKeyEvent=function(a){return a.type=="keypress"};Xinha.prototype.getKey=function(a){return String.fromCharCode(a.charCode)};Xinha.getOuterHTML=function(a){return(new XMLSerializer()).serializeToString(a)};Xinha.cc=String.fromCharCode(8286);Xinha.prototype.setCC=function(i){var c=Xinha.cc;try{if(i=="textarea"){var f=this._textArea;var g=f.selectionStart;var k=f.value.substring(0,g);var a=f.value.substring(g,f.value.length);if(a.match(/^[^<]*>/)){var j=a.indexOf(">")+1;f.value=k+a.substring(0,j)+c+a.substring(j,a.length)}else{f.value=k+c+a}f.value=f.value.replace(new RegExp("(&[^"+c+"]*?)("+c+")([^"+c+"]*?;)"),"$1$3$2");f.value=f.value.replace(new RegExp("(]*>[^"+c+"]*?)("+c+")([^"+c+"]*?<\/script>)"),"$1$3$2");f.value=f.value.replace(new RegExp("^([^"+c+"]*)("+c+")([^"+c+"]*]*>)(.*?)"),"$1$3$2$4");f.value=f.value.replace(c,'MARK')}else{var b=this.getSelection();var d=this._doc.createElement("span");d.id="XinhaOperaCaretMarker";b.getRangeAt(0).insertNode(d)}}catch(h){}};Xinha.prototype.findCC=function(i){if(i=="textarea"){var h=this._textArea;var j=h.value.search(/(((\s|(MARK))*<\/span>)?)/);if(j==-1){return}var e=RegExp.$1;var f=j+e.length;var k=h.value.substring(0,j);var b=h.value.substring(f,h.value.length);h.value=k;h.scrollTop=h.scrollHeight;var d=h.scrollTop;h.value+=b;h.setSelectionRange(j,j);h.focus();h.scrollTop=d}else{var g=this._doc.getElementById("XinhaOperaCaretMarker");if(g){this.focusEditor();var a=this.createRange();a.selectNode(g);var c=this.getSelection();c.addRange(a);c.collapseToStart();this.scrollToElement(g);g.parentNode.removeChild(g);return}}};Xinha.getDoctype=function(a){var b="";if(a.doctype){b+=""}return b};Xinha.prototype._standardInitIframe=Xinha.prototype.initIframe;Xinha.prototype.initIframe=function(){if(!this._iframeLoadDone){if(this._iframe.contentWindow&&this._iframe.contentWindow.xinhaReadyToRoll){this._iframeLoadDone=true;this._standardInitIframe()}else{var a=this;setTimeout(function(){a.initIframe()},5)}}};Xinha._addEventOperaOrig=Xinha._addEvent;Xinha._addEvent=function(a,c,b){if(a.tagName&&a.tagName.toLowerCase()=="select"&&c=="change"){return Xinha.addDom0Event(a,c,b)}return Xinha._addEventOperaOrig(a,c,b)}; \ No newline at end of file + + /*--------------------------------------:noTabs=true:tabSize=2:indentSize=2:-- + -- Xinha (is not htmlArea) - http://xinha.gogo.co.nz/ + -- + -- Use of Xinha is granted by the terms of the htmlArea License (based on + -- BSD license) please read license.txt in this package for details. + -- + -- Xinha was originally based on work by Mihai Bazon which is: + -- Copyright (c) 2003-2004 dynarch.com. + -- Copyright (c) 2002-2003 interactivetools.com, inc. + -- This copyright notice MUST stay intact for use. + -- + -- This is the Opera compatability plugin, part of the Xinha core. + -- + -- The file is loaded as a special plugin by the Xinha Core when + -- Xinha is being run under an Opera based browser with the Midas + -- editing API. + -- + -- It provides implementation and specialisation for various methods + -- in the core where different approaches per browser are required. + -- + -- Design Notes:: + -- Most methods here will simply be overriding Xinha.prototype. + -- and should be called that, but methods specific to Opera should + -- be a part of the Opera.prototype, we won't trample on namespace + -- that way. + -- + -- $HeadURL: http://svn.xinha.org/trunk/modules/Opera/Opera.js $ + -- $LastChangedDate: 2008-10-13 06:42:42 +1300 (Mon, 13 Oct 2008) $ + -- $LastChangedRevision: 1084 $ + -- $LastChangedBy: ray $ + --------------------------------------------------------------------------*/ + +Opera._pluginInfo = { + name : "Opera", + origin : "Xinha Core", + version : "$LastChangedRevision: 1084 $".replace(/^[^:]*:\s*(.*)\s*\$$/, '$1'), + developer : "The Xinha Core Developer Team", + developer_url : "$HeadURL: http://svn.xinha.org/trunk/modules/Opera/Opera.js $".replace(/^[^:]*:\s*(.*)\s*\$$/, '$1'), + sponsor : "Gogo Internet Services Limited", + sponsor_url : "http://www.gogo.co.nz/", + license : "htmlArea" +}; + +function Opera(editor) { + this.editor = editor; + editor.Opera = this; +} + +/** Allow Opera to handle some key events in a special way. + */ + +Opera.prototype.onKeyPress = function(ev) +{ + var editor = this.editor; + var s = editor.getSelection(); + + // Handle shortcuts + if(editor.isShortCut(ev)) + { + switch(editor.getKey(ev).toLowerCase()) + { + case 'z': + { + if(editor._unLink && editor._unlinkOnUndo) + { + Xinha._stopEvent(ev); + editor._unLink(); + editor.updateToolbar(); + return true; + } + } + break; + + case 'a': + { + // KEY select all + sel = editor.getSelection(); + sel.removeAllRanges(); + range = editor.createRange(); + range.selectNodeContents(editor._doc.body); + sel.addRange(range); + Xinha._stopEvent(ev); + return true; + } + break; + + case 'v': + { + // If we are not using htmlareaPaste, don't let Xinha try and be fancy but let the + // event be handled normally by the browser (don't stopEvent it) + if(!editor.config.htmlareaPaste) + { + return true; + } + } + break; + } + } + + // Handle normal characters + switch(editor.getKey(ev)) + { + // Space, see if the text just typed looks like a URL, or email address + // and link it appropriatly + case ' ': + { + var autoWrap = function (textNode, tag) + { + var rightText = textNode.nextSibling; + if ( typeof tag == 'string') + { + tag = editor._doc.createElement(tag); + } + var a = textNode.parentNode.insertBefore(tag, rightText); + Xinha.removeFromParent(textNode); + a.appendChild(textNode); + rightText.data = ' ' + rightText.data; + + s.collapse(rightText, 1); + + editor._unLink = function() + { + var t = a.firstChild; + a.removeChild(t); + a.parentNode.insertBefore(t, a); + Xinha.removeFromParent(a); + editor._unLink = null; + editor._unlinkOnUndo = false; + }; + editor._unlinkOnUndo = true; + + return a; + }; + + if ( editor.config.convertUrlsToLinks && s && s.isCollapsed && s.anchorNode.nodeType == 3 && s.anchorNode.data.length > 3 && s.anchorNode.data.indexOf('.') >= 0 ) + { + var midStart = s.anchorNode.data.substring(0,s.anchorOffset).search(/\S{4,}$/); + if ( midStart == -1 ) + { + break; + } + + if ( editor._getFirstAncestor(s, 'a') ) + { + break; // already in an anchor + } + + var matchData = s.anchorNode.data.substring(0,s.anchorOffset).replace(/^.*?(\S*)$/, '$1'); + + var mEmail = matchData.match(Xinha.RE_email); + if ( mEmail ) + { + var leftTextEmail = s.anchorNode; + var rightTextEmail = leftTextEmail.splitText(s.anchorOffset); + var midTextEmail = leftTextEmail.splitText(midStart); + + autoWrap(midTextEmail, 'a').href = 'mailto:' + mEmail[0]; + break; + } + + RE_date = /([0-9]+\.)+/; //could be date or ip or something else ... + RE_ip = /(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)/; + var mUrl = matchData.match(Xinha.RE_url); + if ( mUrl ) + { + if (RE_date.test(matchData)) + { + break; //ray: disabling linking of IP numbers because of general bugginess (see Ticket #1085) + /*if (!RE_ip.test(matchData)) + { + break; + }*/ + } + var leftTextUrl = s.anchorNode; + var rightTextUrl = leftTextUrl.splitText(s.anchorOffset); + var midTextUrl = leftTextUrl.splitText(midStart); + autoWrap(midTextUrl, 'a').href = (mUrl[1] ? mUrl[1] : 'http://') + mUrl[2]; + break; + } + } + } + break; + } + + // Handle special keys + switch ( ev.keyCode ) + { +/* This is now handled by a plugin + case 13: // ENTER + + break;*/ + + case 27: // ESCAPE + { + if ( editor._unLink ) + { + editor._unLink(); + Xinha._stopEvent(ev); + } + break; + } + break; + + case 8: // KEY backspace + case 46: // KEY delete + { + // We handle the mozilla backspace directly?? + if ( !ev.shiftKey && this.handleBackspace() ) + { + Xinha._stopEvent(ev); + } + } + + default: + { + editor._unlinkOnUndo = false; + + // Handle the "auto-linking", specifically this bit of code sets up a handler on + // an self-titled anchor (eg www.gogo.co.nz) + // when the text content is edited, such that it will update the href on the anchor + + if ( s.anchorNode && s.anchorNode.nodeType == 3 ) + { + // See if we might be changing a link + var a = editor._getFirstAncestor(s, 'a'); + // @todo: we probably need here to inform the setTimeout below that we not changing a link and not start another setTimeout + if ( !a ) + { + break; // not an anchor + } + + if ( !a._updateAnchTimeout ) + { + if ( s.anchorNode.data.match(Xinha.RE_email) && a.href.match('mailto:' + s.anchorNode.data.trim()) ) + { + var textNode = s.anchorNode; + var fnAnchor = function() + { + a.href = 'mailto:' + textNode.data.trim(); + // @fixme: why the hell do another timeout is started ? + // This lead to never ending timer if we dont remove this line + // But when removed, the email is not correctly updated + // + // - to fix this we should make fnAnchor check to see if textNode.data has + // stopped changing for say 5 seconds and if so we do not make this setTimeout + a._updateAnchTimeout = setTimeout(fnAnchor, 250); + }; + a._updateAnchTimeout = setTimeout(fnAnchor, 1000); + break; + } + + var m = s.anchorNode.data.match(Xinha.RE_url); + + if ( m && a.href.match(new RegExp( 'http(s)?://' + Xinha.escapeStringForRegExp( s.anchorNode.data.trim() ) ) ) ) + { + var txtNode = s.anchorNode; + var fnUrl = function() + { + // Sometimes m is undefined becase the url is not an url anymore (was www.url.com and become for example www.url) + // ray: shouldn't the link be un-linked then? + m = txtNode.data.match(Xinha.RE_url); + if(m) + { + a.href = (m[1] ? m[1] : 'http://') + m[2]; + } + + // @fixme: why the hell do another timeout is started ? + // This lead to never ending timer if we dont remove this line + // But when removed, the url is not correctly updated + // + // - to fix this we should make fnUrl check to see if textNode.data has + // stopped changing for say 5 seconds and if so we do not make this setTimeout + a._updateAnchTimeout = setTimeout(fnUrl, 250); + }; + a._updateAnchTimeout = setTimeout(fnUrl, 1000); + } + } + } + } + break; + } + + return false; // Let other plugins etc continue from here. +} + +/** When backspace is hit, the Opera onKeyPress will execute this method. + * I don't remember what the exact purpose of this is though :-( + */ + +Opera.prototype.handleBackspace = function() +{ + var editor = this.editor; + setTimeout( + function() + { + var sel = editor.getSelection(); + var range = editor.createRange(sel); + var SC = range.startContainer; + var SO = range.startOffset; + var EC = range.endContainer; + var EO = range.endOffset; + var newr = SC.nextSibling; + if ( SC.nodeType == 3 ) + { + SC = SC.parentNode; + } + if ( ! ( /\S/.test(SC.tagName) ) ) + { + var p = document.createElement("p"); + while ( SC.firstChild ) + { + p.appendChild(SC.firstChild); + } + SC.parentNode.insertBefore(p, SC); + Xinha.removeFromParent(SC); + var r = range.cloneRange(); + r.setStartBefore(newr); + r.setEndAfter(newr); + r.extractContents(); + sel.removeAllRanges(); + sel.addRange(r); + } + }, + 10); +}; + +Opera.prototype.inwardHtml = function(html) +{ + // Both IE and Opera use strike internally instead of del (#523) + // Xinha will present del externally (see Xinha.prototype.outwardHtml + html = html.replace(/<(\/?)del(\s|>|\/)/ig, "<$1strike$2"); + + return html; +} + +Opera.prototype.outwardHtml = function(html) +{ + + return html; +} + +Opera.prototype.onExecCommand = function(cmdID, UI, param) +{ + + switch(cmdID) + { + + /*case 'paste': + { + alert(Xinha._lc("The Paste button does not work in Mozilla based web browsers (technical security reasons). Press CTRL-V on your keyboard to paste directly.")); + return true; // Indicate paste is done, stop command being issued to browser by Xinha.prototype.execCommand + } + break;*/ + + case 'removeformat': + var editor = this.editor; + var sel = editor.getSelection(); + var selSave = editor.saveSelection(sel); + var range = editor.createRange(sel); + + var els = editor._doc.body.getElementsByTagName('*'); + + var start = ( range.startContainer.nodeType == 1 ) ? range.startContainer : range.startContainer.parentNode; + var i, el; + if (sel.isCollapsed) range.selectNodeContents(editor._doc.body); + + for (i=0; i sel.anchorOffset && sel.anchorNode.childNodes[sel.anchorOffset].nodeType == 1 ) + { + return sel.anchorNode.childNodes[sel.anchorOffset]; + } + else if ( sel.anchorNode.nodeType == 1 ) + { + return sel.anchorNode; + } + else + { + return null; // return sel.anchorNode.parentNode; + } + } + return null; +}; + +/** + * Determines if the given selection is empty (collapsed). + * @param selection Selection object as returned by getSelection + * @returns true|false + */ + +Xinha.prototype.selectionEmpty = function(sel) +{ + if ( !sel ) + { + return true; + } + + if ( typeof sel.isCollapsed != 'undefined' ) + { + return sel.isCollapsed; + } + + return true; +}; + +/** + * Returns a range object to be stored + * and later restored with Xinha.prototype.restoreSelection() + * + * @returns Range + */ +Xinha.prototype.saveSelection = function() +{ + return this.createRange(this.getSelection()).cloneRange(); +} +/** + * Restores a selection previously stored + * @param savedSelection Range object as returned by Xinha.prototype.restoreSelection() + */ +Xinha.prototype.restoreSelection = function(savedSelection) +{ + var sel = this.getSelection(); + sel.removeAllRanges(); + sel.addRange(savedSelection); +} +/** + * Selects the contents of the given node. If the node is a "control" type element, (image, form input, table) + * the node itself is selected for manipulation. + * + * @param node DomNode + * @param collapseToStart A boolean that, when supplied, says to collapse the selection. True collapses to the start, and false to the end. + */ + +Xinha.prototype.selectNodeContents = function(node, collapseToStart) +{ + this.focusEditor(); + this.forceRedraw(); + var range; + var collapsed = typeof collapseToStart == "undefined" ? true : false; + var sel = this.getSelection(); + range = this._doc.createRange(); + // Tables and Images get selected as "objects" rather than the text contents + if ( collapsed && node.tagName && node.tagName.toLowerCase().match(/table|img|input|textarea|select/) ) + { + range.selectNode(node); + } + else + { + range.selectNodeContents(node); + } + sel.removeAllRanges(); + sel.addRange(range); + if (typeof collapseToStart != "undefined") + { + if (collapseToStart) + { + sel.collapse(range.startContainer, range.startOffset); + } else + { + sel.collapse(range.endContainer, range.endOffset); + } + } +}; + +/** Insert HTML at the current position, deleting the selection if any. + * + * @param html string + */ + +Xinha.prototype.insertHTML = function(html) +{ + var sel = this.getSelection(); + var range = this.createRange(sel); + this.focusEditor(); + // construct a new document fragment with the given HTML + var fragment = this._doc.createDocumentFragment(); + var div = this._doc.createElement("div"); + div.innerHTML = html; + while ( div.firstChild ) + { + // the following call also removes the node from div + fragment.appendChild(div.firstChild); + } + // this also removes the selection + var node = this.insertNodeAtSelection(fragment); +}; + +/** Get the HTML of the current selection. HTML returned has not been passed through outwardHTML. + * + * @returns string + */ + +Xinha.prototype.getSelectedHTML = function() +{ + var sel = this.getSelection(); + if (sel.isCollapsed) return ''; + var range = this.createRange(sel); + return Xinha.getHTML(range.cloneContents(), false, this); +}; + + +/** Get a Selection object of the current selection. Note that selection objects are browser specific. + * + * @returns Selection + */ + +Xinha.prototype.getSelection = function() +{ + var sel = this._iframe.contentWindow.getSelection(); + if(sel && sel.focusNode && sel.focusNode.tagName && sel.focusNode.tagName == 'HTML') + { // Bad news batman, this selection is wonky + var bod = this._doc.getElementsByTagName('body')[0]; + var rng = this.createRange(); + rng.selectNodeContents(bod); + sel.removeAllRanges(); + sel.addRange(rng); + sel.collapseToEnd(); + } + return sel; +}; + +/** Create a Range object from the given selection. Note that range objects are browser specific. + * + * @param sel Selection object (see getSelection) + * @returns Range + */ + +Xinha.prototype.createRange = function(sel) +{ + this.activateEditor(); + if ( typeof sel != "undefined" ) + { + try + { + return sel.getRangeAt(0); + } + catch(ex) + { + return this._doc.createRange(); + } + } + else + { + return this._doc.createRange(); + } +}; + +/** Determine if the given event object is a keydown/press event. + * + * @param event Event + * @returns true|false + */ + +Xinha.prototype.isKeyEvent = function(event) +{ + return event.type == "keypress"; +} + +/** Return the character (as a string) of a keyEvent - ie, press the 'a' key and + * this method will return 'a', press SHIFT-a and it will return 'A'. + * + * @param keyEvent + * @returns string + */ + +Xinha.prototype.getKey = function(keyEvent) +{ + return String.fromCharCode(keyEvent.charCode); +} + +/** Return the HTML string of the given Element, including the Element. + * + * @param element HTML Element DomNode + * @returns string + */ + +Xinha.getOuterHTML = function(element) +{ + return (new XMLSerializer()).serializeToString(element); +}; + + +/* Caret position remembering when switch between text and html mode. + * Largely this is the same as for Gecko except: + * + * Opera does not have window.find() so we use instead an element with known + * id (MARK) so that we can + * do _doc.getElementById() on it. + * + * Opera for some reason will not set the insert point (flashing caret) + * anyway though, in theory, the iframe is focused (in findCC below) and then + * the selection (containing the span above) is collapsed, it should show + * caret. I don't know why not. Seems to require user to actually + * click to get the caret to show (or type anything without it acting wierd)? + * Very annoying. + * + */ +Xinha.cc = String.fromCharCode(8286); +Xinha.prototype.setCC = function ( target ) +{ + // Do a two step caret insertion, first a single char, then we'll replace that with the + // id'd span. + var cc = Xinha.cc; + + try + { + if ( target == "textarea" ) + { + var ta = this._textArea; + var index = ta.selectionStart; + var before = ta.value.substring( 0, index ) + var after = ta.value.substring( index, ta.value.length ); + + if ( after.match(/^[^<]*>/) ) // make sure cursor is in an editable area (outside tags, script blocks, entities, and inside the body) + { + var tagEnd = after.indexOf(">") + 1; + ta.value = before + after.substring( 0, tagEnd ) + cc + after.substring( tagEnd, after.length ); + } + else ta.value = before + cc + after; + ta.value = ta.value.replace(new RegExp ('(&[^'+cc+']*?)('+cc+')([^'+cc+']*?;)'), "$1$3$2"); + ta.value = ta.value.replace(new RegExp ('(]*>[^'+cc+']*?)('+cc+')([^'+cc+']*?<\/script>)'), "$1$3$2"); + ta.value = ta.value.replace(new RegExp ('^([^'+cc+']*)('+cc+')([^'+cc+']*]*>)(.*?)'), "$1$3$2$4"); + + ta.value = ta.value.replace(cc, 'MARK'); + } + else + { + var sel = this.getSelection(); + + var mark = this._doc.createElement('span'); + mark.id = 'XinhaOperaCaretMarker'; + sel.getRangeAt(0).insertNode( mark ); + + } + } catch (e) {} +}; + +Xinha.prototype.findCC = function ( target ) +{ + if ( target == 'textarea' ) + { + var ta = this._textArea; + var pos = ta.value.search( /(((\s|(MARK))*<\/span>)?)/ ); + if ( pos == -1 ) return; + var cc = RegExp.$1; + var end = pos + cc.length; + var before = ta.value.substring( 0, pos ); + var after = ta.value.substring( end, ta.value.length ); + ta.value = before ; + + ta.scrollTop = ta.scrollHeight; + var scrollPos = ta.scrollTop; + + ta.value += after; + ta.setSelectionRange(pos,pos); + + ta.focus(); + + ta.scrollTop = scrollPos; + + } + else + { + var marker = this._doc.getElementById('XinhaOperaCaretMarker'); + if(marker) + { + this.focusEditor();// this._iframe.contentWindow.focus(); + + var rng = this.createRange(); + rng.selectNode(marker); + + var sel = this.getSelection(); + sel.addRange(rng); + sel.collapseToStart(); + + this.scrollToElement(marker); + + marker.parentNode.removeChild(marker); + return; + } + } +}; + +/*--------------------------------------------------------------------------*/ +/*------------ EXTEND SOME STANDARD "Xinha.prototype" METHODS --------------*/ +/*--------------------------------------------------------------------------*/ + +/* +Xinha.prototype._standardToggleBorders = Xinha.prototype._toggleBorders; +Xinha.prototype._toggleBorders = function() +{ + var result = this._standardToggleBorders(); + + // flashing the display forces moz to listen (JB:18-04-2005) - #102 + var tables = this._doc.getElementsByTagName('TABLE'); + for(var i = 0; i < tables.length; i++) + { + tables[i].style.display="none"; + tables[i].style.display="table"; + } + + return result; +}; +*/ + +/** Return the doctype of a document, if set + * + * @param doc DOM element document + * @returns string the actual doctype + */ +Xinha.getDoctype = function (doc) +{ + var d = ''; + if (doc.doctype) + { + d += '"; + } + return d; +}; + +Xinha.prototype._standardInitIframe = Xinha.prototype.initIframe; +Xinha.prototype.initIframe = function() +{ + if(!this._iframeLoadDone) + { + if(this._iframe.contentWindow && this._iframe.contentWindow.xinhaReadyToRoll) + { + this._iframeLoadDone = true; + this._standardInitIframe(); + } + else + { + // Timeout a little (Opera is a bit dodgy about using an event) + var editor = this; + setTimeout( function() { editor.initIframe() }, 5); + } + } +} + +// For some reason, Opera doesn't listen to addEventListener for at least select elements with event = change +// so we will have to intercept those and use a Dom0 event, these are used eg for the fontname/size/format +// dropdowns in the toolbar +Xinha._addEventOperaOrig = Xinha._addEvent; +Xinha._addEvent = function(el, evname, func) +{ + if(el.tagName && el.tagName.toLowerCase() == 'select' && evname == 'change') + { + return Xinha.addDom0Event(el,evname,func); + } + + return Xinha._addEventOperaOrig(el,evname,func); +}