/*
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
*/
/**
* Collection of functions to manage a rich text editor
*
* @author Matthew Minix matt.minix@gmail.com
* @constructor editorStart
* @version 0.1
* @note xpcom->nsIDOMElement->setCaretAfterElement() //Probably useful later
**/
function WysiwygEditor() {
/* Constants */
// const doesn't work,
this.ELEMENT_NODE = 1;
this.TEXT_NODE = 3;
/* Class Globals */
this.editorDocument = null;
this.editorBody = null;
this.editorWindow = null;
}
WysiwygEditor.prototype = {
/**
* New object listing of CSS values that can be toggled
*
* @returns null
**/
cssItems: function() {
this.textStyles = {
//property: new Array('onValue', 'offValue');
fontStyle: new Array('italics', 'normal'),
fontVariant: new Array('small-caps', 'normal'),
fontWeight: new Array('bold', 'normal'),
borderBottomWidth: new Array('1px', ''),
borderBottomColor: new Array('red', ''),
borderBottomStyle: new Array('solid', '')
//textIndent: pixels, 0;
//textTransform: capitalize, lowercase, uppercase, none
//wordSpacing: new Array('', )
//listStyleImage: url('');
//listStylePosition: /* inside outside */;
//listStyleType: /* square, upper roman none disc circle decimal upper alpha */ ;
//color: black, none;
//letterSpacing: pixel, normal;
//lineHeight: pixel, normal;
//textAlign: right, left, center, justify;
//fontSize: new Array('14px', '');
}
},
//TEXT DECORATION PROBLEM BLAH
/**
* New object listing attributes of (almost) every HTML tag, and what
* can be done to convert them to their XHTML counterparts where possible
*
* @returns null
**/
htmlItems: function() {
/***************
* element: array(
* "0 = drop, 1 = use, 2 = convert",
* "if convert, convert to what",
* "converted style",
* "computedStyles of element",
* "0 = inline, 1 = block, 2 = table, 3 = list, 4 = special",
* "0 = needs closing tag, 1 = self closing, 2 = either",
* "0 = not a form item, 1 = form item"
***************/
const DROP_ELEMENT_CONTENT = -1;
const DROP_ELEMENT = 0;
const USE_ELEMENT = 1;
const CONVERT_ELEMENT = 2;
const INLINE = 0;
const BLOCK = 1;
const TABLE = 2;
const LIST = 3;
const SPECIAL = 4;
const NEEDS_CLOSING_TAG = 0;
const SELF_CLOSING = 1;
const MAY_OR_MAY_NOT_CLOSE = 2;
const NOT_A_FORM_ITEM = 0;
const FORM_ITEM = 1;
this.elements = {
a: new Array(USE_ELEMENT, null, null, INLINE, NEEDS_CLOSING_TAG, NOT_A_FORM_ITEM),
abbr: new Array(USE_ELEMENT, null, null, INLINE, NEEDS_CLOSING_TAG, NOT_A_FORM_ITEM),
acronym: new Array(USE_ELEMENT, null, null, INLINE, NEEDS_CLOSING_TAG, NOT_A_FORM_ITEM),
address: new Array(USE_ELEMENT, null, null, BLOCK, NEEDS_CLOSING_TAG, NOT_A_FORM_ITEM),
applet: new Array(DROP_ELEMENT_CONTENT, null, null, TABLE, NEEDS_CLOSING_TAG, NOT_A_FORM_ITEM),
area: new Array(USE_ELEMENT, null, null, TABLE, MAY_OR_MAY_NOT_CLOSE, NOT_A_FORM_ITEM),
b: new Array(CONVERT_ELEMENT, 'strong', null, INLINE, NEEDS_CLOSING_TAG, NOT_A_FORM_ITEM),
base: new Array(USE_ELEMENT, null, null, TABLE, MAY_OR_MAY_NOT_CLOSE, NOT_A_FORM_ITEM),
basefont: new Array(DROP_ELEMENT, null, null, INLINE, NEEDS_CLOSING_TAG, NOT_A_FORM_ITEM),
bdo: new Array(USE_ELEMENT, null, null, INLINE, NEEDS_CLOSING_TAG, NOT_A_FORM_ITEM),
big: new Array(CONVERT_ELEMENT, 'span', 'font-size: 400%', INLINE, NEEDS_CLOSING_TAG, NOT_A_FORM_ITEM),
blockquote: new Array(USE_ELEMENT, null, null, TABLE, NEEDS_CLOSING_TAG, NOT_A_FORM_ITEM),
br: new Array(USE_ELEMENT, null, null, SPECIAL, SELF_CLOSING, NOT_A_FORM_ITEM),
button: new Array(USE_ELEMENT, null, null, FORM, NEEDS_CLOSING_TAG, NOT_A_FORM_ITEM),
caption: new Array(USE_ELEMENT, null, null, TABLE, NEEDS_CLOSING_TAG, NOT_A_FORM_ITEM),
center: new Array(CONVERT_ELEMENT, 'div', "text-align: center", BLOCK, NEEDS_CLOSING_TAG, NOT_A_FORM_ITEM),
cite: new Array(CONVERT_ELEMENT, 'em', null, INLINE, NEEDS_CLOSING_TAG, NOT_A_FORM_ITEM),
code: new Array(USE_ELEMENT, null, null, BLOCK, NEEDS_CLOSING_TAG, NOT_A_FORM_ITEM),
col: new Array(DROP_ELEMENT, null, null, TABLE, NEEDS_CLOSING_TAG, NOT_A_FORM_ITEM),
colgroup: new Array(DROP_ELEMENT, null, null, TABLE, NEEDS_CLOSING_TAG, NOT_A_FORM_ITEM),
dd: new Array(USE_ELEMENT, null, null, LIST, NEEDS_CLOSING_TAG, NOT_A_FORM_ITEM),
del: new Array(CONVERT_ELEMENT, 'span', 'text-decoration: line-through', INLINE, NEEDS_CLOSING_TAG, NOT_A_FORM_ITEM),
dfn: new Array(CONVERT_ELEMENT, 'em', null, INLINE, NEEDS_CLOSING_TAG, NOT_A_FORM_ITEM),
dir: new Array(CONVERT_ELEMENT, 'ul', null, LIST, NEEDS_CLOSING_TAG, NOT_A_FORM_ITEM),
div: new Array(USE_ELEMENT, null, null, BLOCK, NEEDS_CLOSING_TAG, NOT_A_FORM_ITEM),
dl: new Array(USE_ELEMENT, null, null, LIST, NEEDS_CLOSING_TAG, NOT_A_FORM_ITEM),
dt: new Array(USE_ELEMENT, null, null, LIST, NEEDS_CLOSING_TAG, NOT_A_FORM_ITEM),
em: new Array(USE_ELEMENT, null, null, INLINE, NEEDS_CLOSING_TAG, NOT_A_FORM_ITEM),
embed: new Array(DROP_ELEMENT, null, null, TABLE, NEEDS_CLOSING_TAG, NOT_A_FORM_ITEM),
fieldset: new Array(USE_ELEMENT, null, null, FORM, NEEDS_CLOSING_TAG, NOT_A_FORM_ITEM),
font: new Array(CONVERT_ELEMENT, 'span', null, INLINE, NEEDS_CLOSING_TAG, NOT_A_FORM_ITEM),
form: new Array(USE_ELEMENT, null, null, FORM, NEEDS_CLOSING_TAG, FORM_ITEM),
h1: new Array(USE_ELEMENT, null, null, BLOCK, NEEDS_CLOSING_TAG, NOT_A_FORM_ITEM),
h2: new Array(USE_ELEMENT, null, null, BLOCK, NEEDS_CLOSING_TAG, NOT_A_FORM_ITEM),
h3: new Array(USE_ELEMENT, null, null, BLOCK, NEEDS_CLOSING_TAG, NOT_A_FORM_ITEM),
h4: new Array(USE_ELEMENT, null, null, BLOCK, NEEDS_CLOSING_TAG, NOT_A_FORM_ITEM),
h5: new Array(USE_ELEMENT, null, null, BLOCK, NEEDS_CLOSING_TAG, NOT_A_FORM_ITEM),
h6: new Array(USE_ELEMENT, null, null, BLOCK, NEEDS_CLOSING_TAG, NOT_A_FORM_ITEM),
hr: new Array(USE_ELEMENT, null, null, SPECIAL, SELF_CLOSING, NOT_A_FORM_ITEM),
i: new Array(CONVERT_ELEMENT, 'em', null, INLINE, NEEDS_CLOSING_TAG, NOT_A_FORM_ITEM),
iframe: new Array(USE_ELEMENT, null, null, TABLE, NEEDS_CLOSING_TAG, NOT_A_FORM_ITEM),
img: new Array(USE_ELEMENT, null, null, SPECIAL, SELF_CLOSING, NOT_A_FORM_ITEM),
input: new Array(USE_ELEMENT, null, null, FORM, NEEDS_CLOSING_TAG, FORM_ITEM),
isindex: new Array(CONVERT_ELEMENT, 'form', null, FORM, NEEDS_CLOSING_TAG, NOT_A_FORM_ITEM),
ins: new Array(CONVERT_ELEMENT, 'span', 'text-decoration: underline', INLINE, NEEDS_CLOSING_TAG, NOT_A_FORM_ITEM),
kbd: new Array(CONVERT_ELEMENT, 'code', null, INLINE, NEEDS_CLOSING_TAG, NOT_A_FORM_ITEM),
label: new Array(USE_ELEMENT, null, null, FORM, NEEDS_CLOSING_TAG, NOT_A_FORM_ITEM),
legend: new Array(DROP_ELEMENT, null, null, FORM, NEEDS_CLOSING_TAG, NOT_A_FORM_ITEM),
li: new Array(USE_ELEMENT, null, null, LIST, MAY_OR_MAY_NOT_CLOSE, NOT_A_FORM_ITEM),
map: new Array(USE_ELEMENT, null, null, TABLE, NEEDS_CLOSING_TAG, NOT_A_FORM_ITEM),
menu: new Array(CONVERT_ELEMENT, 'ul', null, LIST, NEEDS_CLOSING_TAG, NOT_A_FORM_ITEM),
noembed: new Array(USE_ELEMENT, null, null, TABLE, NEEDS_CLOSING_TAG, NOT_A_FORM_ITEM),
noscript: new Array(USE_ELEMENT, null, null, SPECIAL, NEEDS_CLOSING_TAG, NOT_A_FORM_ITEM),
object: new Array(USE_ELEMENT, null, null, TABLE, NEEDS_CLOSING_TAG, NOT_A_FORM_ITEM),
ol: new Array(USE_ELEMENT, null, null, LIST, NEEDS_CLOSING_TAG, NOT_A_FORM_ITEM),
optgroup: new Array(USE_ELEMENT, null, null, FORM, NEEDS_CLOSING_TAG, NOT_A_FORM_ITEM),
option: new Array(USE_ELEMENT, null, null, FORM, NEEDS_CLOSING_TAG, FORM_ITEM),
p: new Array(USE_ELEMENT, null, null, BLOCK, MAY_OR_MAY_NOT_CLOSE, NOT_A_FORM_ITEM),
param: new Array(USE_ELEMENT, null, null, TABLE, NEEDS_CLOSING_TAG, NOT_A_FORM_ITEM),
pre: new Array(USE_ELEMENT, null, null, TABLE, NEEDS_CLOSING_TAG, NOT_A_FORM_ITEM),
q: new Array(DROP_ELEMENT, null, null, INLINE, NEEDS_CLOSING_TAG, NOT_A_FORM_ITEM),
s: new Array(CONVERT_ELEMENT, 'span', 'text-decoration: line-through', INLINE, NEEDS_CLOSING_TAG, NOT_A_FORM_ITEM),
samp: new Array(CONVERT_ELEMENT, 'code', null, INLINE, NEEDS_CLOSING_TAG, NOT_A_FORM_ITEM),
select: new Array(USE_ELEMENT, null, null, FORM, NEEDS_CLOSING_TAG, FORM_ITEM),
small: new Array(CONVERT_ELEMENT, 'span', 'font-size: 50%', INLINE, NEEDS_CLOSING_TAG, NOT_A_FORM_ITEM),
span: new Array(USE_ELEMENT, null, null, INLINE, NEEDS_CLOSING_TAG, NOT_A_FORM_ITEM),
strike: new Array(CONVERT_ELEMENT, 'span', 'text-decoration: line-through', INLINE, NEEDS_CLOSING_TAG, NOT_A_FORM_ITEM),
strong: new Array(USE_ELEMENT, null, null, INLINE, NEEDS_CLOSING_TAG, NOT_A_FORM_ITEM),
sub: new Array(CONVERT_ELEMENT, 'span', 'vertical-align: sub', INLINE, NEEDS_CLOSING_TAG, NOT_A_FORM_ITEM),
sup: new Array(CONVERT_ELEMENT, 'span', 'vertical-align: super', INLINE, NEEDS_CLOSING_TAG, NOT_A_FORM_ITEM),
table: new Array(USE_ELEMENT, null, null, TABLE, NEEDS_CLOSING_TAG, NOT_A_FORM_ITEM),
tbody: new Array(DROP_ELEMENT, null, null, TABLE, NEEDS_CLOSING_TAG, NOT_A_FORM_ITEM),
td: new Array(USE_ELEMENT, null, null, TABLE, NEEDS_CLOSING_TAG, NOT_A_FORM_ITEM),
textarea: new Array(USE_ELEMENT, null, null, FORM, NEEDS_CLOSING_TAG, FORM_ITEM),
tfoot: new Array(DROP_ELEMENT, null, null, TABLE, NEEDS_CLOSING_TAG, NOT_A_FORM_ITEM),
th: new Array(USE_ELEMENT, null, null, TABLE, NEEDS_CLOSING_TAG, NOT_A_FORM_ITEM),
thead: new Array(DROP_ELEMENT, null, null, TABLE, NEEDS_CLOSING_TAG, NOT_A_FORM_ITEM),
tr: new Array(USE_ELEMENT, null, null, TABLE, NEEDS_CLOSING_TAG, NOT_A_FORM_ITEM),
tt: new Array(CONVERT_ELEMENT, 'code', null, INLINE, NEEDS_CLOSING_TAG, NOT_A_FORM_ITEM),
u: new Array(CONVERT_ELEMENT, 'span', 'text-decoration: underline', INLINE, NEEDS_CLOSING_TAG, NOT_A_FORM_ITEM),
ul: new Array(USE_ELEMENT, null, null, LIST, NEEDS_CLOSING_TAG, NOT_A_FORM_ITEM),
'var': new Array(CONVERT_ELEMENT, 'em', null, INLINE, NEEDS_CLOSING_TAG, NOT_A_FORM_ITEM)
}
},
/**
* Runs sent action
*
* @param wysiwygAction Action to be run on the editor
* @returns null
**/
textItems: function(wysiwygAction) {
switch(wysiwygAction) {
/**
* IMPORTANT NOTES
* Do not inherit: text-decoration, vertical-align
* Need to be in a block - indent/outdent, align,
* Need to be outside of a block - horizontal line
* Need to surround items - lists
**/
case 'wysiwygTextBold': this.styleToggle('fontWeight', 'bold', '400'); break;
case 'wysiwygTextItalic': this.styleToggle('fontStyle', 'italic', 'normal'); break;
//case 'wysiwygTextUnderline': this.styleToggle('borderBottomColor', 'red', ''); break;
case 'wysiwygTextIndentText': this.styleToggle('borderBottomWidth', '1px', ''); break;
case 'wysiwygTextOutdentText': this.styleToggle('borderBottomStyle', 'solid', '');; break;
case 'wysiwygTextHorizontalLine': this.htmlCommand("inserthorizontalrule"); break;
case 'wysiwygTextCenterAlign': this.htmlCommand("justifycenter"); break;
case 'wysiwygTextLeftAlign': this.htmlCommand("justifyleft"); break;
case 'wysiwygTextRightAlign': this.htmlCommand("justifyright"); break;
case 'wysiwygTextJustifyAlign': this.htmlCommand("justifyfull"); break;
case 'wysiwygTextSubscript': this.htmlCommand("subscript"); break;
case 'wysiwygTextPostscript': this.htmlCommand("superscript"); break;
case 'wysiwygTextUnorderedList': this.htmlCommand("insertunorderedlist"); break;
case 'wysiwygTextOrderedList': this.htmlCommand("insertorderedlist"); break;
case 'wysiwygTextLink':
if (linkText == "") {
alert("No Text Selected!");
} else {
var myUrl = prompt("EnterURL", "http://");
if (myUrl) {
this.htmlCommand("createLink", myUrl);
}
}
break;
case 'wysiwygTextSmallCaps': this.styleToggle('fontVariant', 'small-caps', 'normal'); break;
case 'wysiwygTextSpecialCharacters': this.optimizeHTML(this.editorBody); alert(this.editorDocument.documentElement.innerHTML); break;
case 'wysiwygTextAnchor': break;
}
},
/**
* Starts the editor, and sets the variables of this class.
*
* @constructor
* @returns null
**/
editorStart: function() {
this.editorDocument = document.getElementById('content-frame').contentDocument;
this.editorWindow = document.getElementById('content-frame').contentWindow;
this.editorBody = document.getElementById('content-frame').contentDocument.body;
var editor = this.editorDocument;
editor.designMode = "on";
var body = this.editorBody;
body.style.backgroundColor = "white";
body.style.fontFamily = "arial";
setFocus();
},
/**
* Gives focus to the rich text area
*
* @returns null
**/
setFocus: function() {
setTimeout(function() {
editorDocument.contentWindow.focus();
}, 100);
},
/**
* Finds the common ancestor of two nodes
*
* @param firstNode First node to use for traversing up a tree
* @param secondNode Second node to traverse up a tree
* @returns the common ancestor node, or false if nodes are in seperate tree
**/
findNodesAncestor: function(firstNode, secondNode) {
var startParents = new Array();
var endParents = new Array();
var commonAncestor = null;
var ancestorFound = false;
if(firstNode == secondNode) {
firstNode.parentNode.normalize();
return firstNode;
}
startParents.push(firstNode);
while(firstNode.parentNode.tagName != 'HTML') {
firstNode = firstNode.parentNode;
startParents.push(firstNode);
}
endParents.push(secondNode);
while(secondNode.parentNode.tagName != 'HTML') {
secondNode = secondNode.parentNode;
endParents.push(secondNode);
}
for(var j = 0; j < startParents.length; j++) {
if(ancestorFound) break;
for(var k = 0; k < endParents.length; k++) {
if(startParents[j] == endParents[k]) {
commonAncestor = startParents[j];
commonAncestor.normalize();
ancestorFound = true;
break;
}
}
}
if(ancestorFound)
return commonAncestor;
else
return false;
},
/**
* Finds all nodes inbetween and including startNode and EndNode in an array
*
* @param firstNode Node to start the search
* @param secondNode Node to end the search
* @returns array of node objects
* @todo Generate exception if two nodes have no common ancestor
* @warning Do not use on non HTML XML
**/
nodesInbetween: function(startNode, endNode) {
var commonAncestor, ancestorDescendants;
var descendantLength = 0;
var startNodeFound = false;
var inbetweenNodes = new Array();
commonAncestor = this.findNodesAncestor(startNode, endNode);
ancestorDescendants = this.getChildren(commonAncestor);
descendantLength = ancestorDescendants.length;
for(var i = 0; i < descendantLength; i++) {
if(ancestorDescendants[i] == startNode) {
startNodeFound = true;
}
if(startNodeFound)
inbetweenNodes.push(ancestorDescendants[i]);
if(ancestorDescendants[i] == endNode)
break;
}
return ancestorDescendants;
},
/**
* Takes a text node and it's offsets, and splits into 3 seperate nodes
*
* @param node Text Node to split
* @param startOffset Start of middle node
* @param endOffset End of middle node
* @returns Middle node element
**/
splitTextNode: function(node, startOffset, endOffset) {
var beforeNode, afterNode, newElement, textNode;
var elementContainer = this.editorDocument.createElement('span');
if(!(endOffset)) endOffset = 0;
if(startOffset > 0) {
beforeNode = this.editorDocument.createTextNode(node.textContent.substr(0, startOffset));
elementContainer.appendChild(beforeNode);
}
if((startOffset > 0) && (endOffset > 0)) {
textNode = this.editorDocument.createTextNode(node.textContent.substr(startOffset, endOffset - startOffset));
}
else if(startOffset > 0) {
textNode = this.editorDocument.createTextNode(node.textContent.substr(startOffset));
}
else {
textNode = this.editorDocument.createTextNode(node.textContent.substr(0, endOffset));
}
newElement = this.editorDocument.createElement('span');
newElement.appendChild(textNode);
elementContainer.appendChild(newElement);
if(endOffset > 0) {
afterNode = this.editorDocument.createTextNode(node.textContent.substr(endOffset));
elementContainer.appendChild(afterNode);
}
node.parentNode.replaceChild(elementContainer, node);
return newElement;
},
/**
* Gets all children of sent node.
*
* @param activeNode Node to collect the children of
* @returns Array of Nodes
*
* @warning Not all nodes are tags, this function makes no
* distinction between Element Nodes, and Text Nodes, (or other
* nodes for that matter)
* @warning This function does not return the nodes in a tree
* But as an array, starting with the sent node, and making its
* Way down from top to bottom.
**/
getChildren: function(activeNode) {
var toReturn = new Array(activeNode);
var wasReturned = new Array();
if(activeNode.childNodes.length > 0) {
for(var j = 0; j < activeNode.childNodes.length; j++) {
wasReturned = this.getChildren(activeNode.childNodes[j]); //Returns Array
for(k = 0; k < wasReturned.length; k++)
toReturn.push(wasReturned[k]);
wasReturned = new Array();
}
}
return toReturn;
},
/**
* Removes all excess elements with no style or duplicated style
*
* @param nodeStart Node to optimize the children of
* @returns null
*
* @todo work with all tags, convert deprecated tags (where possible) to
* their new tags/styles.
* @todo If the node has only one element node, and the text
* Nodes are empty, apply styles of the element node to parent node
**/
optimizeHTML: function(nodeStart) {
if(nodeStart.nodeType == this.ELEMENT_NODE) { //Don't care about text nodes.
var cssItems = new this.cssItems();
if(nodeStart.childNodes)
for(var i = 0; i 0) {
deadNode.parentNode.insertBefore(deadNode.firstChild, deadNode);
}
deadNode.parentNode.removeChild(deadNode);
},
/**
* Returns boolean of whether a node is a descendant of another
*
* @param searchNode Node that may or may not be a descendant
* @param ancestorNode Node that may or may not be an ancestor
* @returns boolean
**/
isDescendant: function(searchNode, ancestorNode) {
while(searchNode.tagName) {
if(searchNode == ancestorNode) return true;
searchNode = searchNode.parentNode;
}
return false;
},
/**
* Returns boolean of whether a node is an ancestor of another
*
* @param searchNode Node that may or may not be an ancestor
* @param descendantNode Node that may or may not be a descendant
* @returns boolean
**/
isAncestor: function(searchNode, descendantNode) {
var searchStatus = false;
if(searchNode == descendantNode) return true;
if(searchNode.childNodes) for(var i = 0; i