View file wym-editor/wym.js

File size: 19.77Kb
/*
 * WYMeditor : what you see is What You Mean web-based editor
 * Copyright (C) 2006 Jean-François Hovinne - Daniel Reszka
 * Use of WYMeditor is granted by the terms of the MIT License (http://www.opensource.org/licenses/mit-license.php).
 *
 * For further information visit:
 * 		http://www.wymeditor.org/
 * 
 * File Name:
 *		wym.js
 *		Main javascript functions.
 *		See the documentation for more info.
 * 
 * File Authors:
 * 		Jean-François Hovinne ([email protected])
*/

//global vars
var selectedElement=null;
var bCleanPaste=true;

//called at body.onload
function init()
{
	getHTML();
	setImgEvent();
	displayPasteCleanup(true);
	if(moz)
	{
		iframe().contentDocument.designMode="on";
		iframe().contentDocument.addEventListener('keydown',iframe_keydown_handler,false);
		iframe().contentDocument.addEventListener('keyup',iframe_keyup_handler,false);
		iframe().contentDocument.addEventListener('mouseup',iframe_mouseup_handler,false);
		iframe().contentDocument.addEventListener('blur',function(evt){bCleanPaste=false;displayPasteCleanup(true);},false);
		execCom("styleWithCSS",false);
	}
}

//these functions return base objects
function editor()
{
	if(ie) return(document.getElementById('editor'));
	else if(moz) return(iframe().contentDocument.body);
}
function txthtml()
{
	return(document.getElementById('txthtml'));
}
function container()
{
	return(getSelectedContainer());
}
function classespanel()
{
	return(document.getElementById('m_class'));
}
//returns selected image
function selected()
{
  return(selectedElement);
}
function selectedId()
{
	if(selectedElement!=null)return(selectedElement.id);
	else return(null);
}

function iframe()
{
	return(document.getElementById('iframe_editor'));
}

//we 'release' the image selection
function release()
{
	selectedElement=null;
}

//used to get the cursor position
function saveCaret()
{
    editor().caretPos=document.selection.createRange();
}

//little hack to get the current cursor position
function getCaretPos()
{
    var bookmark="~caret~pos~";
    var orig=editor().innerHTML;
    var caretPos=editor().caretPos;
    if(caretPos!=null)
    {
    	caretPos.text=bookmark;
    	var i=editor().innerHTML.search(bookmark);
    	editor().innerHTML=orig;

    	var hid=document.getElementById('caretpos');
    	hid.value=i;
    }
    return(i);
}

//insert HTML at cursor pos
function insertAtCursor(sHtml)
{
	if(ie)
	{
		var pos=getCaretPos();
		if(pos>-1)
		{
			var html=editor().innerHTML;
			editor().innerHTML=insertAt(html,sHtml,pos);
		}
	}
	else if(moz)
	{
		execCom("inserthtml",sHtml);
	}
}

//insert an HTML element *after* the current one
function insertAfter(elem,currentElem)
{
	if(currentElem.nextSibling!=null)editor().insertBefore(elem,currentElem.nextSibling);
	else editor().appendChild(elem);
}

//put editor value in txthtml
function getHTML()
{
	if(ie)
	{
		txthtml().innerText="";
		txthtml().innerText=editor().innerHTML;
	}
	else if(moz)
	{
		txthtml().value="";
		txthtml().value=editor().innerHTML;
	}
}

//put cleaned editor value in txthtml
function getCleanHTML()
{
	if(ie)
	{
		txthtml().innerText="";
		txthtml().innerText=cleanupHTML_ie(editor().innerHTML);
	}
	else if(moz)
	{
		txthtml().value="";
		
		//various cleanups, see util.js
		removeLastBr(editor());	
		txthtml().value=cleanupHTML_moz(editor().innerHTML);
	}
}

//put txthtml value in editor
function setHTML()
{
	if(ie) editor().innerHTML=txthtml().innerText;
	else if(moz)editor().innerHTML=txthtml().value;
}

//set txthtml (in)visible
function htmlVisible()
{
	if(txthtml().style.display!="none")txthtml().style.display="none";
	else txthtml().style.display="inline";
}

//buttons events
function execCom(cmd,opt)
{
	if(moz)
	{
		//well, moz sets <b><strong>#text</strong></b> or <i><em>#text</em></i>
		//in this case, we don't use execCommand: we replace the <strong> or <em> nodes by a textNode
		switch(cmd.toLowerCase())
		{
			case "bold":
				container=getSelectedContainer();
				if(container.tagName.toLowerCase()=="strong")
				{
					ntext=iframe().contentDocument.createTextNode(container.innerHTML);
					container.parentNode.replaceChild(ntext,container);
				}
				else iframe().contentDocument.execCommand(cmd,'',opt);
				break;
			case "italic":
				container=getSelectedContainer();
				if(container.tagName.toLowerCase()=="em")
				{
					ntext=iframe().contentDocument.createTextNode(container.innerHTML);
					container.parentNode.replaceChild(ntext,container);
				}
				else iframe().contentDocument.execCommand(cmd,'',opt);
				break;
			default:
				iframe().contentDocument.execCommand(cmd,'',opt);
				break;
		}
	}
	else if(ie) {document.execCommand(cmd);}
}

//main function to get the current selected container
function getSelectedContainer()
{
	if(selectedElement==null)
	{
		if(ie)
		{
			var caretPos=editor().caretPos;
    			if(caretPos!=null)
    			{
    				if(caretPos.parentElement!=undefined)return(caretPos.parentElement());
    			}
    		}
    		else if(moz)
    		{
			var sel=iframe().contentWindow.getSelection();
			var node=sel.focusNode;
			if(node.nodeName=="#text")return(node.parentNode);
			else return(node);
    		}
    	}
	else return(selectedElement);
}

//get the top container (the first editor's child which contains the element)
function getMainContainer(elem)
{
	if(ie)
	{
		nodes=editor().children;
		for(var x=0;x<nodes.length;x++)
		{
			if(nodes.item(x).contains(elem)) //not supported by Mozilla
			{
				container=nodes.item(x);
				break;
			}
		}
		return(container);
	}
	else if(moz)
	{
		nodes=editor().childNodes;
		for(var y=0;y<nodes.length;y++)
		{
			if(nodeContains(nodes.item(y),elem) || nodes.item(y)==elem)
			{
				container=nodes.item(y);
				break;
			}
		}
		return(container);
	}
}

//Moz only: Mozilla doesn't support node.contains()
function nodeContains(node,elem)
{
	parent=elem.parentNode;
	if(parent.tagName.toLowerCase()=="body" || parent.tagName.toLowerCase()=="html")return(false);
	else if(parent==node)return(true);
	else return(nodeContains(node,parent));
}

//recursive function which returns the element's parent having a particular type

//we get the element's parent
//if its type (tagname) isn't sContainerType, we get its parent
//and so on ...
function getContainerOfType(elem,sContainerType)
{
	if(elem!=null && elem.id!="editor")
	{
		if(elem.tagName!=sContainerType)
		{
			elem=elem.parentNode;
			return(getContainerOfType(elem,sContainerType));
		}
		else return(elem);
	}
	else return(null);
}

//the same as getContainerOfType, but we use an array of possible types
function getContainerOfTypeArray(elem,aContainerTypes)
{
	var bFound=false;
	if(elem!=null && elem.id!="editor")
	{
		for(i=0;i<aContainerTypes.length;i++)
		{
			if(elem.tagName==aContainerTypes[i])
			{
				bFound=true;
				break;
			}
		}
		if(!bFound)
		{
			elem=elem.parentNode;
			return(getContainerOfTypeArray(elem,aContainerTypes));
		}
		else return(elem);
	}
	else return(null);
}

//check if we can apply the class to the container
// '*' = all containers allowed
function getAllowedContainer(container,aAllowedContainers)
{
	if(container!=null && container.id!="editor")
	{
		var bAllowed=false;
		for(i=0;i<aAllowedContainers.length;i++)
		{
			if(aAllowedContainers[i]==container.tagName || aAllowedContainers[i]=="*")
			{
				bAllowed=true;
				break;
			}
		}
		if(bAllowed)return(container);
		else return(getAllowedContainer(container.parentNode,aAllowedContainers));
	}
	else return(null);
}

//switch the container to a new one with another type
function setContainer(sType)
{
	container=getSelectedContainer();
	if(container!=null)
	{
		switch(container.tagName)
		{
			case "P":
			case "H1":
			case "H2":
			case "H3":
			case "H4":
			case "H5":
			case "H6":
			case "PRE":
			case "BLOCKQUOTE":
				break;
			default:
				var aTypes=new Array("P","H1","H2","H3","H4","H5","H6","PRE","BLOCKQUOTE");
				container=getContainerOfTypeArray(container,aTypes);
				break;
		}
       	
		if(container!=null)
		{
			var html=container.innerHTML;
			var newNode=document.createElement(sType);
			if(ie)container.replaceNode(newNode);
			else if(moz)container.parentNode.replaceChild(newNode,container);
			newNode.innerHTML=html;
		}
	}
}

//set the class to the container
function setClass(sValue,sAllowedContainers,sConflictingClasses,sAllowedClasses)
{
	var bConflictFound=false;
	var bAllowedFound=(sAllowedClasses=="" || sAllowedClasses==null);
	var container=null;
	var attrClass;
	var sClasses="";
	
	if(sConflictingClasses==null)sConflictingClasses="";
	if(sAllowedClasses==null)sAllowedClasses="";

	//sAllowedContainers : string e.g. "P,DIV,SPAN"
	// '*' = all containers allowed
	var aCt=sAllowedContainers.split(",");

	//find the container (from cursor pos or selected element)
	container=getSelectedContainer();
	
	if(ie) sClasses=container.className;
	if(moz) attrClass=container.attributes.getNamedItem("class");
	if(attrClass!=null) sClasses=attrClass.value;

	//find allowed container
	//if current container isn't allowed, take the parent, and so on ...
	if(container!=null)container=getAllowedContainer(container,aCt);
	
	if(container!=null)
	{
		//check if there isn't a conflict with existent classes
		var aClE=sClasses.split(" "); 							//array of classes already applied to the container
		var aClC=sConflictingClasses.split(",");					//array of conflicting classes
		var aClA=sAllowedClasses.split(",");						//array of compatible classes
		
		if(sClasses=="")bAllowedFound=true;						//if no classes already applied, every class is allowed

		for(var i=0;i<aClE.length;i++)
		{
			for(var j=0;j<aClC.length;j++)
			{
				if((aClC[j]==aClE[i] && aClC[j]!="") || (aClC[j]=="*" && aClE[i]!="" && aClE[i]!=sValue))
				{
					bConflictFound=true;
					break;
				}
			}
			if(bConflictFound)break;
			
			if(!bAllowedFound)
			{
				for(var k=0;k<aClA.length;k++)
				{
					if((aClA[k]==aClE[i] && aClA[k]!="") || (aClA[k]=="*" && aClE[i]!="") || (aClE[i]==sValue))
					{
						bAllowedFound=true;
						break;	
					}
				}
				if(bAllowedFound)break;
			}
		}

		//apply or remove it if no conflict
		if(!bConflictFound && bAllowedFound)
		{
			if(sClasses==sValue || sClasses.indexOf(sValue+" ")>-1 || sClasses.indexOf(" "+sValue)>-1)
			{
				sClasses=sClasses.replace(sValue,"");
				sClasses=sClasses.replace("  "," ");
				sValue=sClasses;
			}
			else
			{
				if(sClasses=="")sClasses=sValue;
				else sValue=sClasses+" "+sValue;
			}

			sValue=sTrim(sValue);
			if(sValue=="")
			{
				if(ie)container.removeAttribute("className");
				else if(moz)container.attributes.removeNamedItem("class");
			}
			else
			{
				if(ie)container.setAttribute("className",sValue,0);
				else if(moz)container.setAttribute("class",sValue);
			}
			displayClasses();
		}
	}
}

//highlight the container's classes
function displayClasses()
{
	var container=getSelectedContainer();
	var nodes=document.getElementById('m_class').getElementsByTagName("A"); //get the buttons from the panel
	for(var i=0;i<nodes.length;i++)
	{
		//clearing
		if(ie)nodes.item(i).setAttribute("className","",0);
		else if(moz)
		{
			if(nodes.item(i).attributes.getNamedItem("class")!=null)
				nodes.item(i).attributes.removeNamedItem("class");
		}
	} 
	
	if(container==null)container=selected(); //an image ?
	if(container!=null)
	{
		var sClasses="";
		var attrClass=container.attributes.getNamedItem("class");
		if(attrClass!=null)sClasses=attrClass.value;
		
		var aClE=sClasses.split(" "); //get the classes names
		for(i=0;i<aClE.length;i++)
		{
			if(aClE[i]!="")
			{
				for(var j=0;j<nodes.length;j++)
				{
					if(nodes.item(j).name==aClE[i])
					{
						if(ie)nodes.item(j).setAttribute("className","active",0); //set the 'active' class
						else if(moz)nodes.item(j).setAttribute("class","active",0);
						break;
					}
				}
			}
		}
	}
}

//remove the class attribute
function removeClassAttr()
{
	var container=getSelectedContainer();
	if(container!=null)
	{
		if(ie) container.removeAttribute("className",false);
		else if(moz) container.removeAttribute("class",false);
	}
}

// !! remove all attributes
function removeAttrs()
{
var container=getSelectedContainer();
	if(container!=null)
	{
		container.clearAttributes();
	}
}

//open a dialog
function openDialog(sDialogType)
{
	var sUrl=dialogs["base"]+dialogs[sDialogType];
	var dialog=window.open(sUrl,"dialog",dialogs_features);
}

//open the preview
function openPreview()
{
	var dialog=window.open(preview_url,"preview",preview_features);
}

//set a unique id to images (when needed)
function setImgIds()
{
	var img=editor().getElementsByTagName("img");
	for(var i=0;i<img.length;i++)
	{
		if(img.id==undefined || img.id==null || img.id=="")img.id=getUniqueId();
	}
}

//handles click events on images
//so we can modify images
function setImgEvent()
{
	var img=editor().getElementsByTagName("img");
	for(var i=0;i<img.length;i++)
	{
		if(ie)
		{
			img[i].onmousedown=function()
			{
				img_mousedown_handler(this);
			}
			img[i].ondblclick=function()
			{
				img_dblclick_handler(this);
			}
		}
		else if(moz)
		{
			img.item(i).addEventListener("mousedown",img_mousedown_handler_moz,false);
			img.item(i).addEventListener("dblclick",img_dblclick_handler_moz,false);
		}
	}
}

function img_mousedown_handler(img)
{
	if(img.id==undefined || img.id==null || img.id=="")img.id=getUniqueId();
	selectedElement=img;
	displayClasses();
}

function img_mousedown_handler_moz()
{
	if(this.id==undefined || this.id==null || this.id=="")this.id=getUniqueId();
	selectedElement=this;
	displayClasses();
}

function img_dblclick_handler(img)
{
	if(img.id==undefined || img.id==null || img.id=="")img.id=getUniqueId();
	selectedElement=img;
	openDialog("image");
}

function img_dblclick_handler_moz()
{
	if(this.id==undefined || this.id==null || this.id=="")this.id=getUniqueId();
	selectedElement=this;
	openDialog("image");
}

//IE specific - Gecko has nested buttons for row and cells creation/removing
//insert a row or column
//sObjectType values : "ROW","COL"
//bBefore : boolean (true : inserts before selected, false : inserts after selected object)
function table_insertObject(sObjectType,bBefore)
{
	var pos=0;
	if(!bBefore)pos=1;

	var container=getSelectedContainer();
	if(container!=null)
	{
		//find the table element
		var table=getContainerOfType(container,"TABLE");
		if(table!=null)
		{
			//find the selected td / tr
			var td=getContainerOfType(container,"TD");
			var tr=getContainerOfType(container,"TR");

			if(tr!=null && td!=null)
			{
				var tdindex=td.cellIndex;
				switch(sObjectType)
				{
					//insert a new row and create cols in it
					case "ROW":
						var newRow=table.insertRow(tr.rowIndex+pos);
						
						for(x=0;x<tr.cells.length;x++)
						{
							newRow.insertCell();
						}
						break;
					//insert a new column in each row
					case "COL":
						for(x=0;x<table.rows.length;x++)
						{
							table.rows[x].insertCell(tdindex+pos);
						}
						break;
					default:
						break;
				}
			}
		}
	}
}

//delete a row or column
//sObjectType values : "ROW","COL"
function table_deleteObject(sObjectType)
{
	var container=getSelectedContainer();
	if(container!=null)
	{
		var table=getContainerOfType(container,"TABLE");
		if(table!=null)
		{
			var td=getContainerOfType(container,"TD");
			var tr=getContainerOfType(container,"TR");

			if(tr!=null && td!=null)
			{
				var tdindex=td.cellIndex;
				switch(sObjectType)
				{
					//delete the row
					case "ROW":
						if(table.rows.length>1)table.deleteRow(tr.rowIndex);
						break;
					//delete each coll at tdindex
					case "COL":
						if(tr.cells.length>1)
						{
							for(x=0;x<table.rows.length;x++)
							{
								table.rows[x].deleteCell(tdindex);
							}
						}
						break;
					default:
						break;
				}
			}
		}
	}
}

//display a visual feedback while copying-cutting-pasting
function displayPasteCleanup(bln)
{
	var elem_on=document.getElementById('m_paste_cleanup_flag_on');
	var elem_off=document.getElementById('m_paste_cleanup_flag_off');
	
	if(bln)
	{
		elem_on.style.display="inline";
		elem_off.style.display="none";
	}
	else
	{
		elem_on.style.display="none";
		elem_off.style.display="inline";
	}
}

//paste data from any application
//onbeforepaste and onpaste events
function pasteData(sData)
{
	if(!bCleanPaste || sData!=null)
	{
		//cancel the default behavior
		if(ie) event.returnValue=false;

		var parent;
		var newNode;
		var sTmp;
		var tmpContainer=null;
		var sPasted=sData;
		
		var container=getSelectedContainer();
		
		//get the data as raw text (no markup)
		if(ie) sPasted=window.clipboardData.getData("Text");
		
		//if we are in a P,heading,..., we get the parentNode
		//if we are in a TD, we add a temporary P, to keep the following code simple
		//(it will be eventually removed)
		switch(container.tagName)
		{
			case "P":
			case "H1":
			case "H2":
			case "H3":
			case "H4":
			case "H5":
			case "H6":
			case "PRE":
			case "BLOCKQUOTE":
				parent=container.parentNode;
				break;
			case "TD":
				parent=container;
				tmpContainer=document.createElement("P");
				container.appendChild(tmpContainer);
				container=tmpContainer;
				break;
			default:
				break;
		}
		
		//we have the parent node
		//let's add the data
		if(parent!=null)
		{
			//split the data, using double newlines as the separator
			var aP;
			if(ie) aP=sPasted.split("\r\n\r\n");
			else if(moz) aP=sPasted.split("\n\n");
	
			//add a P for each item
			for(x=0;x<aP.length;x++)
			{
				newNode=document.createElement("P");
				
				if(container.nextSibling!=null)parent.insertBefore(newNode,container.nextSibling);
				else parent.appendChild(newNode);
	
				sTmp=aP[x];
				
				//simple newlines are replaced by a break
				//maybe we need a trim("\r\n")
				sTmp=sTmp.replace(/\r\n/g,"<br />");
				newNode.innerHTML=sTmp;
				
				//switch the container to add the next P at the good position
				container=newNode;
			}
			
			//remove the temp container (if in a TD)
			if(tmpContainer!=null)tmpContainer.removeNode();
			
			getCleanHTML();
		}
	}
}


//GECKO

//keydown & keyup handlers, mainly used for cleanups

function iframe_keydown_handler(evt)
{
	if(evt.keyCode==86 && evt.ctrlKey) //CTRL+V -> PASTE
	{	
		//prevent CTRL+V
		if(moz_prevent_copy) evt.preventDefault();
	}
}

function iframe_keyup_handler(evt)
{
	var blnFound=false;
	
	if(evt.keyCode==13 && !evt.shiftKey) //RETURN key
	{
		//cleanup <br><br> between paragraphs
		nodes=editor().childNodes;

		for(var x=0;x<nodes.length;x++)
		{
			if(nodes.item(x).nodeName.toLowerCase()=="br" && nodes.item(x+1).nodeName.toLowerCase()=="br")
			{
				editor().removeChild(nodes.item(x));
				blnFound=true;
				break;
			}
		}
		
		if(blnFound) execCom("formatblock","P");
	}
	
	else if(evt.keyCode!=8 && evt.keyCode!=46) //BACKSPACE AND DELETE
	{
		//cleanup not allowed main containers when deleting a P
		
		mainContainer=getMainContainer(getSelectedContainer());
		switch (mainContainer.tagName)
		{
			case "P":
			case "H1":
			case "H2":
			case "H3":
			case "H4":
			case "H5":
			case "H6":
			case "PRE":
			case "BLOCKQUOTE":
			case "TABLE":
				break;
			default:
				execCom("formatblock","P");
				break;
		}
	}
	
	displayClasses();
}

function iframe_mouseup_handler(evt)
{
	if(evt.target.nodeName.toLowerCase()!="img")
	{
		setImgEvent();
		release();
	}
	displayClasses();
}