/* @preserve CLEditor WYSIWYG HTML Editor v1.3.0 http://premiumsoftware.net/cleditor requires jQuery v1.4.2 or later Copyright 2010, Chris Landowski, Premium Software, LLC Dual licensed under the MIT or GPL Version 2 licenses. */ // ==ClosureCompiler== // @compilation_level SIMPLE_OPTIMIZATIONS // @output_file_name jquery.cleditor.min.js // ==/ClosureCompiler== (function($) { //============== // jQuery Plugin //============== $.cleditor = { // Define the defaults used for all new cleditor instances defaultOptions: { width: 500, // width not including margins, borders or padding height: 250, // height not including margins, borders or padding controls: // controls to add to the toolbar "bold italic underline strikethrough subscript superscript | font size " + "style | color highlight removeformat | bullets numbering | outdent " + "indent | alignleft center alignright justify | undo redo | " + "rule image link unlink | cut copy paste pastetext | print source", colors: // colors in the color popup "FFF FCC FC9 FF9 FFC 9F9 9FF CFF CCF FCF " + "CCC F66 F96 FF6 FF3 6F9 3FF 6FF 99F F9F " + "BBB F00 F90 FC6 FF0 3F3 6CC 3CF 66C C6C " + "999 C00 F60 FC3 FC0 3C0 0CC 36F 63F C3C " + "666 900 C60 C93 990 090 399 33F 60C 939 " + "333 600 930 963 660 060 366 009 339 636 " + "000 300 630 633 330 030 033 006 309 303", fonts: // font names in the font popup "Arial,Arial Black,Comic Sans MS,Courier New,Narrow,Garamond," + "Georgia,Impact,Sans Serif,Serif,Tahoma,Trebuchet MS,Verdana", sizes: // sizes in the font size popup "1,2,3,4,5,6,7", styles: // styles in the style popup [["Paragraph", "

"], ["Header 1", "

"], ["Header 2", "

"], ["Header 3", "

"], ["Header 4","

"], ["Header 5","

"], ["Header 6","
"]], useCSS: false, // use CSS to style HTML when possible (not supported in ie) docType: // Document type contained within the editor '', docCSSFile: // CSS file used to style the document contained within the editor "", bodyStyle: // style to assign to document body contained within the editor "margin:4px; font:10pt Arial,Verdana; cursor:text" }, // Define all usable toolbar buttons - the init string property is // expanded during initialization back into the buttons object and // seperate object properties are created for each button. // e.g. buttons.size.title = "Font Size" buttons: { // name,title,command,popupName (""=use name) init: "bold,,|" + "italic,,|" + "underline,,|" + "strikethrough,,|" + "subscript,,|" + "superscript,,|" + "font,,fontname,|" + "size,Font Size,fontsize,|" + "style,,formatblock,|" + "color,Font Color,forecolor,|" + "highlight,Text Highlight Color,hilitecolor,color|" + "removeformat,Remove Formatting,|" + "bullets,,insertunorderedlist|" + "numbering,,insertorderedlist|" + "outdent,,|" + "indent,,|" + "alignleft,Align Text Left,justifyleft|" + "center,,justifycenter|" + "alignright,Align Text Right,justifyright|" + "justify,,justifyfull|" + "undo,,|" + "redo,,|" + "rule,Insert Horizontal Rule,inserthorizontalrule|" + "image,Insert Image,insertimage,url|" + "link,Insert Hyperlink,createlink,url|" + "unlink,Remove Hyperlink,|" + "cut,,|" + "copy,,|" + "paste,,|" + "pastetext,Paste as Text,inserthtml,|" + "print,,|" + "source,Show Source" }, // imagesPath - returns the path to the images folder imagesPath: function() { return imagesPath(); } }; // cleditor - creates a new editor for each of the matched textareas $.fn.cleditor = function(options) { // Create a new jQuery object to hold the results var $result = $([]); // Loop through all matching textareas and create the editors this.each(function(idx, elem) { if (elem.tagName == "TEXTAREA") { var data = $.data(elem, CLEDITOR); if (!data) data = new cleditor(elem, options); $result = $result.add(data); } }); // return the new jQuery object return $result; }; //================== // Private Variables //================== var // Misc constants BACKGROUND_COLOR = "backgroundColor", BUTTON = "button", BUTTON_NAME = "buttonName", CHANGE = "change", CLEDITOR = "cleditor", CLICK = "click", DISABLED = "disabled", DIV_TAG = "
", TRANSPARENT = "transparent", UNSELECTABLE = "unselectable", // Class name constants MAIN_CLASS = "cleditorMain", // main containing div TOOLBAR_CLASS = "cleditorToolbar", // toolbar div inside main div GROUP_CLASS = "cleditorGroup", // group divs inside the toolbar div BUTTON_CLASS = "cleditorButton", // button divs inside group div DISABLED_CLASS = "cleditorDisabled",// disabled button divs DIVIDER_CLASS = "cleditorDivider", // divider divs inside group div POPUP_CLASS = "cleditorPopup", // popup divs inside body LIST_CLASS = "cleditorList", // list popup divs inside body COLOR_CLASS = "cleditorColor", // color popup div inside body PROMPT_CLASS = "cleditorPrompt", // prompt popup divs inside body MSG_CLASS = "cleditorMsg", // message popup div inside body // Test for ie ie = $.browser.msie, ie6 = /msie\s6/i.test(navigator.userAgent), // Test for iPhone/iTouch/iPad iOS = /iphone|ipad|ipod/i.test(navigator.userAgent), // Popups are created once as needed and shared by all editor instances popups = {}, // Used to prevent the document click event from being bound more than once documentClickAssigned, // Local copy of the buttons object buttons = $.cleditor.buttons; //=============== // Initialization //=============== // Expand the buttons.init string back into the buttons object // and create seperate object properties for each button. // e.g. buttons.size.title = "Font Size" $.each(buttons.init.split("|"), function(idx, button) { var items = button.split(","), name = items[0]; buttons[name] = { stripIndex: idx, name: name, title: items[1] === "" ? name.charAt(0).toUpperCase() + name.substr(1) : items[1], command: items[2] === "" ? name : items[2], popupName: items[3] === "" ? name : items[3] }; }); delete buttons.init; //============ // Constructor //============ // cleditor - creates a new editor for the passed in textarea element cleditor = function(area, options) { var editor = this; // Get the defaults and override with options editor.options = options = $.extend({}, $.cleditor.defaultOptions, options); // Hide the textarea and associate it with this editor var $area = editor.$area = $(area) .hide() .data(CLEDITOR, editor) .blur(function() { // Update the iframe when the textarea loses focus updateFrame(editor, true); }); // Create the main container and append the textarea var $main = editor.$main = $(DIV_TAG) .addClass(MAIN_CLASS) .width(options.width) .height(options.height); // Create the toolbar var $toolbar = editor.$toolbar = $(DIV_TAG) .addClass(TOOLBAR_CLASS) .appendTo($main); // Add the first group to the toolbar var $group = $(DIV_TAG) .addClass(GROUP_CLASS) .appendTo($toolbar); // Add the buttons to the toolbar $.each(options.controls.split(" "), function(idx, buttonName) { if (buttonName === "") return true; // Divider if (buttonName == "|") { // Add a new divider to the group var $div = $(DIV_TAG) .addClass(DIVIDER_CLASS) .appendTo($group); // Create a new group $group = $(DIV_TAG) .addClass(GROUP_CLASS) .appendTo($toolbar); } // Button else { // Get the button definition var button = buttons[buttonName]; // Add a new button to the group var $buttonDiv = $(DIV_TAG) .data(BUTTON_NAME, button.name) .addClass(BUTTON_CLASS) .attr("title", button.title) .bind(CLICK, $.proxy(buttonClick, editor)) .appendTo($group) .hover(hoverEnter, hoverLeave); // Prepare the button image var map = {}; if (button.css) map = button.css; else if (button.image) map.backgroundImage = imageUrl(button.image); if (button.stripIndex) map.backgroundPosition = button.stripIndex * -24; $buttonDiv.css(map); // Add the unselectable attribute for ie if (ie) $buttonDiv.attr(UNSELECTABLE, "on"); // Create the popup if (button.popupName) createPopup(button.popupName, options, button.popupClass, button.popupContent, button.popupHover); } }); // Add the main div to the DOM and append the textarea $main.insertBefore($area) .append($area); // Bind the document click event handler if (!documentClickAssigned) { $(document).click(function(e) { // Dismiss all non-prompt popups var $target = $(e.target); if (!$target.add($target.parents()).is("." + PROMPT_CLASS)) hidePopups(); }); documentClickAssigned = true; } // Bind the window resize event when the width or height is auto or % if (/auto|%/.test("" + options.width + options.height)) $(window).resize(function() {refresh(editor);}); // Create the iframe and resize the controls refresh(editor); }; //=============== // Public Methods //=============== var fn = cleditor.prototype, // Expose the following private functions as methods on the cleditor object. // The closure compiler will rename the private functions. However, the // exposed method names on the cleditor object will remain fixed. methods = [ ["clear", clear], ["disable", disable], ["execCommand", execCommand], ["focus", focus], ["hidePopups", hidePopups], ["sourceMode", sourceMode, true], ["refresh", refresh], ["select", select], ["selectedHTML", selectedHTML, true], ["selectedText", selectedText, true], ["showMessage", showMessage], ["updateFrame", updateFrame], ["updateTextArea", updateTextArea] ]; $.each(methods, function(idx, method) { fn[method[0]] = function() { var editor = this, args = [editor]; // using each here would cast booleans into objects! for(var x = 0; x < arguments.length; x++) {args.push(arguments[x]);} var result = method[1].apply(editor, args); if (method[2]) return result; return editor; }; }); // change - shortcut for .bind("change", handler) or .trigger("change") fn.change = function(handler) { var $this = $(this); return handler ? $this.bind(CHANGE, handler) : $this.trigger(CHANGE); }; //=============== // Event Handlers //=============== // buttonClick - click event handler for toolbar buttons function buttonClick(e) { var editor = this, buttonDiv = e.target, buttonName = $.data(buttonDiv, BUTTON_NAME), button = buttons[buttonName], popupName = button.popupName, popup = popups[popupName]; // Check if disabled if (editor.disabled || $(buttonDiv).attr(DISABLED) == DISABLED) return; // Fire the buttonClick event var data = { editor: editor, button: buttonDiv, buttonName: buttonName, popup: popup, popupName: popupName, command: button.command, useCSS: editor.options.useCSS }; if (button.buttonClick && button.buttonClick(e, data) === false) return false; // Toggle source if (buttonName == "source") { // Show the iframe if (sourceMode(editor)) { delete editor.range; editor.$area.hide(); editor.$frame.show(); buttonDiv.title = button.title; } // Show the textarea else { editor.$frame.hide(); editor.$area.show(); buttonDiv.title = "Show Rich Text"; } // Enable or disable the toolbar buttons // IE requires the timeout setTimeout(function() {refreshButtons(editor);}, 100); } // Check for rich text mode else if (!sourceMode(editor)) { // Handle popups if (popupName) { var $popup = $(popup); // URL if (popupName == "url") { // Check for selection before showing the link url popup if (buttonName == "link" && selectedText(editor) === "") { showMessage(editor, "A selection is required when inserting a link.", buttonDiv); return false; } // Wire up the submit button click event handler $popup.children(":button") .unbind(CLICK) .bind(CLICK, function() { // Insert the image or link if a url was entered var $text = $popup.find(":text"), url = $.trim($text.val()); if (url !== "") execCommand(editor, data.command, url, null, data.button); // Reset the text, hide the popup and set focus $text.val("http://"); hidePopups(); focus(editor); }); } // Paste as Text else if (popupName == "pastetext") { // Wire up the submit button click event handler $popup.children(":button") .unbind(CLICK) .bind(CLICK, function() { // Insert the unformatted text replacing new lines with break tags var $textarea = $popup.find("textarea"), text = $textarea.val().replace(/\n/g, "
"); if (text !== "") execCommand(editor, data.command, text, null, data.button); // Reset the text, hide the popup and set focus $textarea.val(""); hidePopups(); focus(editor); }); } // Show the popup if not already showing for this button if (buttonDiv !== $.data(popup, BUTTON)) { showPopup(editor, popup, buttonDiv); return false; // stop propagination to document click } // propaginate to documnt click return; } // Print else if (buttonName == "print") editor.$frame[0].contentWindow.print(); // All other buttons else if (!execCommand(editor, data.command, data.value, data.useCSS, buttonDiv)) return false; } // Focus the editor focus(editor); } // hoverEnter - mouseenter event handler for buttons and popup items function hoverEnter(e) { var $div = $(e.target).closest("div"); $div.css(BACKGROUND_COLOR, $div.data(BUTTON_NAME) ? "#FFF" : "#FFC"); } // hoverLeave - mouseleave event handler for buttons and popup items function hoverLeave(e) { $(e.target).closest("div").css(BACKGROUND_COLOR, "transparent"); } // popupClick - click event handler for popup items function popupClick(e) { var editor = this, popup = e.data.popup, target = e.target; // Check for message and prompt popups if (popup === popups.msg || $(popup).hasClass(PROMPT_CLASS)) return; // Get the button info var buttonDiv = $.data(popup, BUTTON), buttonName = $.data(buttonDiv, BUTTON_NAME), button = buttons[buttonName], command = button.command, value, useCSS = editor.options.useCSS; // Get the command value if (buttonName == "font") // Opera returns the fontfamily wrapped in quotes value = target.style.fontFamily.replace(/"/g, ""); else if (buttonName == "size") { if (target.tagName == "DIV") target = target.children[0]; value = target.innerHTML; } else if (buttonName == "style") value = "<" + target.tagName + ">"; else if (buttonName == "color") value = hex(target.style.backgroundColor); else if (buttonName == "highlight") { value = hex(target.style.backgroundColor); if (ie) command = 'backcolor'; else useCSS = true; } // Fire the popupClick event var data = { editor: editor, button: buttonDiv, buttonName: buttonName, popup: popup, popupName: button.popupName, command: command, value: value, useCSS: useCSS }; if (button.popupClick && button.popupClick(e, data) === false) return; // Execute the command if (data.command && !execCommand(editor, data.command, data.value, data.useCSS, buttonDiv)) return false; // Hide the popup and focus the editor hidePopups(); focus(editor); } //================== // Private Functions //================== // checksum - returns a checksum using the Adler-32 method function checksum(text) { var a = 1, b = 0; for (var index = 0; index < text.length; ++index) { a = (a + text.charCodeAt(index)) % 65521; b = (b + a) % 65521; } return (b << 16) | a; } // clear - clears the contents of the editor function clear(editor) { editor.$area.val(""); updateFrame(editor); } // createPopup - creates a popup and adds it to the body function createPopup(popupName, options, popupTypeClass, popupContent, popupHover) { // Check if popup already exists if (popups[popupName]) return popups[popupName]; // Create the popup var $popup = $(DIV_TAG) .hide() .addClass(POPUP_CLASS) .appendTo("body"); // Add the content // Custom popup if (popupContent) $popup.html(popupContent); // Color else if (popupName == "color") { var colors = options.colors.split(" "); if (colors.length < 10) $popup.width("auto"); $.each(colors, function(idx, color) { $(DIV_TAG).appendTo($popup) .css(BACKGROUND_COLOR, "#" + color); }); popupTypeClass = COLOR_CLASS; } // Font else if (popupName == "font") $.each(options.fonts.split(","), function(idx, font) { $(DIV_TAG).appendTo($popup) .css("fontFamily", font) .html(font); }); // Size else if (popupName == "size") $.each(options.sizes.split(","), function(idx, size) { $(DIV_TAG).appendTo($popup) .html("" + size + ""); }); // Style else if (popupName == "style") $.each(options.styles, function(idx, style) { $(DIV_TAG).appendTo($popup) .html(style[1] + style[0] + style[1].replace("<", "
'); popupTypeClass = PROMPT_CLASS; } // Paste as Text else if (popupName == "pastetext") { $popup.html('Paste your content here and click submit.

'); popupTypeClass = PROMPT_CLASS; } // Add the popup type class name if (!popupTypeClass && !popupContent) popupTypeClass = LIST_CLASS; $popup.addClass(popupTypeClass); // Add the unselectable attribute to all items if (ie) { $popup.attr(UNSELECTABLE, "on") .find("div,font,p,h1,h2,h3,h4,h5,h6") .attr(UNSELECTABLE, "on"); } // Add the hover effect to all items if ($popup.hasClass(LIST_CLASS) || popupHover === true) $popup.children().hover(hoverEnter, hoverLeave); // Add the popup to the array and return it popups[popupName] = $popup[0]; return $popup[0]; } // disable - enables or disables the editor function disable(editor, disabled) { // Update the textarea and save the state if (disabled) { editor.$area.attr(DISABLED, DISABLED); editor.disabled = true; } else { editor.$area.removeAttr(DISABLED); delete editor.disabled; } // Switch the iframe into design mode. // ie6 does not support designMode. // ie7 & ie8 do not properly support designMode="off". try { if (ie) editor.doc.body.contentEditable = !disabled; else editor.doc.designMode = !disabled ? "on" : "off"; } // Firefox 1.5 throws an exception that can be ignored // when toggling designMode from off to on. catch (err) {} // Enable or disable the toolbar buttons refreshButtons(editor); } // execCommand - executes a designMode command function execCommand(editor, command, value, useCSS, button) { // Restore the current ie selection restoreRange(editor); // Set the styling method if (!ie) { if (useCSS === undefined || useCSS === null) useCSS = editor.options.useCSS; editor.doc.execCommand("styleWithCSS", 0, useCSS.toString()); } // Execute the command and check for error var success = true, description; if (ie && command.toLowerCase() == "inserthtml") getRange(editor).pasteHTML(value); else { try { success = editor.doc.execCommand(command, 0, value || null); } catch (err) { description = err.description; success = false; } if (!success) { if ("cutcopypaste".indexOf(command) > -1) showMessage(editor, "For security reasons, your browser does not support the " + command + " command. Try using the keyboard shortcut or context menu instead.", button); else showMessage(editor, (description ? description : "Error executing the " + command + " command."), button); } } // Enable the buttons refreshButtons(editor); return success; } // focus - sets focus to either the textarea or iframe function focus(editor) { setTimeout(function() { if (sourceMode(editor)) editor.$area.focus(); else editor.$frame[0].contentWindow.focus(); refreshButtons(editor); }, 0); } // getRange - gets the current text range object function getRange(editor) { if (ie) return getSelection(editor).createRange(); return getSelection(editor).getRangeAt(0); } // getSelection - gets the current text range object function getSelection(editor) { if (ie) return editor.doc.selection; return editor.$frame[0].contentWindow.getSelection(); } // Returns the hex value for the passed in string. // hex("rgb(255, 0, 0)"); // #FF0000 // hex("#FF0000"); // #FF0000 // hex("#F00"); // #FF0000 function hex(s) { var m = /rgba?\((\d+), (\d+), (\d+)/.exec(s), c = s.split(""); if (m) { s = ( m[1] << 16 | m[2] << 8 | m[3] ).toString(16); while (s.length < 6) s = "0" + s; } return "#" + (s.length == 6 ? s : c[1] + c[1] + c[2] + c[2] + c[3] + c[3]); } // hidePopups - hides all popups function hidePopups() { $.each(popups, function(idx, popup) { $(popup) .hide() .unbind(CLICK) .removeData(BUTTON); }); } // imagesPath - returns the path to the images folder function imagesPath() { var cssFile = "jquery.cleditor.css", href = $("link[href$='" + cssFile +"']").attr("href"); return href.substr(0, href.length - cssFile.length) + "images/"; } // imageUrl - Returns the css url string for a filemane function imageUrl(filename) { return "url(" + imagesPath() + filename + ")"; } // refresh - creates the iframe and resizes the controls function refresh(editor) { var $main = editor.$main, options = editor.options; // Remove the old iframe if (editor.$frame) editor.$frame.remove(); // Create a new iframe var $frame = editor.$frame = $('