| // htmlArea v3.0 - Copyright (c) 2002-2004 interactivetools.com, inc. |
| // This copyright notice MUST stay intact for use (see license.txt). |
| // |
| // Portions (c) dynarch.com, 2003-2004 |
| // |
| // A free WYSIWYG editor replacement for <textarea> fields. |
| // For full source code and docs, visit http://www.interactivetools.com/ |
| // |
| // Version 3.0 developed by Mihai Bazon. |
| // http://dynarch.com/mishoo |
| // |
| // $Id$ |
| |
| if (typeof _editor_url == "string") { |
| // Leave exactly one backslash at the end of _editor_url |
| _editor_url = _editor_url.replace(/\x2f*$/, '/'); |
| } else { |
| alert("WARNING: _editor_url is not set! You should set this variable to the editor files path; it should preferably be an absolute path, like in '/htmlarea', but it can be relative if you prefer. Further we will try to load the editor files correctly but we'll probably fail."); |
| _editor_url = ''; |
| } |
| |
| // make sure we have a language |
| if (typeof _editor_lang == "string") { |
| _editor_lang = _editor_lang.toLowerCase(); |
| } else { |
| _editor_lang = "en"; |
| } |
| |
| // Creates a new HTMLArea object. Tries to replace the textarea with the given |
| // ID with it. |
| function HTMLArea(textarea, config) { |
| if (HTMLArea.checkSupportedBrowser()) { |
| if (typeof config == "undefined") { |
| this.config = new HTMLArea.Config(); |
| } else { |
| this.config = config; |
| } |
| this._htmlArea = null; |
| this._textArea = textarea; |
| this._editMode = "wysiwyg"; |
| this.plugins = {}; |
| this._timerToolbar = null; |
| this._timerUndo = null; |
| this._undoQueue = new Array(this.config.undoSteps); |
| this._undoPos = -1; |
| this._customUndo = false; |
| this._mdoc = document; // cache the document, we need it in plugins |
| this.doctype = ''; |
| } |
| }; |
| |
| // load some scripts |
| (function() { |
| var scripts = HTMLArea._scripts = [ _editor_url + "htmlarea.js", |
| _editor_url + "dialog.js", |
| _editor_url + "popupwin.js", |
| _editor_url + "lang/" + _editor_lang + ".js" ]; |
| var head = document.getElementsByTagName("head")[0]; |
| // start from 1, htmlarea.js is already loaded |
| for (var i = 1; i < scripts.length; ++i) { |
| var script = document.createElement("script"); |
| script.src = scripts[i]; |
| head.appendChild(script); |
| } |
| })(); |
| |
| // cache some regexps |
| HTMLArea.RE_tagName = /(<\/|<)\s*([^ \t\n>]+)/ig; |
| HTMLArea.RE_doctype = /(<!doctype((.|\n)*?)>)\n?/i; |
| HTMLArea.RE_head = /<head>((.|\n)*?)<\/head>/i; |
| HTMLArea.RE_body = /<body>((.|\n)*?)<\/body>/i; |
| |
| HTMLArea.Config = function () { |
| this.version = "3.0"; |
| |
| this.width = "auto"; |
| this.height = "auto"; |
| |
| // enable creation of a status bar? |
| this.statusBar = true; |
| |
| // maximum size of the undo queue |
| this.undoSteps = 20; |
| |
| // the time interval at which undo samples are taken |
| this.undoTimeout = 500; // 1/2 sec. |
| |
| // the next parameter specifies whether the toolbar should be included |
| // in the size or not. |
| this.sizeIncludesToolbar = true; |
| |
| // if true then HTMLArea will retrieve the full HTML, starting with the |
| // <HTML> tag. |
| this.fullPage = false; |
| |
| // style included in the iframe document |
| this.pageStyle = ""; |
| |
| // set to true if you want Word code to be cleaned upon Paste |
| this.killWordOnPaste = false; |
| |
| // BaseURL included in the iframe document |
| this.baseURL = document.baseURI || document.URL; |
| if (this.baseURL && this.baseURL.match(/(.*)\/([^\/]+)/)) |
| this.baseURL = RegExp.$1 + "/"; |
| |
| // URL-s |
| this.imgURL = "images/"; |
| this.popupURL = "popups/"; |
| |
| /** CUSTOMIZING THE TOOLBAR |
| * ------------------------- |
| * |
| * It is recommended that you customize the toolbar contents in an |
| * external file (i.e. the one calling HTMLArea) and leave this one |
| * unchanged. That's because when we (InteractiveTools.com) release a |
| * new official version, it's less likely that you will have problems |
| * upgrading HTMLArea. |
| */ |
| this.toolbar = [ |
| [ "fontname", "space", |
| "fontsize", "space", |
| "formatblock", "space", |
| "bold", "italic", "underline", "strikethrough", "separator", |
| "subscript", "superscript", "separator", |
| "copy", "cut", "paste", "space", "undo", "redo" ], |
| |
| [ "justifyleft", "justifycenter", "justifyright", "justifyfull", "separator", |
| "lefttoright", "righttoleft", "separator", |
| "insertorderedlist", "insertunorderedlist", "outdent", "indent", "separator", |
| "forecolor", "hilitecolor", "separator", |
| "inserthorizontalrule", "createlink", "insertimage", "inserttable", "htmlmode", "separator", |
| "popupeditor", "separator", "showhelp", "about" ] |
| ]; |
| |
| this.fontname = { |
| "Arial": 'arial,helvetica,sans-serif', |
| "Courier New": 'courier new,courier,monospace', |
| "Georgia": 'georgia,times new roman,times,serif', |
| "Tahoma": 'tahoma,arial,helvetica,sans-serif', |
| "Times New Roman": 'times new roman,times,serif', |
| "Verdana": 'verdana,arial,helvetica,sans-serif', |
| "impact": 'impact', |
| "WingDings": 'wingdings' |
| }; |
| |
| this.fontsize = { |
| "1 (8 pt)": "1", |
| "2 (10 pt)": "2", |
| "3 (12 pt)": "3", |
| "4 (14 pt)": "4", |
| "5 (18 pt)": "5", |
| "6 (24 pt)": "6", |
| "7 (36 pt)": "7" |
| }; |
| |
| this.formatblock = { |
| "Heading 1": "h1", |
| "Heading 2": "h2", |
| "Heading 3": "h3", |
| "Heading 4": "h4", |
| "Heading 5": "h5", |
| "Heading 6": "h6", |
| "Normal": "p", |
| "Address": "address", |
| "Formatted": "pre" |
| }; |
| |
| this.customSelects = {}; |
| |
| function cut_copy_paste(e, cmd, obj) { |
| e.execCommand(cmd); |
| }; |
| |
| // ADDING CUSTOM BUTTONS: please read below! |
| // format of the btnList elements is "ID: [ ToolTip, Icon, Enabled in text mode?, ACTION ]" |
| // - ID: unique ID for the button. If the button calls document.execCommand |
| // it's wise to give it the same name as the called command. |
| // - ACTION: function that gets called when the button is clicked. |
| // it has the following prototype: |
| // function(editor, buttonName) |
| // - editor is the HTMLArea object that triggered the call |
| // - buttonName is the ID of the clicked button |
| // These 2 parameters makes it possible for you to use the same |
| // handler for more HTMLArea objects or for more different buttons. |
| // - ToolTip: default tooltip, for cases when it is not defined in the -lang- file (HTMLArea.I18N) |
| // - Icon: path to an icon image file for the button (TODO: use one image for all buttons!) |
| // - Enabled in text mode: if false the button gets disabled for text-only mode; otherwise enabled all the time. |
| this.btnList = { |
| bold: [ "Bold", "ed_format_bold.gif", false, function(e) {e.execCommand("bold");} ], |
| italic: [ "Italic", "ed_format_italic.gif", false, function(e) {e.execCommand("italic");} ], |
| underline: [ "Underline", "ed_format_underline.gif", false, function(e) {e.execCommand("underline");} ], |
| strikethrough: [ "Strikethrough", "ed_format_strike.gif", false, function(e) {e.execCommand("strikethrough");} ], |
| subscript: [ "Subscript", "ed_format_sub.gif", false, function(e) {e.execCommand("subscript");} ], |
| superscript: [ "Superscript", "ed_format_sup.gif", false, function(e) {e.execCommand("superscript");} ], |
| justifyleft: [ "Justify Left", "ed_align_left.gif", false, function(e) {e.execCommand("justifyleft");} ], |
| justifycenter: [ "Justify Center", "ed_align_center.gif", false, function(e) {e.execCommand("justifycenter");} ], |
| justifyright: [ "Justify Right", "ed_align_right.gif", false, function(e) {e.execCommand("justifyright");} ], |
| justifyfull: [ "Justify Full", "ed_align_justify.gif", false, function(e) {e.execCommand("justifyfull");} ], |
| insertorderedlist: [ "Ordered List", "ed_list_num.gif", false, function(e) {e.execCommand("insertorderedlist");} ], |
| insertunorderedlist: [ "Bulleted List", "ed_list_bullet.gif", false, function(e) {e.execCommand("insertunorderedlist");} ], |
| outdent: [ "Decrease Indent", "ed_indent_less.gif", false, function(e) {e.execCommand("outdent");} ], |
| indent: [ "Increase Indent", "ed_indent_more.gif", false, function(e) {e.execCommand("indent");} ], |
| forecolor: [ "Font Color", "ed_color_fg.gif", false, function(e) {e.execCommand("forecolor");} ], |
| hilitecolor: [ "Background Color", "ed_color_bg.gif", false, function(e) {e.execCommand("hilitecolor");} ], |
| inserthorizontalrule: [ "Horizontal Rule", "ed_hr.gif", false, function(e) {e.execCommand("inserthorizontalrule");} ], |
| createlink: [ "Insert Web Link", "ed_link.gif", false, function(e) {e.execCommand("createlink", true);} ], |
| insertimage: [ "Insert/Modify Image", "ed_image.gif", false, function(e) {e.execCommand("insertimage");} ], |
| inserttable: [ "Insert Table", "insert_table.gif", false, function(e) {e.execCommand("inserttable");} ], |
| htmlmode: [ "Toggle HTML Source", "ed_html.gif", true, function(e) {e.execCommand("htmlmode");} ], |
| popupeditor: [ "Enlarge Editor", "fullscreen_maximize.gif", true, function(e) {e.execCommand("popupeditor");} ], |
| about: [ "About this editor", "ed_about.gif", true, function(e) {e.execCommand("about");} ], |
| showhelp: [ "Help using editor", "ed_help.gif", true, function(e) {e.execCommand("showhelp");} ], |
| undo: [ "Undoes your last action", "ed_undo.gif", false, function(e) {e.execCommand("undo");} ], |
| redo: [ "Redoes your last action", "ed_redo.gif", false, function(e) {e.execCommand("redo");} ], |
| cut: [ "Cut selection", "ed_cut.gif", false, cut_copy_paste ], |
| copy: [ "Copy selection", "ed_copy.gif", false, cut_copy_paste ], |
| paste: [ "Paste from clipboard", "ed_paste.gif", false, cut_copy_paste ], |
| lefttoright: [ "Direction left to right", "ed_left_to_right.gif", false, function(e) {e.execCommand("lefttoright");} ], |
| righttoleft: [ "Direction right to left", "ed_right_to_left.gif", false, function(e) {e.execCommand("righttoleft");} ] |
| }; |
| /* ADDING CUSTOM BUTTONS |
| * --------------------- |
| * |
| * It is recommended that you add the custom buttons in an external |
| * file and leave this one unchanged. That's because when we |
| * (InteractiveTools.com) release a new official version, it's less |
| * likely that you will have problems upgrading HTMLArea. |
| * |
| * Example on how to add a custom button when you construct the HTMLArea: |
| * |
| * var editor = new HTMLArea("your_text_area_id"); |
| * var cfg = editor.config; // this is the default configuration |
| * cfg.btnList["my-hilite"] = |
| * [ function(editor) { editor.surroundHTML('<span style="background:yellow">', '</span>'); }, // action |
| * "Highlight selection", // tooltip |
| * "my_hilite.gif", // image |
| * false // disabled in text mode |
| * ]; |
| * cfg.toolbar.push(["linebreak", "my-hilite"]); // add the new button to the toolbar |
| * |
| * An alternate (also more convenient and recommended) way to |
| * accomplish this is to use the registerButton function below. |
| */ |
| // initialize tooltips from the I18N module and generate correct image path |
| for (var i in this.btnList) { |
| var btn = this.btnList[i]; |
| btn[1] = _editor_url + this.imgURL + btn[1]; |
| if (typeof HTMLArea.I18N.tooltips[i] != "undefined") { |
| btn[0] = HTMLArea.I18N.tooltips[i]; |
| } |
| } |
| }; |
| |
| /** Helper function: register a new button with the configuration. It can be |
| * called with all 5 arguments, or with only one (first one). When called with |
| * only one argument it must be an object with the following properties: id, |
| * tooltip, image, textMode, action. Examples: |
| * |
| * 1. config.registerButton("my-hilite", "Hilite text", "my-hilite.gif", false, function(editor) {...}); |
| * 2. config.registerButton({ |
| * id : "my-hilite", // the ID of your button |
| * tooltip : "Hilite text", // the tooltip |
| * image : "my-hilite.gif", // image to be displayed in the toolbar |
| * textMode : false, // disabled in text mode |
| * action : function(editor) { // called when the button is clicked |
| * editor.surroundHTML('<span class="hilite">', '</span>'); |
| * }, |
| * context : "p" // will be disabled if outside a <p> element |
| * }); |
| */ |
| HTMLArea.Config.prototype.registerButton = function(id, tooltip, image, textMode, action, context) { |
| var the_id; |
| if (typeof id == "string") { |
| the_id = id; |
| } else if (typeof id == "object") { |
| the_id = id.id; |
| } else { |
| alert("ERROR [HTMLArea.Config::registerButton]:\ninvalid arguments"); |
| return false; |
| } |
| // check for existing id |
| if (typeof this.customSelects[the_id] != "undefined") { |
| // alert("WARNING [HTMLArea.Config::registerDropdown]:\nA dropdown with the same ID already exists."); |
| } |
| if (typeof this.btnList[the_id] != "undefined") { |
| // alert("WARNING [HTMLArea.Config::registerDropdown]:\nA button with the same ID already exists."); |
| } |
| switch (typeof id) { |
| case "string": this.btnList[id] = [ tooltip, image, textMode, action, context ]; break; |
| case "object": this.btnList[id.id] = [ id.tooltip, id.image, id.textMode, id.action, id.context ]; break; |
| } |
| }; |
| |
| /** The following helper function registers a dropdown box with the editor |
| * configuration. You still have to add it to the toolbar, same as with the |
| * buttons. Call it like this: |
| * |
| * FIXME: add example |
| */ |
| HTMLArea.Config.prototype.registerDropdown = function(object) { |
| // check for existing id |
| if (typeof this.customSelects[object.id] != "undefined") { |
| // alert("WARNING [HTMLArea.Config::registerDropdown]:\nA dropdown with the same ID already exists."); |
| } |
| if (typeof this.btnList[object.id] != "undefined") { |
| // alert("WARNING [HTMLArea.Config::registerDropdown]:\nA button with the same ID already exists."); |
| } |
| this.customSelects[object.id] = object; |
| }; |
| |
| /** Call this function to remove some buttons/drop-down boxes from the toolbar. |
| * Pass as the only parameter a string containing button/drop-down names |
| * delimited by spaces. Note that the string should also begin with a space |
| * and end with a space. Example: |
| * |
| * config.hideSomeButtons(" fontname fontsize textindicator "); |
| * |
| * It's useful because it's easier to remove stuff from the defaul toolbar than |
| * create a brand new toolbar ;-) |
| */ |
| HTMLArea.Config.prototype.hideSomeButtons = function(remove) { |
| var toolbar = this.toolbar; |
| for (var i = 0; i < toolbar.length; i++) { //in toolbar) { |
| var line = toolbar[i]; |
| for (var j = line.length; --j >= 0; ) { |
| if (remove.indexOf(" " + line[j] + " ") >= 0) { |
| var len = 1; |
| if (/separator|space/.test(line[j + 1])) { |
| len = 2; |
| } |
| line.splice(j, len); |
| } |
| } |
| } |
| }; |
| |
| /** Helper function: replace all TEXTAREA-s in the document with HTMLArea-s. */ |
| HTMLArea.replaceAll = function(config) { |
| var tas = document.getElementsByTagName("textarea"); |
| for (var i = tas.length; i > 0; (new HTMLArea(tas[--i], config)).generate()); |
| }; |
| |
| /** Helper function: replaces the TEXTAREA with the given ID with HTMLArea. */ |
| HTMLArea.replace = function(id, config) { |
| var ta = HTMLArea.getElementById("textarea", id); |
| return ta ? (new HTMLArea(ta, config)).generate() : null; |
| }; |
| |
| // Creates the toolbar and appends it to the _htmlarea |
| HTMLArea.prototype._createToolbar = function () { |
| var editor = this; // to access this in nested functions |
| |
| var toolbar = document.createElement("div"); |
| this._toolbar = toolbar; |
| toolbar.className = "toolbar"; |
| toolbar.unselectable = "1"; |
| var tb_row = null; |
| var tb_objects = new Object(); |
| this._toolbarObjects = tb_objects; |
| |
| // creates a new line in the toolbar |
| function newLine() { |
| var table = document.createElement("table"); |
| table.border = "0px"; |
| table.cellSpacing = "0px"; |
| table.cellPadding = "0px"; |
| toolbar.appendChild(table); |
| // TBODY is required for IE, otherwise you don't see anything |
| // in the TABLE. |
| var tb_body = document.createElement("tbody"); |
| table.appendChild(tb_body); |
| tb_row = document.createElement("tr"); |
| tb_body.appendChild(tb_row); |
| }; // END of function: newLine |
| // init first line |
| newLine(); |
| |
| // updates the state of a toolbar element. This function is member of |
| // a toolbar element object (unnamed objects created by createButton or |
| // createSelect functions below). |
| function setButtonStatus(id, newval) { |
| var oldval = this[id]; |
| var el = this.element; |
| if (oldval != newval) { |
| switch (id) { |
| case "enabled": |
| if (newval) { |
| HTMLArea._removeClass(el, "buttonDisabled"); |
| el.disabled = false; |
| } else { |
| HTMLArea._addClass(el, "buttonDisabled"); |
| el.disabled = true; |
| } |
| break; |
| case "active": |
| if (newval) { |
| HTMLArea._addClass(el, "buttonPressed"); |
| } else { |
| HTMLArea._removeClass(el, "buttonPressed"); |
| } |
| break; |
| } |
| this[id] = newval; |
| } |
| }; // END of function: setButtonStatus |
| |
| // this function will handle creation of combo boxes. Receives as |
| // parameter the name of a button as defined in the toolBar config. |
| // This function is called from createButton, above, if the given "txt" |
| // doesn't match a button. |
| function createSelect(txt) { |
| var options = null; |
| var el = null; |
| var cmd = null; |
| var customSelects = editor.config.customSelects; |
| var context = null; |
| switch (txt) { |
| case "fontsize": |
| case "fontname": |
| case "formatblock": |
| // the following line retrieves the correct |
| // configuration option because the variable name |
| // inside the Config object is named the same as the |
| // button/select in the toolbar. For instance, if txt |
| // == "formatblock" we retrieve config.formatblock (or |
| // a different way to write it in JS is |
| // config["formatblock"]. |
| options = editor.config[txt]; |
| cmd = txt; |
| break; |
| default: |
| // try to fetch it from the list of registered selects |
| cmd = txt; |
| var dropdown = customSelects[cmd]; |
| if (typeof dropdown != "undefined") { |
| options = dropdown.options; |
| context = dropdown.context; |
| } else { |
| alert("ERROR [createSelect]:\nCan't find the requested dropdown definition"); |
| } |
| break; |
| } |
| if (options) { |
| el = document.createElement("select"); |
| var obj = { |
| name : txt, // field name |
| element : el, // the UI element (SELECT) |
| enabled : true, // is it enabled? |
| text : false, // enabled in text mode? |
| cmd : cmd, // command ID |
| state : setButtonStatus, // for changing state |
| context : context |
| }; |
| tb_objects[txt] = obj; |
| for (var i in options) { |
| var op = document.createElement("option"); |
| op.appendChild(document.createTextNode(i)); |
| op.value = options[i]; |
| el.appendChild(op); |
| } |
| HTMLArea._addEvent(el, "change", function () { |
| editor._comboSelected(el, txt); |
| }); |
| } |
| return el; |
| }; // END of function: createSelect |
| |
| // appends a new button to toolbar |
| function createButton(txt) { |
| // the element that will be created |
| var el = null; |
| var btn = null; |
| switch (txt) { |
| case "separator": |
| el = document.createElement("div"); |
| el.className = "separator"; |
| break; |
| case "space": |
| el = document.createElement("div"); |
| el.className = "space"; |
| break; |
| case "linebreak": |
| newLine(); |
| return false; |
| case "textindicator": |
| el = document.createElement("div"); |
| el.appendChild(document.createTextNode("A")); |
| el.className = "indicator"; |
| el.title = HTMLArea.I18N.tooltips.textindicator; |
| var obj = { |
| name : txt, // the button name (i.e. 'bold') |
| element : el, // the UI element (DIV) |
| enabled : true, // is it enabled? |
| active : false, // is it pressed? |
| text : false, // enabled in text mode? |
| cmd : "textindicator", // the command ID |
| state : setButtonStatus // for changing state |
| }; |
| tb_objects[txt] = obj; |
| break; |
| default: |
| btn = editor.config.btnList[txt]; |
| } |
| if (!el && btn) { |
| el = document.createElement("div"); |
| el.title = btn[0]; |
| el.className = "button"; |
| // let's just pretend we have a button object, and |
| // assign all the needed information to it. |
| var obj = { |
| name : txt, // the button name (i.e. 'bold') |
| element : el, // the UI element (DIV) |
| enabled : true, // is it enabled? |
| active : false, // is it pressed? |
| text : btn[2], // enabled in text mode? |
| cmd : btn[3], // the command ID |
| state : setButtonStatus, // for changing state |
| context : btn[4] || null // enabled in a certain context? |
| }; |
| tb_objects[txt] = obj; |
| // handlers to emulate nice flat toolbar buttons |
| HTMLArea._addEvent(el, "mouseover", function () { |
| if (obj.enabled) { |
| HTMLArea._addClass(el, "buttonHover"); |
| } |
| }); |
| HTMLArea._addEvent(el, "mouseout", function () { |
| if (obj.enabled) with (HTMLArea) { |
| _removeClass(el, "buttonHover"); |
| _removeClass(el, "buttonActive"); |
| (obj.active) && _addClass(el, "buttonPressed"); |
| } |
| }); |
| HTMLArea._addEvent(el, "mousedown", function (ev) { |
| if (obj.enabled) with (HTMLArea) { |
| _addClass(el, "buttonActive"); |
| _removeClass(el, "buttonPressed"); |
| _stopEvent(is_ie ? window.event : ev); |
| } |
| }); |
| // when clicked, do the following: |
| HTMLArea._addEvent(el, "click", function (ev) { |
| if (obj.enabled) with (HTMLArea) { |
| _removeClass(el, "buttonActive"); |
| _removeClass(el, "buttonHover"); |
| obj.cmd(editor, obj.name, obj); |
| _stopEvent(is_ie ? window.event : ev); |
| } |
| }); |
| var img = document.createElement("img"); |
| img.src = btn[1]; |
| img.style.width = "18px"; |
| img.style.height = "18px"; |
| el.appendChild(img); |
| } else if (!el) { |
| el = createSelect(txt); |
| } |
| if (el) { |
| var tb_cell = document.createElement("td"); |
| tb_row.appendChild(tb_cell); |
| tb_cell.appendChild(el); |
| } else { |
| alert("FIXME: Unknown toolbar item: " + txt); |
| } |
| return el; |
| }; |
| |
| var first = true; |
| for (var i = 0; i < this.config.toolbar.length; i++) {// in this.config.toolbar) { |
| if (!first) { |
| createButton("linebreak"); |
| } else { |
| first = false; |
| } |
| var group = this.config.toolbar[i]; |
| for (var j = 0; j < group.length; j++) {// in group) { |
| var code = group[j]; |
| if (/^([IT])\[(.*?)\]/.test(code)) { |
| // special case, create text label |
| var l7ed = RegExp.$1 == "I"; // localized? |
| var label = RegExp.$2; |
| if (l7ed) { |
| label = HTMLArea.I18N.custom[label]; |
| } |
| var tb_cell = document.createElement("td"); |
| tb_row.appendChild(tb_cell); |
| tb_cell.className = "label"; |
| tb_cell.innerHTML = label; |
| } else { |
| createButton(code); |
| } |
| } |
| } |
| |
| this._htmlArea.appendChild(toolbar); |
| }; |
| |
| HTMLArea.prototype._createStatusBar = function() { |
| var statusbar = document.createElement("div"); |
| statusbar.className = "statusBar"; |
| this._htmlArea.appendChild(statusbar); |
| this._statusBar = statusbar; |
| // statusbar.appendChild(document.createTextNode(HTMLArea.I18N.msg["Path"] + ": ")); |
| // creates a holder for the path view |
| div = document.createElement("span"); |
| div.className = "statusBarTree"; |
| div.innerHTML = HTMLArea.I18N.msg["Path"] + ": "; |
| this._statusBarTree = div; |
| this._statusBar.appendChild(div); |
| if (!this.config.statusBar) { |
| // disable it... |
| statusbar.style.display = "none"; |
| } |
| }; |
| |
| // Creates the HTMLArea object and replaces the textarea with it. |
| HTMLArea.prototype.generate = function () { |
| var editor = this; // we'll need "this" in some nested functions |
| // get the textarea |
| var textarea = this._textArea; |
| if (typeof textarea == "string") { |
| // it's not element but ID |
| this._textArea = textarea = HTMLArea.getElementById("textarea", textarea); |
| } |
| this._ta_size = { |
| w: textarea.offsetWidth, |
| h: textarea.offsetHeight |
| }; |
| textarea.style.display = "none"; |
| |
| // create the editor framework |
| var htmlarea = document.createElement("div"); |
| htmlarea.className = "htmlarea"; |
| this._htmlArea = htmlarea; |
| |
| // insert the editor before the textarea. |
| textarea.parentNode.insertBefore(htmlarea, textarea); |
| |
| if (textarea.form) { |
| // we have a form, on submit get the HTMLArea content and |
| // update original textarea. |
| var f = textarea.form; |
| if (typeof f.onsubmit == "function") { |
| var funcref = f.onsubmit; |
| if (typeof f.__msh_prevOnSubmit == "undefined") { |
| f.__msh_prevOnSubmit = []; |
| } |
| f.__msh_prevOnSubmit.push(funcref); |
| } |
| f.onsubmit = function() { |
| editor._textArea.value = editor.getHTML(); |
| var a = this.__msh_prevOnSubmit; |
| // call previous submit methods if they were there. |
| if (typeof a != "undefined") { |
| for (var i in a) { |
| a[i](); |
| } |
| } |
| }; |
| } |
| |
| // add a handler for the "back/forward" case -- on body.unload we save |
| // the HTML content into the original textarea. |
| window.onunload = function() { |
| editor._textArea.value = editor.getHTML(); |
| }; |
| |
| // creates & appends the toolbar |
| this._createToolbar(); |
| |
| // create the IFRAME |
| var iframe = document.createElement("iframe"); |
| htmlarea.appendChild(iframe); |
| |
| this._iframe = iframe; |
| |
| // creates & appends the status bar, if the case |
| this._createStatusBar(); |
| |
| // remove the default border as it keeps us from computing correctly |
| // the sizes. (somebody tell me why doesn't this work in IE) |
| |
| if (!HTMLArea.is_ie) { |
| iframe.style.borderWidth = "1px"; |
| // iframe.frameBorder = "1"; |
| // iframe.marginHeight = "0"; |
| // iframe.marginWidth = "0"; |
| } |
| |
| // size the IFRAME according to user's prefs or initial textarea |
| var height = (this.config.height == "auto" ? (this._ta_size.h + "px") : this.config.height); |
| height = parseInt(height); |
| var width = (this.config.width == "auto" ? (this._ta_size.w + "px") : this.config.width); |
| width = parseInt(width); |
| |
| if (!HTMLArea.is_ie) { |
| height -= 2; |
| width -= 2; |
| } |
| |
| iframe.style.width = width + "px"; |
| if (this.config.sizeIncludesToolbar) { |
| // substract toolbar height |
| height -= this._toolbar.offsetHeight; |
| height -= this._statusBar.offsetHeight; |
| } |
| if (height < 0) { |
| height = 0; |
| } |
| iframe.style.height = height + "px"; |
| |
| // the editor including the toolbar now have the same size as the |
| // original textarea.. which means that we need to reduce that a bit. |
| textarea.style.width = iframe.style.width; |
| textarea.style.height = iframe.style.height; |
| |
| // IMPORTANT: we have to allow Mozilla a short time to recognize the |
| // new frame. Otherwise we get a stupid exception. |
| function initIframe() { |
| var doc = editor._iframe.contentWindow.document; |
| if (!doc) { |
| // Try again.. |
| // FIXME: don't know what else to do here. Normally |
| // we'll never reach this point. |
| if (HTMLArea.is_gecko) { |
| setTimeout(initIframe, 100); |
| return false; |
| } else { |
| alert("ERROR: IFRAME can't be initialized."); |
| } |
| } |
| if (HTMLArea.is_gecko) { |
| // enable editable mode for Mozilla |
| doc.designMode = "on"; |
| } |
| editor._doc = doc; |
| if (!editor.config.fullPage) { |
| doc.open(); |
| var html = "<html>\n"; |
| html += "<head>\n"; |
| if (editor.config.baseURL) |
| html += '<base href="' + editor.config.baseURL + '" />'; |
| html += "<style> html,body { border: 0px; } " + |
| editor.config.pageStyle + "</style>\n"; |
| html += "</head>\n"; |
| html += "<body>\n"; |
| html += editor._textArea.value; |
| html += "</body>\n"; |
| html += "</html>"; |
| doc.write(html); |
| doc.close(); |
| } else { |
| var html = editor._textArea.value; |
| if (html.match(HTMLArea.RE_doctype)) { |
| editor.setDoctype(RegExp.$1); |
| html = html.replace(HTMLArea.RE_doctype, ""); |
| } |
| doc.open(); |
| doc.write(html); |
| doc.close(); |
| } |
| |
| if (HTMLArea.is_ie) { |
| // enable editable mode for IE. For some reason this |
| // doesn't work if done in the same place as for Gecko |
| // (above). |
| doc.body.contentEditable = true; |
| } |
| |
| editor.focusEditor(); |
| // intercept some events; for updating the toolbar & keyboard handlers |
| HTMLArea._addEvents |
| (doc, ["keydown", "keypress", "mousedown", "mouseup", "drag"], |
| function (event) { |
| return editor._editorEvent(HTMLArea.is_ie ? editor._iframe.contentWindow.event : event); |
| }); |
| |
| // check if any plugins have registered refresh handlers |
| for (var i in editor.plugins) { |
| var plugin = editor.plugins[i].instance; |
| if (typeof plugin.onGenerate == "function") |
| plugin.onGenerate(); |
| } |
| |
| setTimeout(function() { |
| editor.updateToolbar(); |
| }, 250); |
| |
| if (typeof editor.onGenerate == "function") |
| editor.onGenerate(); |
| }; |
| setTimeout(initIframe, 100); |
| }; |
| |
| // Switches editor mode; parameter can be "textmode" or "wysiwyg". If no |
| // parameter was passed this function toggles between modes. |
| HTMLArea.prototype.setMode = function(mode) { |
| if (typeof mode == "undefined") { |
| mode = ((this._editMode == "textmode") ? "wysiwyg" : "textmode"); |
| } |
| switch (mode) { |
| case "textmode": |
| this._textArea.value = this.getHTML(); |
| this._iframe.style.display = "none"; |
| this._textArea.style.display = "block"; |
| if (this.config.statusBar) { |
| this._statusBar.innerHTML = HTMLArea.I18N.msg["TEXT_MODE"]; |
| } |
| break; |
| case "wysiwyg": |
| if (HTMLArea.is_gecko) { |
| // disable design mode before changing innerHTML |
| try { |
| this._doc.designMode = "off"; |
| } catch(e) {}; |
| } |
| if (!this.config.fullPage) |
| this._doc.body.innerHTML = this.getHTML(); |
| else |
| this.setFullHTML(this.getHTML()); |
| this._iframe.style.display = "block"; |
| this._textArea.style.display = "none"; |
| if (HTMLArea.is_gecko) { |
| // we need to refresh that info for Moz-1.3a |
| try { |
| this._doc.designMode = "on"; |
| } catch(e) {}; |
| } |
| if (this.config.statusBar) { |
| this._statusBar.innerHTML = ''; |
| this._statusBar.appendChild(document.createTextNode(HTMLArea.I18N.msg["Path"] + ": ")); |
| this._statusBar.appendChild(this._statusBarTree); |
| } |
| break; |
| default: |
| alert("Mode <" + mode + "> not defined!"); |
| return false; |
| } |
| this._editMode = mode; |
| this.focusEditor(); |
| }; |
| |
| HTMLArea.prototype.setFullHTML = function(html) { |
| var save_multiline = RegExp.multiline; |
| RegExp.multiline = true; |
| if (html.match(HTMLArea.RE_doctype)) { |
| this.setDoctype(RegExp.$1); |
| html = html.replace(HTMLArea.RE_doctype, ""); |
| } |
| RegExp.multiline = save_multiline; |
| if (!HTMLArea.is_ie) { |
| if (html.match(HTMLArea.RE_head)) |
| this._doc.getElementsByTagName("head")[0].innerHTML = RegExp.$1; |
| if (html.match(HTMLArea.RE_body)) |
| this._doc.getElementsByTagName("body")[0].innerHTML = RegExp.$1; |
| } else { |
| var html_re = /<html>((.|\n)*?)<\/html>/i; |
| html = html.replace(html_re, "$1"); |
| this._doc.open(); |
| this._doc.write(html); |
| this._doc.close(); |
| this._doc.body.contentEditable = true; |
| return true; |
| } |
| }; |
| |
| /*************************************************** |
| * Category: PLUGINS |
| ***************************************************/ |
| |
| // this is the variant of the function above where the plugin arguments are |
| // already packed in an array. Externally, it should be only used in the |
| // full-screen editor code, in order to initialize plugins with the same |
| // parameters as in the opener window. |
| HTMLArea.prototype.registerPlugin2 = function(plugin, args) { |
| if (typeof plugin == "string") |
| plugin = eval(plugin); |
| var obj = new plugin(this, args); |
| if (obj) { |
| var clone = {}; |
| var info = plugin._pluginInfo; |
| for (var i in info) |
| clone[i] = info[i]; |
| clone.instance = obj; |
| clone.args = args; |
| this.plugins[plugin._pluginInfo.name] = clone; |
| } else |
| alert("Can't register plugin " + plugin.toString() + "."); |
| }; |
| |
| // Create the specified plugin and register it with this HTMLArea |
| HTMLArea.prototype.registerPlugin = function() { |
| var plugin = arguments[0]; |
| var args = []; |
| for (var i = 1; i < arguments.length; ++i) |
| args.push(arguments[i]); |
| this.registerPlugin2(plugin, args); |
| }; |
| |
| // static function that loads the required plugin and lang file, based on the |
| // language loaded already for HTMLArea. You better make sure that the plugin |
| // _has_ that language, otherwise shit might happen ;-) |
| HTMLArea.loadPlugin = function(pluginName) { |
| var dir = _editor_url + "plugins/" + pluginName; |
| var plugin = pluginName.replace(/([a-z])([A-Z])([a-z])/g, |
| function (str, l1, l2, l3) { |
| return l1 + "-" + l2.toLowerCase() + l3; |
| }).toLowerCase() + ".js"; |
| var plugin_file = dir + "/" + plugin; |
| var plugin_lang = dir + "/lang/" + HTMLArea.I18N.lang + ".js"; |
| HTMLArea._scripts.push(plugin_file, plugin_lang); |
| document.write("<script type='text/javascript' src='" + plugin_file + "'></script>"); |
| document.write("<script type='text/javascript' src='" + plugin_lang + "'></script>"); |
| }; |
| |
| HTMLArea.loadStyle = function(style, plugin) { |
| var url = _editor_url || ''; |
| if (typeof plugin != "undefined") { |
| url += "plugins/" + plugin + "/"; |
| } |
| url += style; |
| document.write("<style type='text/css'>@import url(" + url + ");</style>"); |
| }; |
| HTMLArea.loadStyle("htmlarea.css"); |
| |
| /*************************************************** |
| * Category: EDITOR UTILITIES |
| ***************************************************/ |
| |
| // The following function is a slight variation of the word cleaner code posted |
| // by Weeezl (user @ InteractiveTools forums). |
| HTMLArea.prototype._wordClean = function() { |
| var D = this.getInnerHTML(); |
| if (D.indexOf('class=Mso') >= 0) { |
| |
| // make one line |
| D = D.replace(/\r\n/g, ' '). |
| replace(/\n/g, ' '). |
| replace(/\r/g, ' '). |
| replace(/\ \;/g,' '); |
| |
| // keep tags, strip attributes |
| D = D.replace(/ class=[^\s|>]*/gi,''). |
| //replace(/<p [^>]*TEXT-ALIGN: justify[^>]*>/gi,'<p align="justify">'). |
| replace(/ style=\"[^>]*\"/gi,''). |
| replace(/ align=[^\s|>]*/gi,''); |
| |
| //clean up tags |
| D = D.replace(/<b [^>]*>/gi,'<b>'). |
| replace(/<i [^>]*>/gi,'<i>'). |
| replace(/<li [^>]*>/gi,'<li>'). |
| replace(/<ul [^>]*>/gi,'<ul>'); |
| |
| // replace outdated tags |
| D = D.replace(/<b>/gi,'<strong>'). |
| replace(/<\/b>/gi,'</strong>'); |
| |
| // mozilla doesn't like <em> tags |
| D = D.replace(/<em>/gi,'<i>'). |
| replace(/<\/em>/gi,'</i>'); |
| |
| // kill unwanted tags |
| D = D.replace(/<\?xml:[^>]*>/g, ''). // Word xml |
| replace(/<\/?st1:[^>]*>/g,''). // Word SmartTags |
| replace(/<\/?[a-z]\:[^>]*>/g,''). // All other funny Word non-HTML stuff |
| replace(/<\/?font[^>]*>/gi,''). // Disable if you want to keep font formatting |
| replace(/<\/?span[^>]*>/gi,' '). |
| replace(/<\/?div[^>]*>/gi,' '). |
| replace(/<\/?pre[^>]*>/gi,' '). |
| replace(/<\/?h[1-6][^>]*>/gi,' '); |
| |
| //remove empty tags |
| //D = D.replace(/<strong><\/strong>/gi,''). |
| //replace(/<i><\/i>/gi,''). |
| //replace(/<P[^>]*><\/P>/gi,''); |
| |
| // nuke double tags |
| oldlen = D.length + 1; |
| while(oldlen > D.length) { |
| oldlen = D.length; |
| // join us now and free the tags, we'll be free hackers, we'll be free... ;-) |
| D = D.replace(/<([a-z][a-z]*)> *<\/\1>/gi,' '). |
| replace(/<([a-z][a-z]*)> *<([a-z][^>]*)> *<\/\1>/gi,'<$2>'); |
| } |
| D = D.replace(/<([a-z][a-z]*)><\1>/gi,'<$1>'). |
| replace(/<\/([a-z][a-z]*)><\/\1>/gi,'<\/$1>'); |
| |
| // nuke double spaces |
| D = D.replace(/ */gi,' '); |
| |
| this.setHTML(D); |
| this.updateToolbar(); |
| } |
| }; |
| |
| HTMLArea.prototype.forceRedraw = function() { |
| this._doc.body.style.visibility = "hidden"; |
| this._doc.body.style.visibility = "visible"; |
| // this._doc.body.innerHTML = this.getInnerHTML(); |
| }; |
| |
| // focuses the iframe window. returns a reference to the editor document. |
| HTMLArea.prototype.focusEditor = function() { |
| switch (this._editMode) { |
| case "wysiwyg" : this._iframe.contentWindow.focus(); break; |
| case "textmode": this._textArea.focus(); break; |
| default : alert("ERROR: mode " + this._editMode + " is not defined"); |
| } |
| return this._doc; |
| }; |
| |
| // takes a snapshot of the current text (for undo) |
| HTMLArea.prototype._undoTakeSnapshot = function() { |
| ++this._undoPos; |
| if (this._undoPos >= this.config.undoSteps) { |
| // remove the first element |
| this._undoQueue.shift(); |
| --this._undoPos; |
| } |
| // use the fasted method (getInnerHTML); |
| var take = true; |
| var txt = this.getInnerHTML(); |
| if (this._undoPos > 0) |
| take = (this._undoQueue[this._undoPos - 1] != txt); |
| if (take) { |
| this._undoQueue[this._undoPos] = txt; |
| } else { |
| this._undoPos--; |
| } |
| }; |
| |
| HTMLArea.prototype.undo = function() { |
| if (this._undoPos > 0) { |
| var txt = this._undoQueue[--this._undoPos]; |
| if (txt) this.setHTML(txt); |
| else ++this._undoPos; |
| } |
| }; |
| |
| HTMLArea.prototype.redo = function() { |
| if (this._undoPos < this._undoQueue.length - 1) { |
| var txt = this._undoQueue[++this._undoPos]; |
| if (txt) this.setHTML(txt); |
| else --this._undoPos; |
| } |
| }; |
| |
| // updates enabled/disable/active state of the toolbar elements |
| HTMLArea.prototype.updateToolbar = function(noStatus) { |
| var doc = this._doc; |
| var text = (this._editMode == "textmode"); |
| var ancestors = null; |
| if (!text) { |
| ancestors = this.getAllAncestors(); |
| if (this.config.statusBar && !noStatus) { |
| this._statusBarTree.innerHTML = HTMLArea.I18N.msg["Path"] + ": "; // clear |
| for (var i = ancestors.length; --i >= 0;) { |
| var el = ancestors[i]; |
| if (!el) { |
| // hell knows why we get here; this |
| // could be a classic example of why |
| // it's good to check for conditions |
| // that are impossible to happen ;-) |
| continue; |
| } |
| var a = document.createElement("a"); |
| a.href = "#"; |
| a.el = el; |
| a.editor = this; |
| a.onclick = function() { |
| this.blur(); |
| this.editor.selectNodeContents(this.el); |
| this.editor.updateToolbar(true); |
| return false; |
| }; |
| a.oncontextmenu = function() { |
| // TODO: add context menu here |
| this.blur(); |
| var info = "Inline style:\n\n"; |
| info += this.el.style.cssText.split(/;\s*/).join(";\n"); |
| alert(info); |
| return false; |
| }; |
| var txt = el.tagName.toLowerCase(); |
| a.title = el.style.cssText; |
| if (el.id) { |
| txt += "#" + el.id; |
| } |
| if (el.className) { |
| txt += "." + el.className; |
| } |
| a.appendChild(document.createTextNode(txt)); |
| this._statusBarTree.appendChild(a); |
| if (i != 0) { |
| this._statusBarTree.appendChild(document.createTextNode(String.fromCharCode(0xbb))); |
| } |
| } |
| } |
| } |
| for (var i in this._toolbarObjects) { |
| var btn = this._toolbarObjects[i]; |
| var cmd = i; |
| var inContext = true; |
| if (btn.context && !text) { |
| inContext = false; |
| var context = btn.context; |
| var attrs = []; |
| if (/(.*)\[(.*?)\]/.test(context)) { |
| context = RegExp.$1; |
| attrs = RegExp.$2.split(","); |
| } |
| context = context.toLowerCase(); |
| var match = (context == "*"); |
| for (var k in ancestors) { |
| if (!ancestors[k]) { |
| // the impossible really happens. |
| continue; |
| } |
| if (match || (ancestors[k].tagName.toLowerCase() == context)) { |
| inContext = true; |
| for (var ka in attrs) { |
| if (!eval("ancestors[k]." + attrs[ka])) { |
| inContext = false; |
| break; |
| } |
| } |
| if (inContext) { |
| break; |
| } |
| } |
| } |
| } |
| btn.state("enabled", (!text || btn.text) && inContext); |
| if (typeof cmd == "function") { |
| continue; |
| } |
| // look-it-up in the custom dropdown boxes |
| var dropdown = this.config.customSelects[cmd]; |
| if ((!text || btn.text) && (typeof dropdown != "undefined")) { |
| dropdown.refresh(this); |
| continue; |
| } |
| switch (cmd) { |
| case "fontname": |
| case "fontsize": |
| case "formatblock": |
| if (!text) try { |
| var value = ("" + doc.queryCommandValue(cmd)).toLowerCase(); |
| if (!value) { |
| // FIXME: what do we do here? |
| break; |
| } |
| // HACK -- retrieve the config option for this |
| // combo box. We rely on the fact that the |
| // variable in config has the same name as |
| // button name in the toolbar. |
| var options = this.config[cmd]; |
| var k = 0; |
| // btn.element.selectedIndex = 0; |
| for (var j in options) { |
| // FIXME: the following line is scary. |
| if ((j.toLowerCase() == value) || |
| (options[j].substr(0, value.length).toLowerCase() == value)) { |
| btn.element.selectedIndex = k; |
| break; |
| } |
| ++k; |
| } |
| } catch(e) {}; |
| break; |
| case "textindicator": |
| if (!text) { |
| try {with (btn.element.style) { |
| backgroundColor = HTMLArea._makeColor( |
| doc.queryCommandValue(HTMLArea.is_ie ? "backcolor" : "hilitecolor")); |
| if (/transparent/i.test(backgroundColor)) { |
| // Mozilla |
| backgroundColor = HTMLArea._makeColor(doc.queryCommandValue("backcolor")); |
| } |
| color = HTMLArea._makeColor(doc.queryCommandValue("forecolor")); |
| fontFamily = doc.queryCommandValue("fontname"); |
| fontWeight = doc.queryCommandState("bold") ? "bold" : "normal"; |
| fontStyle = doc.queryCommandState("italic") ? "italic" : "normal"; |
| }} catch (e) { |
| // alert(e + "\n\n" + cmd); |
| } |
| } |
| break; |
| case "htmlmode": btn.state("active", text); break; |
| case "lefttoright": |
| case "righttoleft": |
| var el = this.getParentElement(); |
| while (el && !HTMLArea.isBlockElement(el)) |
| el = el.parentNode; |
| if (el) |
| btn.state("active", (el.style.direction == ((cmd == "righttoleft") ? "rtl" : "ltr"))); |
| break; |
| default: |
| try { |
| btn.state("active", (!text && doc.queryCommandState(cmd))); |
| } catch (e) {} |
| } |
| } |
| // take undo snapshots |
| if (this._customUndo && !this._timerUndo) { |
| this._undoTakeSnapshot(); |
| var editor = this; |
| this._timerUndo = setTimeout(function() { |
| editor._timerUndo = null; |
| }, this.config.undoTimeout); |
| } |
| // check if any plugins have registered refresh handlers |
| for (var i in this.plugins) { |
| var plugin = this.plugins[i].instance; |
| if (typeof plugin.onUpdateToolbar == "function") |
| plugin.onUpdateToolbar(); |
| } |
| }; |
| |
| /** Returns a node after which we can insert other nodes, in the current |
| * selection. The selection is removed. It splits a text node, if needed. |
| */ |
| HTMLArea.prototype.insertNodeAtSelection = function(toBeInserted) { |
| if (!HTMLArea.is_ie) { |
| var sel = this._getSelection(); |
| var range = this._createRange(sel); |
| // remove the current selection |
| sel.removeAllRanges(); |
| range.deleteContents(); |
| var node = range.startContainer; |
| var pos = range.startOffset; |
| switch (node.nodeType) { |
| case 3: // Node.TEXT_NODE |
| // we have to split it at the caret position. |
| if (toBeInserted.nodeType == 3) { |
| // do optimized insertion |
| node.insertData(pos, toBeInserted.data); |
| range = this._createRange(); |
| range.setEnd(node, pos + toBeInserted.length); |
| range.setStart(node, pos + toBeInserted.length); |
| sel.addRange(range); |
| } else { |
| node = node.splitText(pos); |
| var selnode = toBeInserted; |
| if (toBeInserted.nodeType == 11 /* Node.DOCUMENT_FRAGMENT_NODE */) { |
| selnode = selnode.firstChild; |
| } |
| node.parentNode.insertBefore(toBeInserted, node); |
| this.selectNodeContents(selnode); |
| this.updateToolbar(); |
| } |
| break; |
| case 1: // Node.ELEMENT_NODE |
| var selnode = toBeInserted; |
| if (toBeInserted.nodeType == 11 /* Node.DOCUMENT_FRAGMENT_NODE */) { |
| selnode = selnode.firstChild; |
| } |
| node.insertBefore(toBeInserted, node.childNodes[pos]); |
| this.selectNodeContents(selnode); |
| this.updateToolbar(); |
| break; |
| } |
| } else { |
| return null; // this function not yet used for IE <FIXME> |
| } |
| }; |
| |
| // Returns the deepest node that contains both endpoints of the selection. |
| HTMLArea.prototype.getParentElement = function() { |
| var sel = this._getSelection(); |
| var range = this._createRange(sel); |
| if (HTMLArea.is_ie) { |
| switch (sel.type) { |
| case "Text": |
| case "None": |
| // It seems that even for selection of type "None", |
| // there _is_ a parent element and it's value is not |
| // only correct, but very important to us. MSIE is |
| // certainly the buggiest browser in the world and I |
| // wonder, God, how can Earth stand it? |
| return range.parentElement(); |
| case "Control": |
| return range.item(0); |
| default: |
| return this._doc.body; |
| } |
| } else try { |
| var p = range.commonAncestorContainer; |
| if (!range.collapsed && range.startContainer == range.endContainer && |
| range.startOffset - range.endOffset <= 1 && range.startContainer.hasChildNodes()) |
| p = range.startContainer.childNodes[range.startOffset]; |
| /* |
| alert(range.startContainer + ":" + range.startOffset + "\n" + |
| range.endContainer + ":" + range.endOffset); |
| */ |
| while (p.nodeType == 3) { |
| p = p.parentNode; |
| } |
| return p; |
| } catch (e) { |
| return null; |
| } |
| }; |
| |
| // Returns an array with all the ancestor nodes of the selection. |
| HTMLArea.prototype.getAllAncestors = function() { |
| var p = this.getParentElement(); |
| var a = []; |
| while (p && (p.nodeType == 1) && (p.tagName.toLowerCase() != 'body')) { |
| a.push(p); |
| p = p.parentNode; |
| } |
| a.push(this._doc.body); |
| return a; |
| }; |
| |
| // Selects the contents inside the given node |
| HTMLArea.prototype.selectNodeContents = function(node, pos) { |
| this.focusEditor(); |
| this.forceRedraw(); |
| var range; |
| var collapsed = (typeof pos != "undefined"); |
| if (HTMLArea.is_ie) { |
| range = this._doc.body.createTextRange(); |
| range.moveToElementText(node); |
| (collapsed) && range.collapse(pos); |
| range.select(); |
| } else { |
| var sel = this._getSelection(); |
| range = this._doc.createRange(); |
| range.selectNodeContents(node); |
| (collapsed) && range.collapse(pos); |
| sel.removeAllRanges(); |
| sel.addRange(range); |
| } |
| }; |
| |
| /** Call this function to insert HTML code at the current position. It deletes |
| * the selection, if any. |
| */ |
| HTMLArea.prototype.insertHTML = function(html) { |
| var sel = this._getSelection(); |
| var range = this._createRange(sel); |
| if (HTMLArea.is_ie) { |
| range.pasteHTML(html); |
| } else { |
| // 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); |
| } |
| }; |
| |
| /** |
| * Call this function to surround the existing HTML code in the selection with |
| * your tags. FIXME: buggy! This function will be deprecated "soon". |
| */ |
| HTMLArea.prototype.surroundHTML = function(startTag, endTag) { |
| var html = this.getSelectedHTML(); |
| // the following also deletes the selection |
| this.insertHTML(startTag + html + endTag); |
| }; |
| |
| /// Retrieve the selected block |
| HTMLArea.prototype.getSelectedHTML = function() { |
| var sel = this._getSelection(); |
| var range = this._createRange(sel); |
| var existing = null; |
| if (HTMLArea.is_ie) { |
| existing = range.htmlText; |
| } else { |
| existing = HTMLArea.getHTML(range.cloneContents(), false, this); |
| } |
| return existing; |
| }; |
| |
| /// Return true if we have some selection |
| HTMLArea.prototype.hasSelectedText = function() { |
| // FIXME: come _on_ mishoo, you can do better than this ;-) |
| return this.getSelectedHTML() != ''; |
| }; |
| |
| HTMLArea.prototype._createLink = function(link) { |
| var editor = this; |
| var outparam = null; |
| if (typeof link == "undefined") { |
| link = this.getParentElement(); |
| if (link && !/^a$/i.test(link.tagName)) |
| link = null; |
| } |
| if (link) outparam = { |
| f_href : HTMLArea.is_ie ? editor.stripBaseURL(link.href) : link.getAttribute("href"), |
| f_title : link.title, |
| f_target : link.target |
| }; |
| this._popupDialog("link.html", function(param) { |
| if (!param) |
| return false; |
| var a = link; |
| if (!a) { |
| editor._doc.execCommand("createlink", false, param.f_href); |
| a = editor.getParentElement(); |
| var sel = editor._getSelection(); |
| var range = editor._createRange(sel); |
| if (!HTMLArea.is_ie) { |
| a = range.startContainer; |
| if (!/^a$/i.test(a.tagName)) |
| a = a.nextSibling; |
| } |
| } else a.href = param.f_href.trim(); |
| if (!/^a$/i.test(a.tagName)) |
| return false; |
| a.target = param.f_target.trim(); |
| a.title = param.f_title.trim(); |
| editor.selectNodeContents(a); |
| editor.updateToolbar(); |
| }, outparam); |
| }; |
| |
| // Called when the user clicks on "InsertImage" button. If an image is already |
| // there, it will just modify it's properties. |
| HTMLArea.prototype._insertImage = function(image) { |
| var editor = this; // for nested functions |
| var outparam = null; |
| if (typeof image == "undefined") { |
| image = this.getParentElement(); |
| if (image && !/^img$/i.test(image.tagName)) |
| image = null; |
| } |
| if (image) outparam = { |
| f_url : HTMLArea.is_ie ? editor.stripBaseURL(image.src) : image.getAttribute("src"), |
| f_alt : image.alt, |
| f_border : image.border, |
| f_align : image.align, |
| f_vert : image.vspace, |
| f_horiz : image.hspace |
| }; |
| this._popupDialog("insert_image.html", function(param) { |
| if (!param) { // user must have pressed Cancel |
| return false; |
| } |
| var img = image; |
| if (!img) { |
| var sel = editor._getSelection(); |
| var range = editor._createRange(sel); |
| editor._doc.execCommand("insertimage", false, param.f_url); |
| if (HTMLArea.is_ie) { |
| img = range.parentElement(); |
| // wonder if this works... |
| if (img.tagName.toLowerCase() != "img") { |
| img = img.previousSibling; |
| } |
| } else { |
| img = range.startContainer.previousSibling; |
| } |
| } else { |
| img.src = param.f_url; |
| } |
| for (field in param) { |
| var value = param[field]; |
| switch (field) { |
| case "f_alt" : img.alt = value; break; |
| case "f_border" : img.border = parseInt(value || "0"); break; |
| case "f_align" : img.align = value; break; |
| case "f_vert" : img.vspace = parseInt(value || "0"); break; |
| case "f_horiz" : img.hspace = parseInt(value || "0"); break; |
| } |
| } |
| }, outparam); |
| }; |
| |
| // Called when the user clicks the Insert Table button |
| HTMLArea.prototype._insertTable = function() { |
| var sel = this._getSelection(); |
| var range = this._createRange(sel); |
| var editor = this; // for nested functions |
| this._popupDialog("insert_table.html", function(param) { |
| if (!param) { // user must have pressed Cancel |
| return false; |
| } |
| var doc = editor._doc; |
| // create the table element |
| var table = doc.createElement("table"); |
| // assign the given arguments |
| for (var field in param) { |
| var value = param[field]; |
| if (!value) { |
| continue; |
| } |
| switch (field) { |
| case "f_width" : table.style.width = value + param["f_unit"]; break; |
| case "f_align" : table.align = value; break; |
| case "f_border" : table.border = parseInt(value); break; |
| case "f_spacing" : table.cellspacing = parseInt(value); break; |
| case "f_padding" : table.cellpadding = parseInt(value); break; |
| } |
| } |
| var tbody = doc.createElement("tbody"); |
| table.appendChild(tbody); |
| for (var i = 0; i < param["f_rows"]; ++i) { |
| var tr = doc.createElement("tr"); |
| tbody.appendChild(tr); |
| for (var j = 0; j < param["f_cols"]; ++j) { |
| var td = doc.createElement("td"); |
| tr.appendChild(td); |
| // Mozilla likes to see something inside the cell. |
| (HTMLArea.is_gecko) && td.appendChild(doc.createElement("br")); |
| } |
| } |
| if (HTMLArea.is_ie) { |
| range.pasteHTML(table.outerHTML); |
| } else { |
| // insert the table |
| editor.insertNodeAtSelection(table); |
| } |
| return true; |
| }, null); |
| }; |
| |
| /*************************************************** |
| * Category: EVENT HANDLERS |
| ***************************************************/ |
| |
| // el is reference to the SELECT object |
| // txt is the name of the select field, as in config.toolbar |
| HTMLArea.prototype._comboSelected = function(el, txt) { |
| this.focusEditor(); |
| var value = el.options[el.selectedIndex].value; |
| switch (txt) { |
| case "fontname": |
| case "fontsize": this.execCommand(txt, false, value); break; |
| case "formatblock": |
| (HTMLArea.is_ie) && (value = "<" + value + ">"); |
| this.execCommand(txt, false, value); |
| break; |
| default: |
| // try to look it up in the registered dropdowns |
| var dropdown = this.config.customSelects[txt]; |
| if (typeof dropdown != "undefined") { |
| dropdown.action(this); |
| } else { |
| alert("FIXME: combo box " + txt + " not implemented"); |
| } |
| } |
| }; |
| |
| // the execCommand function (intercepts some commands and replaces them with |
| // our own implementation) |
| HTMLArea.prototype.execCommand = function(cmdID, UI, param) { |
| var editor = this; // for nested functions |
| this.focusEditor(); |
| cmdID = cmdID.toLowerCase(); |
| switch (cmdID) { |
| case "htmlmode" : this.setMode(); break; |
| case "hilitecolor": |
| (HTMLArea.is_ie) && (cmdID = "backcolor"); |
| case "forecolor": |
| this._popupDialog("select_color.html", function(color) { |
| if (color) { // selection not canceled |
| editor._doc.execCommand(cmdID, false, "#" + color); |
| } |
| }, HTMLArea._colorToRgb(this._doc.queryCommandValue(cmdID))); |
| break; |
| case "createlink": |
| this._createLink(); |
| break; |
| case "popupeditor": |
| // this object will be passed to the newly opened window |
| HTMLArea._object = this; |
| if (HTMLArea.is_ie) { |
| //if (confirm(HTMLArea.I18N.msg["IE-sucks-full-screen"])) |
| { |
| window.open(this.popupURL("fullscreen.html"), "ha_fullscreen", |
| "toolbar=no,location=no,directories=no,status=no,menubar=no," + |
| "scrollbars=no,resizable=yes,width=640,height=480"); |
| } |
| } else { |
| window.open(this.popupURL("fullscreen.html"), "ha_fullscreen", |
| "toolbar=no,menubar=no,personalbar=no,width=640,height=480," + |
| "scrollbars=no,resizable=yes"); |
| } |
| break; |
| case "undo": |
| case "redo": |
| if (this._customUndo) |
| this[cmdID](); |
| else |
| this._doc.execCommand(cmdID, UI, param); |
| break; |
| case "inserttable": this._insertTable(); break; |
| case "insertimage": this._insertImage(); break; |
| case "about" : this._popupDialog("about.html", null, this); break; |
| case "showhelp" : window.open(_editor_url + "reference.html", "ha_help"); break; |
| |
| case "killword": this._wordClean(); break; |
| |
| case "cut": |
| case "copy": |
| case "paste": |
| try { |
| if (this.config.killWordOnPaste) |
| this._wordClean(); |
| this._doc.execCommand(cmdID, UI, param); |
| } catch (e) { |
| if (HTMLArea.is_gecko) { |
| if (confirm("Unprivileged scripts cannot access Cut/Copy/Paste programatically " + |
| "for security reasons. Click OK to see a technical note at mozilla.org " + |
| "which shows you how to allow a script to access the clipboard.")) |
| window.open("http://mozilla.org/editor/midasdemo/securityprefs.html"); |
| } |
| } |
| break; |
| case "lefttoright": |
| case "righttoleft": |
| var dir = (cmdID == "righttoleft") ? "rtl" : "ltr"; |
| var el = this.getParentElement(); |
| while (el && !HTMLArea.isBlockElement(el)) |
| el = el.parentNode; |
| if (el) { |
| if (el.style.direction == dir) |
| el.style.direction = ""; |
| else |
| el.style.direction = dir; |
| } |
| break; |
| default: this._doc.execCommand(cmdID, UI, param); |
| } |
| this.updateToolbar(); |
| return false; |
| }; |
| |
| /** A generic event handler for things that happen in the IFRAME's document. |
| * This function also handles key bindings. */ |
| HTMLArea.prototype._editorEvent = function(ev) { |
| var editor = this; |
| var keyEvent = (HTMLArea.is_ie && ev.type == "keydown") || (ev.type == "keypress"); |
| if (keyEvent) { |
| for (var i in editor.plugins) { |
| var plugin = editor.plugins[i].instance; |
| if (typeof plugin.onKeyPress == "function") plugin.onKeyPress(ev); |
| } |
| } |
| if (keyEvent && ev.ctrlKey) { |
| var sel = null; |
| var range = null; |
| var key = String.fromCharCode(HTMLArea.is_ie ? ev.keyCode : ev.charCode).toLowerCase(); |
| var cmd = null; |
| var value = null; |
| switch (key) { |
| case 'a': |
| if (!HTMLArea.is_ie) { |
| // KEY select all |
| sel = this._getSelection(); |
| sel.removeAllRanges(); |
| range = this._createRange(); |
| range.selectNodeContents(this._doc.body); |
| sel.addRange(range); |
| HTMLArea._stopEvent(ev); |
| } |
| break; |
| |
| // simple key commands follow |
| |
| case 'b': cmd = "bold"; break; |
| case 'i': cmd = "italic"; break; |
| case 'u': cmd = "underline"; break; |
| case 's': cmd = "strikethrough"; break; |
| case 'l': cmd = "justifyleft"; break; |
| case 'e': cmd = "justifycenter"; break; |
| case 'r': cmd = "justifyright"; break; |
| case 'j': cmd = "justifyfull"; break; |
| case 'z': cmd = "undo"; break; |
| case 'y': cmd = "redo"; break; |
| case 'v': cmd = "paste"; break; |
| |
| case '0': cmd = "killword"; break; |
| |
| // headings |
| case '1': |
| case '2': |
| case '3': |
| case '4': |
| case '5': |
| case '6': |
| cmd = "formatblock"; |
| value = "h" + key; |
| if (HTMLArea.is_ie) { |
| value = "<" + value + ">"; |
| } |
| break; |
| } |
| if (cmd) { |
| // execute simple command |
| this.execCommand(cmd, false, value); |
| HTMLArea._stopEvent(ev); |
| } |
| } |
| /* |
| else if (keyEvent) { |
| // other keys here |
| switch (ev.keyCode) { |
| case 13: // KEY enter |
| // if (HTMLArea.is_ie) { |
| this.insertHTML("<br />"); |
| HTMLArea._stopEvent(ev); |
| // } |
| break; |
| } |
| } |
| */ |
| // update the toolbar state after some time |
| if (editor._timerToolbar) { |
| clearTimeout(editor._timerToolbar); |
| } |
| editor._timerToolbar = setTimeout(function() { |
| editor.updateToolbar(); |
| editor._timerToolbar = null; |
| }, 50); |
| }; |
| |
| // retrieve the HTML |
| HTMLArea.prototype.getHTML = function() { |
| switch (this._editMode) { |
| case "wysiwyg" : |
| if (!this.config.fullPage) { |
| return HTMLArea.getHTML(this._doc.body, false, this); |
| } else |
| return this.doctype + "\n" + HTMLArea.getHTML(this._doc.documentElement, true, this); |
| case "textmode" : return this._textArea.value; |
| default : alert("Mode <" + mode + "> not defined!"); |
| } |
| return false; |
| }; |
| |
| // retrieve the HTML (fastest version, but uses innerHTML) |
| HTMLArea.prototype.getInnerHTML = function() { |
| switch (this._editMode) { |
| case "wysiwyg" : |
| if (!this.config.fullPage) |
| return this._doc.body.innerHTML; |
| else |
| return this.doctype + "\n" + this._doc.documentElement.innerHTML; |
| case "textmode" : return this._textArea.value; |
| default : alert("Mode <" + mode + "> not defined!"); |
| } |
| return false; |
| }; |
| |
| // completely change the HTML inside |
| HTMLArea.prototype.setHTML = function(html) { |
| switch (this._editMode) { |
| case "wysiwyg" : |
| if (!this.config.fullPage) |
| this._doc.body.innerHTML = html; |
| else |
| // this._doc.documentElement.innerHTML = html; |
| this._doc.body.innerHTML = html; |
| break; |
| case "textmode" : this._textArea.value = html; break; |
| default : alert("Mode <" + mode + "> not defined!"); |
| } |
| return false; |
| }; |
| |
| // sets the given doctype (useful when config.fullPage is true) |
| HTMLArea.prototype.setDoctype = function(doctype) { |
| this.doctype = doctype; |
| }; |
| |
| /*************************************************** |
| * Category: UTILITY FUNCTIONS |
| ***************************************************/ |
| |
| // browser identification |
| |
| HTMLArea.agt = navigator.userAgent.toLowerCase(); |
| HTMLArea.is_ie = ((HTMLArea.agt.indexOf("msie") != -1) && (HTMLArea.agt.indexOf("opera") == -1)); |
| HTMLArea.is_opera = (HTMLArea.agt.indexOf("opera") != -1); |
| HTMLArea.is_mac = (HTMLArea.agt.indexOf("mac") != -1); |
| HTMLArea.is_mac_ie = (HTMLArea.is_ie && HTMLArea.is_mac); |
| HTMLArea.is_win_ie = (HTMLArea.is_ie && !HTMLArea.is_mac); |
| HTMLArea.is_gecko = (navigator.product == "Gecko"); |
| |
| // variable used to pass the object to the popup editor window. |
| HTMLArea._object = null; |
| |
| // function that returns a clone of the given object |
| HTMLArea.cloneObject = function(obj) { |
| var newObj = new Object; |
| |
| // check for array objects |
| if (obj.constructor.toString().indexOf("function Array(") == 1) { |
| newObj = obj.constructor(); |
| } |
| |
| // check for function objects (as usual, IE is fucked up) |
| if (obj.constructor.toString().indexOf("function Function(") == 1) { |
| newObj = obj; // just copy reference to it |
| } else for (var n in obj) { |
| var node = obj[n]; |
| if (typeof node == 'object') { newObj[n] = HTMLArea.cloneObject(node); } |
| else { newObj[n] = node; } |
| } |
| |
| return newObj; |
| }; |
| |
| // FIXME!!! this should return false for IE < 5.5 |
| HTMLArea.checkSupportedBrowser = function() { |
| if (HTMLArea.is_gecko) { |
| if (navigator.productSub < 20021201) { |
| alert("You need at least Mozilla-1.3 Alpha.\n" + |
| "Sorry, your Gecko is not supported."); |
| return false; |
| } |
| if (navigator.productSub < 20030210) { |
| alert("Mozilla < 1.3 Beta is not supported!\n" + |
| "I'll try, though, but it might not work."); |
| } |
| } |
| return HTMLArea.is_gecko || HTMLArea.is_ie; |
| }; |
| |
| // selection & ranges |
| |
| // returns the current selection object |
| HTMLArea.prototype._getSelection = function() { |
| if (HTMLArea.is_ie) { |
| return this._doc.selection; |
| } else { |
| return this._iframe.contentWindow.getSelection(); |
| } |
| }; |
| |
| // returns a range for the current selection |
| HTMLArea.prototype._createRange = function(sel) { |
| if (HTMLArea.is_ie) { |
| return sel.createRange(); |
| } else { |
| this.focusEditor(); |
| if (typeof sel != "undefined") { |
| try { |
| return sel.getRangeAt(0); |
| } catch(e) { |
| return this._doc.createRange(); |
| } |
| } else { |
| return this._doc.createRange(); |
| } |
| } |
| }; |
| |
| // event handling |
| |
| HTMLArea._addEvent = function(el, evname, func) { |
| if (HTMLArea.is_ie) { |
| el.attachEvent("on" + evname, func); |
| } else { |
| el.addEventListener(evname, func, true); |
| } |
| }; |
| |
| HTMLArea._addEvents = function(el, evs, func) { |
| for (var i in evs) { |
| HTMLArea._addEvent(el, evs[i], func); |
| } |
| }; |
| |
| HTMLArea._removeEvent = function(el, evname, func) { |
| if (HTMLArea.is_ie) { |
| el.detachEvent("on" + evname, func); |
| } else { |
| el.removeEventListener(evname, func, true); |
| } |
| }; |
| |
| HTMLArea._removeEvents = function(el, evs, func) { |
| for (var i in evs) { |
| HTMLArea._removeEvent(el, evs[i], func); |
| } |
| }; |
| |
| HTMLArea._stopEvent = function(ev) { |
| if (HTMLArea.is_ie) { |
| ev.cancelBubble = true; |
| ev.returnValue = false; |
| } else { |
| ev.preventDefault(); |
| ev.stopPropagation(); |
| } |
| }; |
| |
| HTMLArea._removeClass = function(el, className) { |
| if (!(el && el.className)) { |
| return; |
| } |
| var cls = el.className.split(" "); |
| var ar = new Array(); |
| for (var i = cls.length; i > 0;) { |
| if (cls[--i] != className) { |
| ar[ar.length] = cls[i]; |
| } |
| } |
| el.className = ar.join(" "); |
| }; |
| |
| HTMLArea._addClass = function(el, className) { |
| // remove the class first, if already there |
| HTMLArea._removeClass(el, className); |
| el.className += " " + className; |
| }; |
| |
| HTMLArea._hasClass = function(el, className) { |
| if (!(el && el.className)) { |
| return false; |
| } |
| var cls = el.className.split(" "); |
| for (var i = cls.length; i > 0;) { |
| if (cls[--i] == className) { |
| return true; |
| } |
| } |
| return false; |
| }; |
| |
| HTMLArea.isBlockElement = function(el) { |
| var blockTags = " body form textarea fieldset ul ol dl li div " + |
| "p h1 h2 h3 h4 h5 h6 quote pre table thead " + |
| "tbody tfoot tr td iframe address "; |
| return (blockTags.indexOf(" " + el.tagName.toLowerCase() + " ") != -1); |
| }; |
| |
| HTMLArea.needsClosingTag = function(el) { |
| var closingTags = " head script style div span tr td tbody table em strong font a title "; |
| return (closingTags.indexOf(" " + el.tagName.toLowerCase() + " ") != -1); |
| }; |
| |
| // performs HTML encoding of some given string |
| HTMLArea.htmlEncode = function(str) { |
| // we don't need regexp for that, but.. so be it for now. |
| str = str.replace(/&/ig, "&"); |
| str = str.replace(/</ig, "<"); |
| str = str.replace(/>/ig, ">"); |
| str = str.replace(/\x22/ig, """); |
| // \x22 means '"' -- we use hex reprezentation so that we don't disturb |
| // JS compressors (well, at least mine fails.. ;) |
| return str; |
| }; |
| |
| // Retrieves the HTML code from the given node. This is a replacement for |
| // getting innerHTML, using standard DOM calls. |
| HTMLArea.getHTML = function(root, outputRoot, editor) { |
| var html = ""; |
| switch (root.nodeType) { |
| case 1: // Node.ELEMENT_NODE |
| case 11: // Node.DOCUMENT_FRAGMENT_NODE |
| var closed; |
| var i; |
| var root_tag = (root.nodeType == 1) ? root.tagName.toLowerCase() : ''; |
| if (HTMLArea.is_ie && root_tag == "head") { |
| if (outputRoot) |
| html += "<head>"; |
| // lowercasize |
| var save_multiline = RegExp.multiline; |
| RegExp.multiline = true; |
| var txt = root.innerHTML.replace(HTMLArea.RE_tagName, function(str, p1, p2) { |
| return p1 + p2.toLowerCase(); |
| }); |
| RegExp.multiline = save_multiline; |
| html += txt; |
| if (outputRoot) |
| html += "</head>"; |
| break; |
| } else if (outputRoot) { |
| closed = (!(root.hasChildNodes() || HTMLArea.needsClosingTag(root))); |
| html = "<" + root.tagName.toLowerCase(); |
| var attrs = root.attributes; |
| for (i = 0; i < attrs.length; ++i) { |
| var a = attrs.item(i); |
| if (!a.specified) { |
| continue; |
| } |
| var name = a.nodeName.toLowerCase(); |
| if (/_moz|contenteditable|_msh/.test(name)) { |
| // avoid certain attributes |
| continue; |
| } |
| var value; |
| if (name != "style") { |
| // IE5.5 reports 25 when cellSpacing is |
| // 1; other values might be doomed too. |
| // For this reason we extract the |
| // values directly from the root node. |
| // I'm starting to HATE JavaScript |
| // development. Browser differences |
| // suck. |
| // |
| // Using Gecko the values of href and src are converted to absolute links |
| // unless we get them using nodeValue() |
| if (typeof root[a.nodeName] != "undefined" && name != "href" && name != "src") { |
| value = root[a.nodeName]; |
| } else { |
| value = a.nodeValue; |
| // IE seems not willing to return the original values - it converts to absolute |
| // links using a.nodeValue, a.value, a.stringValue, root.getAttribute("href") |
| // So we have to strip the baseurl manually -/ |
| if (HTMLArea.is_ie && (name == "href" || name == "src")) { |
| value = editor.stripBaseURL(value); |
| } |
| } |
| } else { // IE fails to put style in attributes list |
| // FIXME: cssText reported by IE is UPPERCASE |
| value = root.style.cssText; |
| } |
| if (/(_moz|^$)/.test(value)) { |
| // Mozilla reports some special tags |
| // here; we don't need them. |
| continue; |
| } |
| html += " " + name + '="' + value + '"'; |
| } |
| html += closed ? " />" : ">"; |
| } |
| for (i = root.firstChild; i; i = i.nextSibling) { |
| html += HTMLArea.getHTML(i, true, editor); |
| } |
| if (outputRoot && !closed) { |
| html += "</" + root.tagName.toLowerCase() + ">"; |
| } |
| break; |
| case 3: // Node.TEXT_NODE |
| // If a text node is alone in an element and all spaces, replace it with an non breaking one |
| // This partially undoes the damage done by moz, which translates ' 's into spaces in the data element |
| if ( !root.previousSibling && !root.nextSibling && root.data.match(/^\s*$/i) ) html = ' '; |
| else html = HTMLArea.htmlEncode(root.data); |
| break; |
| case 8: // Node.COMMENT_NODE |
| html = "<!--" + root.data + "-->"; |
| break; // skip comments, for now. |
| } |
| return html; |
| }; |
| |
| HTMLArea.prototype.stripBaseURL = function(string) { |
| var baseurl = this.config.baseURL; |
| |
| // strip to last directory in case baseurl points to a file |
| baseurl = baseurl.replace(/[^\/]+$/, ''); |
| var basere = new RegExp(baseurl); |
| string = string.replace(basere, ""); |
| |
| // strip host-part of URL which is added by MSIE to links relative to server root |
| baseurl = baseurl.replace(/^(https?:\/\/[^\/]+)(.*)$/, '$1'); |
| basere = new RegExp(baseurl); |
| return string.replace(basere, ""); |
| }; |
| |
| String.prototype.trim = function() { |
| a = this.replace(/^\s+/, ''); |
| return a.replace(/\s+$/, ''); |
| }; |
| |
| // creates a rgb-style color from a number |
| HTMLArea._makeColor = function(v) { |
| if (typeof v != "number") { |
| // already in rgb (hopefully); IE doesn't get here. |
| return v; |
| } |
| // IE sends number; convert to rgb. |
| var r = v & 0xFF; |
| var g = (v >> 8) & 0xFF; |
| var b = (v >> 16) & 0xFF; |
| return "rgb(" + r + "," + g + "," + b + ")"; |
| }; |
| |
| // returns hexadecimal color representation from a number or a rgb-style color. |
| HTMLArea._colorToRgb = function(v) { |
| if (!v) |
| return ''; |
| |
| // returns the hex representation of one byte (2 digits) |
| function hex(d) { |
| return (d < 16) ? ("0" + d.toString(16)) : d.toString(16); |
| }; |
| |
| if (typeof v == "number") { |
| // we're talking to IE here |
| var r = v & 0xFF; |
| var g = (v >> 8) & 0xFF; |
| var b = (v >> 16) & 0xFF; |
| return "#" + hex(r) + hex(g) + hex(b); |
| } |
| |
| if (v.substr(0, 3) == "rgb") { |
| // in rgb(...) form -- Mozilla |
| var re = /rgb\s*\(\s*([0-9]+)\s*,\s*([0-9]+)\s*,\s*([0-9]+)\s*\)/; |
| if (v.match(re)) { |
| var r = parseInt(RegExp.$1); |
| var g = parseInt(RegExp.$2); |
| var b = parseInt(RegExp.$3); |
| return "#" + hex(r) + hex(g) + hex(b); |
| } |
| // doesn't match RE?! maybe uses percentages or float numbers |
| // -- FIXME: not yet implemented. |
| return null; |
| } |
| |
| if (v.substr(0, 1) == "#") { |
| // already hex rgb (hopefully :D ) |
| return v; |
| } |
| |
| // if everything else fails ;) |
| return null; |
| }; |
| |
| // modal dialogs for Mozilla (for IE we're using the showModalDialog() call). |
| |
| // receives an URL to the popup dialog and a function that receives one value; |
| // this function will get called after the dialog is closed, with the return |
| // value of the dialog. |
| HTMLArea.prototype._popupDialog = function(url, action, init) { |
| Dialog(this.popupURL(url), action, init); |
| }; |
| |
| // paths |
| |
| HTMLArea.prototype.imgURL = function(file, plugin) { |
| if (typeof plugin == "undefined") |
| return _editor_url + file; |
| else |
| return _editor_url + "plugins/" + plugin + "/img/" + file; |
| }; |
| |
| HTMLArea.prototype.popupURL = function(file) { |
| var url = ""; |
| if (file.match(/^plugin:\/\/(.*?)\/(.*)/)) { |
| var plugin = RegExp.$1; |
| var popup = RegExp.$2; |
| if (!/\.html$/.test(popup)) |
| popup += ".html"; |
| url = _editor_url + "plugins/" + plugin + "/popups/" + popup; |
| } else |
| url = _editor_url + this.config.popupURL + file; |
| return url; |
| }; |
| |
| /** |
| * FIX: Internet Explorer returns an item having the _name_ equal to the given |
| * id, even if it's not having any id. This way it can return a different form |
| * field even if it's not a textarea. This workarounds the problem by |
| * specifically looking to search only elements having a certain tag name. |
| */ |
| HTMLArea.getElementById = function(tag, id) { |
| var el, i, objs = document.getElementsByTagName(tag); |
| for (i = objs.length; --i >= 0 && (el = objs[i]);) |
| if (el.id == id) |
| return el; |
| return null; |
| }; |
| |
| |
| |
| // EOF |
| // Local variables: // |
| // c-basic-offset:8 // |
| // indent-tabs-mode:t // |
| // End: // |