/* 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