1. Michael Shepanski
  2. jquery-wysiwym

Commits

Michael Shepanski  committed 90c0e0a

YEAY INITIAL VERSION

  • Participants
  • Branches default

Comments (0)

Files changed (11)

File AUTHORS.txt

View file
+Primary Authors:
+    * Michael Shepanski

File LICENSE.txt

View file
+Copyright (c) 2010, Michael Shepanski
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+    * Redistributions of source code must retain the above copyright notice,
+      this list of conditions and the following disclaimer.
+    * Redistributions in binary form must reproduce the above copyright notice,
+      this list of conditions and the following disclaimer in the documentation
+      and/or other materials provided with the distribution.
+    * Neither the name django-mingus nor the names of its contributors
+      may be used to endorse or promote products derived from this software without
+      specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

File README.txt

View file
+==============
+jquery-wysiwym
+==============
+
+This is a script that will convert a standard textarea into a wysiwym editor
+similar to what StackOverflow.com offers.
+
+
+REQUIREMENTS
+------------
+* jquery-1.4.4.js or higher (required).
+* showdown-0.9.js or higher (required for live preview).
+
+
+INSTALL
+-------
+1.) Copy the 'wysiwym' directory to the media location in your site directory.
+
+2.) Include wysiwym.js, markdown.js and wysiwym.css to your site header.
+    <script type='text/javascript' src='/media/js/wysiwym/wysiwym.js'></script>
+    <script type='text/javascript' src='/media/js/wysiwym/markdown.js'></script>
+    <link type='text/css' rel='stylesheet' href='/media/js/wysiwym/wysiwym.css'/>
+
+3.) Hook in the script by calling .wysiwym() on the textarea element
+    in your page.  That's It! ;)
+    $('#mytextarea').wysiwym(WysiwymMarkdown, options);
+
+
+OPTIONS
+-------
+The options argument to wysiwym is a Javascript object that knows about the
+following attributes:
+
+theme           - Color theme to use (light or dark); Default: 'light'.
+helpEnabled     - Set false to disable the help menu; Default: true.
+helpContainer   - jQuery element to place help; Default: undefined (auto-created).
+helpFloat       - CSS float value for helpContainer and helpToggle link; Default 'left'.
+helpToggle      - Set false to always show the help menu (disable toggle); Default: true.
+helpTextShow    - Toggle text to display when help is not visible; Default: 'show markup syntax'.
+helpTextHide    - Toggle text to display when help is visible;  Default: 'hide markup syntax'.
+buttonContainer - jQuery element to place buttons; Default: undefined (auto-created).

File examples/basic.dark.html

View file
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html>
+  <head>
+    <title>Basic Example - jquery-wysiwym</title>
+    <link type='text/css' rel='stylesheet' href='example.css'/>
+    <link type='text/css' rel='stylesheet' href='../wysiwym/wysiwym.css'/>
+    <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.4/jquery.min.js"></script>
+    <script type='text/javascript' src='../wysiwym/wysiwym.js'></script>
+    <script type='text/javascript' src='../wysiwym/markdown.js'></script>
+    <script type='text/javascript'>
+      $(function() {
+        $('#mytextarea').wysiwym(WysiwymMarkdown, {
+            theme: 'dark',
+            helpEnabled: true,
+            helpToggle: true
+        });
+      });
+    </script>
+  </head>
+  <body class='dark'>
+    <div id='container'>
+      <h1>Basic Wysiwym Markdown Editor</h1>
+      <p>The editor on this page is using wysiwym. Currently the only markup language
+        supported is Markdown. However, the library lends itself to be easily adapted
+        to other markup languages. If you write an extention for another markup
+        language and would like to help out with this package, I would love to
+        include it in future releases.  You can download or contibute via the
+        <a href='https://bitbucket.org/mjs7231/jquery-wysiwym'>jquery-wysiwym Bitbucket repository</a>.
+      </p>
+      <textarea id='mytextarea'></textarea>
+    </div>
+  </body>
+</html>

File examples/basic.light.html

View file
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html>
+  <head>
+    <title>Basic Example - jquery-wysiwym</title>
+    <link type='text/css' rel='stylesheet' href='example.css'/>
+    <link type='text/css' rel='stylesheet' href='../wysiwym/wysiwym.css'/>
+    <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.4/jquery.min.js"></script>
+    <script type='text/javascript' src='../wysiwym/wysiwym.js'></script>
+    <script type='text/javascript' src='../wysiwym/markdown.js'></script>
+    <script type='text/javascript'>
+      $(function() {
+        $('#mytextarea').wysiwym(WysiwymMarkdown, {
+            theme: 'light',
+            helpEnabled: true,
+            helpToggle: true
+        });
+      });
+    </script>
+  </head>
+  <body>
+    <div id='container'>
+      <h1>Basic Wysiwym Markdown Editor</h1>
+      <p>The editor on this page is using wysiwym. Currently the only markup language
+        supported is Markdown. However, the library lends itself to be easily adapted
+        to other markup languages. If you write an extention for another markup
+        language and would like to help out with this package, I would love to
+        include it in future releases.  You can download or contibute via the
+        <a href='https://bitbucket.org/mjs7231/jquery-wysiwym'>jquery-wysiwym Bitbucket repository</a>.
+      </p>
+      <textarea id='mytextarea'></textarea>
+    </div>
+  </body>
+</html>

File examples/example.css

View file
+/*-------------------------------------------------------------------
+ * Example CSS
+ * NOTE: These styles are for the demo page, which have nothing
+ * to do with the actual styles for the wysiwym editor. If you
+ * are looking for the light, dark themes for the wysiwym button
+ * and help menu, you should look in wysiwym.css.
+ *---------------------------------------------------------------- */
+body {
+  background-color: #f5f5f5;
+  font-family: tahoma, verdana, arial;
+  font-size: 14px;
+  color: #666;
+}
+a {
+  color: #a50;
+  text-decoration: none;
+}
+a:hover {
+  color: #292;
+  text-decoration: underline;
+}
+#container {
+  width: 800px;
+}
+h1 {
+  font-size: 24px;
+  font-weight: bold;
+  color: #555;
+  margin-bottom: 10px;
+}
+p {
+  margin: 10px 0px 20px 0px;
+}
+textarea {
+  display: block;
+  width: 500px;
+  height: 150px;
+  padding: 3px;
+  margin-top: 3px;
+  font-family: Consolas, Menlo, Monaco, 'Lucida Console', monospace, serif;
+}
+
+/*--- Demo dark theme ---*/
+body.dark {
+  background-color: #050505;
+  color: #777;
+}
+body.dark h1 {
+  color: #aaa;
+}
+textarea {
+  background-color: #282D31;
+  border-width: 0px;
+  color: #ccc;
+}
+

File wysiwym/buttons.dark.png

Added
New image

File wysiwym/buttons.light.png

Added
New image

File wysiwym/markdown.js

View file
+/* ---------------------------------------------------------------------------
+ * Markdown Button Set for Wysiwym Editor
+ *
+ * Created 2009, Michael Shepanski
+ * Creative Commons Attribution-Noncommercial-Share Alike 3.0 United States.
+ * http://creativecommons.org/licenses/by-nc-sa/3.0/us/
+ *
+ * Instructions:
+ *   1. Follow all instructions from the main ajaxcomments and
+ *      jquery.wysiwym.js files.
+ *
+ *   2. Add the following references to html template containing Django's
+ *      Comment application:
+ *        * ajaxcomments/wysiwym/markdown/wysiwym.markdown.css
+ *        * ajaxcomments/wysiwym/markdown/wysiwym.markdown.js
+ *        * ajaxcomments/wysiwym/markdown/showdown.js
+ *---------------------------------------------------------------------------- */
+
+function WysiwymMarkdown(textarea) {
+
+    // Public Variables
+    this.name = 'Markdown';          // Markup Language Name
+    this.buttonLink  = buttonLink;   // Link Callback
+    this.genericSpan = genericSpan;  // Public Callback function
+    this.genericLine = genericLine;  // Public Callback function
+
+    // Private Markdown Variables
+    var prefixRegex = /^[\s\>\.\*\+\-0-9]+\s/;
+    var lineTypes = {
+        BULLET: { prefix: '* ' },
+        NUMBER: { prefix: '# ' },
+        QUOTE:  { prefix: '> ' },
+        CODE:   { prefix: '    ' }
+    };
+
+    // Required Variable (Buttons to Display)
+    this.buttons = [
+        { name: 'Bold',        callback: this.genericSpan, args: {wrapText: '**', dummyText: 'strong text'} },
+        { name: 'Italic',      callback: this.genericSpan, args: {wrapText: '_',  dummyText: 'emphasized text'} },
+        { name: 'Link',        callback: this.buttonLink },
+        { name: 'Bullet List', callback: this.genericLine, args: {lineType: lineTypes.BULLET} },
+        { name: 'Quote',       callback: this.genericLine, args: {lineType: lineTypes.QUOTE} },
+        { name: 'Code',        callback: this.genericLine, args: {lineType: lineTypes.CODE} },
+    ];
+
+    // Used for display purposes only;  Provides a quick reference
+    // for people to see the syntax for this markup language.
+    this.helpItems = [
+        { name: 'Header', syntax: '## Header ##' },
+        { name: 'Bold',   syntax: '**bold**' },
+        { name: 'Italic', syntax: '_italics_' },
+        { name: 'Link',   syntax: '[pk!](http://google.com)' },
+        { name: 'List Item', syntax: '* list item' },
+        { name: 'Blockquote', syntax: '&gt; quoted text' },
+        { name: 'Large Code Block', syntax: '(Begin lines with 4 spaces)' },
+        { name: 'Inline Code Block', syntax: '&lt;code&gt;inline code&lt;/code&gt;' }
+    ]
+
+    // Register Return Line (for auto-indent)
+    $(textarea).bind('keydown', function(event) {
+        if (event.keyCode == 13)
+            return autoIndent();
+        return true;
+    });
+
+    /*-------------------------------------------------------------
+     * buttonLink:  Wraps the Selection with the specified
+     *   markdown link syntax, [This link](http://example.net/)
+     *------------------------------------------------------------- */
+    function buttonLink(event) {
+        var wtext = new WysiwymTextarea(textarea);
+        var cursor = wtext.getCursor();
+        var linkUrl = null;
+        // Set the LinkUrl if this is a proper Link
+        if (wtext.selectionIsWrapped("\[", "](")) {
+            var linkSuffix = wtext.getLine(cursor.end.line).text.substring(cursor.end.position);
+            var matches = linkSuffix.match(/^\]\(.*?\)/);
+            if (matches != null)
+                linkUrl = matches[0].substring(2, matches[0].length-1);
+        }
+        // Update the Textarea
+        if (linkUrl != null) {
+            wtext.unwrapSelection("[", "]("+linkUrl+")");
+        } else if (wtext.getSelectionLength() == 0) {
+            wtext.appendToSelection("Link Title");
+            wtext.wrapSelection("[", "](http://example.com)");
+        } else {
+            wtext.wrapSelection("[", "](http://example.com)");
+        }
+        wtext.update();
+    }
+
+    /*-------------------------------------------------------------
+     * genericSpan:  Wraps the Selection with the specified
+     *   args.wrapText defined on the button.
+     *------------------------------------------------------------- */
+    function genericSpan(event) {
+        var wrapText = event.data.args.wrapText;
+        var dummyText = event.data.args.dummyText;
+        var wtext = new WysiwymTextarea(textarea);
+        // Update the Textarea
+        if (wtext.selectionIsWrapped(wrapText)) {
+            wtext.unwrapSelection(wrapText);
+        } else if (wtext.getSelectionLength() == 0) {
+            wtext.appendToSelection(dummyText);
+            wtext.wrapSelection(wrapText);
+        } else {
+            wtext.wrapSelection(wrapText);
+        }
+        wtext.update();
+    }
+
+    /*-------------------------------------------------------------
+     * buttonLine:  Starts the Lines with the specified
+     *   args.prefixText defined on the button definition
+     *------------------------------------------------------------- */
+    function genericLine(event) {
+        var lineType = event.data.args.lineType;
+        var wtext = new WysiwymTextarea(textarea);
+        // Update the Textarea
+        if (!wtext.selectionLinesHavePrefix(lineType.prefix)) {
+            // Not all Lines are LineType; Remove all prefixes and Add LineType's prefix
+            var selectRange = wtext.getSelectionRange();
+            for (i=selectRange[0]; i<=selectRange[1]; i++) {
+                var lineInfo = getLineProperties(wtext, i);
+                wtext.removeLinePrefix(i, lineInfo.prefix);
+                wtext.insertLinePrefix(i, lineType.prefix);
+            }
+            // Check Blank Lines Around Selection
+            assertBlockWrap(wtext, lineType);
+        } else {
+            // All Lines are LineType; Remove all prefixes
+            wtext.removeSelectionLinesPrefix(lineType.prefix);
+        }
+        wtext.update();
+    }
+
+    /*-------------------------------------------------------------
+     * getLineProperties:  Returns information about a single line
+     *------------------------------------------------------------- */
+    function getLineProperties(wtext, lineNum) {
+        var line = wtext.getLine(lineNum);
+        for (var key in lineTypes) {
+            var type = lineTypes[key];
+            var prefix = type.prefix;
+            if (wtext.lineHasPrefix(lineNum, prefix)) {
+                var fullPrefix = line.text.match(prefixRegex)[0];
+                return { prefix:fullPrefix, text:line.text.substring(prefix, line.text.length) };
+            }
+        }
+        return { prefix:'', text:line.text };
+    }
+
+    /*-------------------------------------------------------------
+     * assertBlockWrap:  Asserts the selected lines have a line
+     *   with the specified prefix before and after.  If not, a
+     *   blank line is inserted to ensure the lineType is applied
+     *   correctly.
+     *------------------------------------------------------------- */
+    function assertBlockWrap(wtext, lineType) {
+        var cursor = wtext.getCursor();
+        // Check the Previous Line
+        if (cursor.start.line != 0) {
+            var lineInfo = getLineProperties(wtext, cursor.start.line-1);
+            if ((lineInfo.prefix != lineType.prefix) && (lineInfo.text.trim() != ""))
+                wtext.insertLinePrefix(cursor.start.line, wtext.NEWLINE);
+        }
+        // Check the Next Line
+        if (cursor.end.line != wtext.getNumLines()-1) {
+            var lineInfo = getLineProperties(wtext, cursor.end.line+1);
+            if ((lineInfo.prefix != lineType.prefix) && (lineInfo.text.trim() != ""))
+                wtext.insertLineSuffix(cursor.end.line, wtext.NEWLINE);
+        }
+    }
+
+    /*-------------------------------------------------------------
+     * autoIndent:  Auto-Indent the next line when enter pushed.
+     *------------------------------------------------------------- */
+    function autoIndent() {
+        var wtext = new WysiwymTextarea(textarea);
+        var cursor = wtext.getCursor();
+        var lineNum = cursor.start.line;
+        var line = wtext.getLine(lineNum);
+        var lineText = line.text.substring(0, cursor.start.position);
+        // Make sure there is Prefix Text
+        var matches = lineText.match(prefixRegex);
+        if (matches == null) { return true; }
+        // If there is no Actual Text, Clear the line
+        var prefix = matches[0].substring(0, matches[0].length);
+        if (prefix.length == cursor.start.position) {
+            // Return on Blank Indented Line (Clear Prefix)
+            wtext.removeLinePrefix(lineNum, prefix);
+            wtext.update();
+            return true;
+        } else {
+            // Normal Auto-Indent
+            wtext.insertSelectionPrefix(wtext.NEWLINE + prefix);
+            wtext.update();
+            return false;
+        }
+    }
+
+}

File wysiwym/wysiwym.css

View file
+/*-------------------------------------------------------------
+ * Styles for Wysiwym Buttons
+ *------------------------------------------------------------- */
+div.wysiwymButtons {
+  height: 18px;
+}
+div.wysiwymButtons div.button {
+  float: left;
+  background-image: url(buttons.light.png);
+  background-repeat: repeat-x;
+  background-position: 0px 0px;
+  border-right: 1px solid #aaa;
+}
+div.wysiwymButtons div.button:first-child {
+  border-top-left-radius: 3px;
+  border-bottom-left-radius: 3px;
+  -webkit-border-top-left-radius: 3px;
+  -webkit-border-bottom-left-radius: 3px;
+  -moz-border-radius-topleft: 3px;
+  -moz-border-radius-bottomleft: 3px;
+}
+div.wysiwymButtons div.button:last-child {
+  border-right: 0px;
+  border-top-right-radius: 3px;
+  border-bottom-right-radius: 3px;
+  -webkit-border-top-right-radius: 3px;
+  -webkit-border-bottom-right-radius: 3px;
+  -moz-border-radius-topright: 3px;
+  -moz-border-radius-bottomright: 3px;
+}
+div.wysiwymButtons div.button span.wrap {
+  width: 22px;
+  height: 18px;
+  display: block;
+  background-image: url(buttons.light.png);
+}
+
+/*--- Buttons for dark theme. ---*/
+div.wysiwymButtons.dark div.button {
+  background-image: url(buttons.dark.png);
+  border-right: 1px solid #222;
+}
+div.wysiwymButtons.dark div.button span.wrap {
+  background-image: url(buttons.dark.png);
+}
+
+/*--- Button Image Offsets (Normal) ---*/
+div.wysiwymButtons div.button span.text            { display: none; }
+div.wysiwymButtons div.button-bold span.wrap       { background-position: -1px -54px; }
+div.wysiwymButtons div.button-italic span.wrap     { background-position: -1px -81px; }
+div.wysiwymButtons div.button-link span.wrap       { background-position: -1px -106px; }
+div.wysiwymButtons div.button-bulletlist span.wrap { background-position: -1px -132px; }
+div.wysiwymButtons div.button-numberlist span.wrap { background-position: -1px -159px; }
+div.wysiwymButtons div.button-quote span.wrap      { background-position: -1px -184px; }
+div.wysiwymButtons div.button-code span.wrap       { background-position: -1px -211px; }
+
+/*--- Button Images Offsets (Hover) ---*/
+div.wysiwymButtons div.button:hover                      { background-position: 0px -30px; }
+div.wysiwymButtons div.button-bold:hover span.wrap       { background-position: -27px -54px; }
+div.wysiwymButtons div.button-italic:hover span.wrap     { background-position: -27px -81px; }
+div.wysiwymButtons div.button-link:hover span.wrap       { background-position: -27px -106px; }
+div.wysiwymButtons div.button-bulletlist:hover span.wrap { background-position: -27px -132px; }
+div.wysiwymButtons div.button-numberlist:hover span.wrap { background-position: -27px -159px; }
+div.wysiwymButtons div.button-quote:hover span.wrap      { background-position: -27px -184px; }
+div.wysiwymButtons div.button-code:hover span.wrap       { background-position: -27px -211px; }
+
+
+/*-------------------------------------------------------------
+ * Styles for Wysiwym Help
+ *------------------------------------------------------------- */
+a.wysiwymHelpToggle {
+  font-size: 10px;
+  line-height: 18px;
+}
+div.wysiwymHelp {
+  padding: 1px 5px;
+  line-height: 18px;
+  font-size: 10px;
+  background-color: #eee;
+  border-radius: 4px;
+  -moz-border-radius: 4px;
+  -webkit-border-radius: 4px;
+}
+div.wysiwymHelp td,
+div.wysiwymHelp th {
+  color: #666;
+  border-bottom: 1px dotted #ddd;
+}
+
+div.wysiwymHelp tr:last-child td,
+div.wysiwymHelp tr:last-child th {
+  border-width: 0px;
+}
+div.wysiwymHelp th {
+  padding-right: 10px;
+  text-align: left;
+  color: #777;
+}
+
+/*--- Help Table for dark theme ---*/
+div.wysiwymHelp.dark {
+  background-color: #111;
+}
+div.wysiwymHelp.dark td,
+div.wysiwymHelp.dark th {
+  color: #555;
+  border-bottom: 1px dotted #222;
+}
+div.wysiwymHelp.dark th {
+  color: #777;
+  font-weight: normal;
+}

File wysiwym/wysiwym.js

View file
+/*----------------------------------------------------------------------------------------------
+ * Simple Wysiwym Editor for jQuery
+ *   * wysiwym(markupSet, id)
+ *   * WysiwymTextarea(textarea)
+ *--------------------------------------------------------------------------------------------- */
+
+/*----------------------------------------------------------------------------------------------
+ * wysiwym(markupSet, id, buttons)
+ * @param markupSet:  (class)  Class definition of the markup set to use for this editor.
+ * @param id:         (string) Optionally specify a unique ID for this editor.
+ * @param buttons:    (jqElem) Optionally specify an element to place buttons into.
+ *--------------------------------------------------------------------------------------------- */
+$.fn.wysiwym = function(markupSet, options) {
+    if (options == undefined) { options = {}; }
+
+    // Setup the Default Options
+    var defaults = {}
+    defaults.theme = 'light';                     // Color theme to use ('light' or  'dark')
+    defaults.helpEnabled = true;                  // Set true to display the help dropdown
+    defaults.helpContainer = undefined;           // jQuery elem to place help (makes one by default)
+    defaults.helpFloat = 'left';                  // CSS float value for helpContainer and helpToggle link
+    defaults.helpToggle = true;                   // Set true to use a toggle link for help
+    defaults.helpTextShow = 'show markup syntax'; // Toggle text to display when help is not visible
+    defaults.helpTextHide = 'hide markup syntax'; // Toggle text to display when help is visible
+    defaults.buttonContainer = undefined;         // jQuery elem to place buttons (makes one by default)
+    options = $.extend(defaults, options);        // Merge options with defaults
+
+    // Setup the Wysiwym Editor
+    return this.each(function() {
+	var textarea = this;                      // Javascript Textarea Object
+	var jqTextarea = $(this);                 // jQuery Textarea Object
+	var markup = new markupSet(textarea);     // Markup Set to use for this Textarea
+
+	// Initialize the Basic Editor HTML
+	$(jqTextarea).wrap("<div class='wysiwymEditor'></div>");
+	var editor = jqTextarea.parent();
+
+	// Add the Button Container and all it's Buttons
+	var buttonContainer = options.buttonContainer;
+	if (buttonContainer == undefined)
+	    buttonContainer = $("<div></div>").insertBefore(jqTextarea);
+	buttonContainer.addClass('wysiwymButtons');
+	buttonContainer.addClass(options.theme);
+	$.each(markup.buttons, function() {
+	    var cssClass = this.name.toLowerCase().replace(' ', '');
+	    var buttonHtml = "";
+	    buttonHtml += "<div class='button button-"+cssClass+"' title='"+this.name+"'>";
+	    buttonHtml += "<span class='wrap'><span class='text'>"+this.name+"</span></span>";
+	    buttonHtml += "</div>";
+	    var button = $(buttonHtml);
+	    var data = { editor:editor, textarea:textarea, button:button, args:this.args };
+	    button.bind('click', data, this.callback);
+	    buttonContainer.append(button);
+	});
+
+	// Include the Help Menu (optional)
+	if (options.helpEnabled) {
+	    var helpContainer = options.helpContainer;
+	    if (helpContainer == undefined)
+		helpContainer = $("<div></div>").insertAfter(jqTextarea);
+	    helpContainer.addClass('wysiwymHelp');
+	    helpContainer.addClass(options.theme);
+	    var helpTable = $("<table cellpadding='0' cellspacing='0' border='0'><tbody></tbody></table>");
+	    var helpBody = helpTable.children('tbody');
+	    $.each(markup.helpItems, function() {
+		helpBody.append("<tr><th>"+this.name+"</th><td>"+this.syntax+"</td></tr>");
+	    });
+	    helpContainer.append(helpTable);
+	    // Display Help Menu Toggle Link (optional)
+	    if (options.helpToggle) {
+		var helpToggle = $("<a href='#' class='wysiwymHelpToggle'>"+options.helpTextShow+"</a>");
+		helpToggle.bind('click', function () {
+		    if (helpContainer.is(':visible')) {
+			helpContainer.slideUp('fast');
+			$(this).text(options.helpTextShow);
+		    } else {
+			helpContainer.slideDown('fast');
+			$(this).text(options.helpTextHide);
+		    }
+		    return false;
+		});
+		if (options.helpFloat) {
+		    helpToggle.css('float', options.helpFloat);
+		    helpContainer.css({'float':options.helpFloat, 'clear':options.helpFloat});
+		}
+		helpContainer.before(helpToggle).hide();
+	    }
+	}
+
+    });
+};
+
+
+/*----------------------------------------------------------------------------------------------
+ * Textarea Object
+ *   This can used used for some or all of your textarea modifications. It will keep track of
+ *   the the current text and cursor positions. The general idea is to keep track of the
+ *   textarea in terms of Line objects.  A line object contains a lineType and supporting text.
+ *--------------------------------------------------------------------------------------------- */
+function WysiwymTextarea(textarea) {
+    this.NEWLINE = "\n";                  // Newline Character specified to Browser
+    var NONE = { prefix: '' }             // Line Type NONE
+    var lines = new Array();              // Array Line objects { type, text, selected }
+    var cursor = {                        // Internal Cursor Object { start, end }
+        start: { line:0, position:0 },    // Cursor Start Position { line, position }
+        end: { line:0, position:0 },      // Cursor End Position { line, position }
+        scroll: 0                         // Current Scroll Position
+    }
+
+    /*-------------------------------------------------------------------
+     * Simple Public Functions
+     *------------------------------------------------------------------ */
+    this.getCursor = function()           { return cursor; }
+    this.getLines = function()            { return lines; }
+    this.getNumLines = function()         { return lines.length; }
+    this.getLine = function(lineNum)      { return lines[lineNum]; }
+    this.getSelectionRange = function()   { return [cursor.start.line, cursor.end.line]; }
+
+    /*-------------------------------------------------------------------
+     * @public getSelection: Return the current selected text.
+     *------------------------------------------------------------------ */
+    this.getSelection = function() {
+        var selection = "";
+        for (i in lines) {
+            var text = lines[i].text;
+            if ((i == cursor.start.line) && (i == cursor.end.line))
+                return text.substring(cursor.start.position, cursor.end.position);
+            if (i == cursor.start.line)
+                selection = selection + text.substring(cursor.start.position, text.length);
+            if ((i > cursor.start.line) && (i < cursor.end.line))
+                selection = selection + text;
+            if (i == cursor.end.line)
+                selection = selection + text.substring(0, cursor.end.position)
+        }
+        return selection;
+    }
+
+    /*-------------------------------------------------------------------
+     * @public getSelection: Return the length of Selection text
+     *------------------------------------------------------------------ */
+    this.getSelectionLength = function() {
+        var selection = this.getSelection();
+        return selection.length;
+    }
+
+    /*-------------------------------------------------------------------
+     * @public selectionHasPrefix: Return true if the current selection
+     *   has the specified prefix text. NOTE: Does not work with return lines.
+     *------------------------------------------------------------------ */
+    this.selectionHasPrefix = function(prefixText) {
+        var lineText = lines[cursor.start.line].text;
+        var start = cursor.start.position - prefixText.length;
+        if (start < 0) { return false; }
+        if (lineText.substring(start, cursor.start.position) != prefixText) { return false; }
+        return true;
+    }
+
+    /*-------------------------------------------------------------------
+     * @public selectionHasSuffix: Return true if the current selection
+     *   has the specified suffix text. NOTE: Does not work with return lines.
+     *------------------------------------------------------------------ */
+    this.selectionHasSuffix = function(suffixText) {
+        var lineText = lines[cursor.end.line].text;
+        var end = cursor.end.position + suffixText.length;
+        if (end > lineText.length) { return false; }
+        if (lineText.substring(cursor.end.position, end) != suffixText) { return false; }
+        return true;
+    }
+
+    /*-------------------------------------------------------------------
+     * @public prependToSelection: Insert the specified text at at the cursor
+     *   start position and make it selected.
+     *------------------------------------------------------------------ */
+    this.prependToSelection = function(insertText) {
+        var lineText = lines[cursor.start.line].text;
+        var newText = lineText.substring(0, cursor.start.position) +
+            insertText + lineText.substring(cursor.start.position, lineText.length);
+        lines[cursor.start.line].text = newText;
+        // Update Class Variables
+        if (cursor.start.line == cursor.end.line)
+            cursor.end.position += insertText.length;
+    }
+
+    /*-------------------------------------------------------------------
+     * @public appendToSelection: Insert the specified text at at the cursor
+     *   end position and make it selected.
+     *------------------------------------------------------------------ */
+    this.appendToSelection = function(insertText) {
+        var lineText = lines[cursor.end.line].text;
+        var newText = lineText.substring(0, cursor.end.position) +
+            insertText + lineText.substring(cursor.end.position, lineText.length);
+        lines[cursor.end.line].text = newText;
+        // Update Class Variables
+        cursor.end.position += insertText.length;
+    }
+
+    /*-------------------------------------------------------------------
+     * @public insertSelectionPrefix: Insert the specified text at at the cursor
+     *   start position.  NOTE: This does not work with return lines.
+     *------------------------------------------------------------------ */
+    this.insertSelectionPrefix = function(prefixText) {
+        var lineText = lines[cursor.start.line].text;
+        var newText = lineText.substring(0, cursor.start.position) +
+            prefixText + lineText.substring(cursor.start.position, lineText.length);
+        lines[cursor.start.line].text = newText;
+        // Update Class Variables
+        cursor.start.position += prefixText.length;
+        if (cursor.start.line == cursor.end.line)
+            cursor.end.position += prefixText.length;
+    }
+
+    /*-------------------------------------------------------------------
+     * @public insertSelectionSuffix: Insert the specified text at at the cursor
+     *   end position.  NOTE: This does not work with return lines.
+     *------------------------------------------------------------------ */
+    this.insertSelectionSuffix = function(suffixText) {
+        var lineText = lines[cursor.end.line].text;
+        var newText = lineText.substring(0, cursor.end.position) +
+            suffixText + lineText.substring(cursor.end.position, lineText.length);
+        lines[cursor.end.line].text = newText;
+    }
+
+    /*-------------------------------------------------------------------
+     * @public removePrefix: Remove the specified prefix text if it
+     *   matches with the passed in value.  Returns boolean True it
+     *   was successful, False otherwise.
+     *------------------------------------------------------------------ */
+    this.removeSelectionPrefix = function(prefixText) {
+        if (this.selectionHasPrefix(prefixText)) {
+            var lineText = lines[cursor.start.line].text;
+            var start = cursor.start.position - prefixText.length;
+            var newText = lineText.substring(0, start) +
+                lineText.substring(cursor.start.position, lineText.length);
+            lines[cursor.start.line].text = newText;
+            // Update Class Variables
+            cursor.start.position -= prefixText.length;
+            if (cursor.start.line == cursor.end.line)
+                cursor.end.position -= prefixText.length;
+            return true;
+        }
+        return false;
+    }
+
+    /*-------------------------------------------------------------------
+     * @public removeSuffix: Remove the specified suffix text if it
+     *   matches with the passed in value.  Returns boolean True if it
+     *   was successful, False otherwise.
+     *------------------------------------------------------------------ */
+    this.removeSelectionSuffix = function(suffixText) {
+        if (this.selectionHasSuffix(suffixText)) {
+            var lineText = lines[cursor.end.line].text;
+            var end = cursor.end.position + suffixText.length;
+            var newText = lineText.substring(0, cursor.end.position) +
+                lineText.substring(end, lineText.length);
+            lines[cursor.end.line].text = newText;
+            return true;
+        }
+        return false;
+    }
+
+    /*-------------------------------------------------------------------
+     * @public isWrapped: Return True if the selection is wrapped
+     *   in the specified prefix and suffix text.  Otherwise False.
+     *   NOTE: Does not work with return lines.
+     *------------------------------------------------------------------ */
+    this.selectionIsWrapped = function(prefixText, suffixText) {
+        if (suffixText == null) { suffixText = prefixText; }
+        return ((this.selectionHasPrefix(prefixText)) && (this.selectionHasSuffix(suffixText)))
+    }
+
+    /*-------------------------------------------------------------------
+     * @public wrap: Wrap the cursor or currently selected text
+     *   with the specified newTextBefore and newTextAfter.
+     *------------------------------------------------------------------ */
+    this.wrapSelection = function(prefixText, suffixText) {
+        if (suffixText == null) { suffixText = prefixText; }
+        this.insertSelectionPrefix(prefixText);
+        this.insertSelectionSuffix(suffixText);
+    }
+
+    /*-------------------------------------------------------------------
+     * @public unwrap: Unwrap the selection by removeing the prefix
+     *   and suffix text specified.  Returns boolean True it
+     *   was successful, False otherwise.
+     *------------------------------------------------------------------ */
+    this.unwrapSelection = function(prefixText, suffixText) {
+        if (suffixText == null) { suffixText = prefixText; }
+        if ((this.selectionHasPrefix(prefixText)) && (this.selectionHasSuffix(suffixText))) {
+            this.removeSelectionPrefix(prefixText);
+            this.removeSelectionSuffix(suffixText);
+            return true;
+        }
+        return false;
+    }
+
+    /*-------------------------------------------------------------------
+     * @public insertLinePrefix: Return True if the line has the
+     *   specified prefix.
+     *------------------------------------------------------------------ */
+    this.lineHasPrefix = function(lineNum, prefixText) {
+        return lines[lineNum].text.substring(0, prefixText.length) == prefixText;
+    }
+
+    /*-------------------------------------------------------------------
+     * @public insertLinePrefix: Add the Prefix to the specified line.
+     *------------------------------------------------------------------ */
+    this.insertLinePrefix = function(lineNum, prefixText) {
+        var lineText = lines[lineNum].text;
+        var newText = prefixText + lineText.substring(0, cursor.start.position) +
+            lineText.substring(cursor.start.position, lineText.length);
+        lines[lineNum].text = newText;
+        // Update Class Variables
+        if (lineNum == cursor.start.line)
+            cursor.start.position += prefixText.length;
+        if (lineNum == cursor.end.line)
+            cursor.end.position += prefixText.length;
+    }
+
+    /*-------------------------------------------------------------------
+     * @public removePrefix: Remove the specified prefix text if it
+     *   matches with the passed in value.  Returns boolean True it
+     *   was successful, False otherwise.
+     *------------------------------------------------------------------ */
+    this.removeLinePrefix = function(lineNum, prefixText) {
+        if (this.lineHasPrefix(lineNum, prefixText)) {
+            var lineText = lines[lineNum].text;
+            var newText = lineText.substring(prefixText.length, lineText.length);
+            lines[lineNum].text = newText;
+            // Update Class Variables
+            if (lineNum == cursor.start.line)
+                cursor.start.position -= prefixText.length;
+            if (lineNum == cursor.end.line)
+                cursor.end.position -= prefixText.length;
+            return true;
+        }
+        return false;
+    }
+
+    /*-------------------------------------------------------------------
+     * @public insertLineSuffix: Append the suffix to the end of the
+     *   specified line.
+     *------------------------------------------------------------------ */
+    this.insertLineSuffix = function(lineNum, suffixText) {
+        var lineText = lines[lineNum].text;
+        var newText = lineText.substring(0, cursor.start.position) +
+            lineText.substring(cursor.start.position, lineText.length) + suffixText;
+        lines[lineNum].text = newText;
+    }
+
+    /*-------------------------------------------------------------------
+     * @public insertLinePrefix: Return True if all lines in the
+     *   selection have the specified prefix.
+     *------------------------------------------------------------------ */
+    this.selectionLinesHavePrefix = function(prefixText) {
+        for (var i=cursor.start.line; i<=cursor.end.line; i++)
+            if (!this.lineHasPrefix(i, prefixText))
+                return false;
+        return true;
+    }
+
+    /*-------------------------------------------------------------------
+     * @public insertLinePrefix: Add the Prefix Text to all lines in
+     *   the selection.
+     *------------------------------------------------------------------ */
+    this.insertSelectionLinesPrefix = function(prefixText) {
+        for (var i=cursor.start.line; i<=cursor.end.line; i++)
+            this.insertLinePrefix(i, prefixText);
+    }
+
+    /*-------------------------------------------------------------------
+     * @public insertLinePrefix: Add the Prefix Text to all lines in
+     *   the selection.
+     *------------------------------------------------------------------ */
+    this.removeSelectionLinesPrefix = function(prefixText) {
+        for (var i=cursor.start.line; i<=cursor.end.line; i++)
+            this.removeLinePrefix(i, prefixText);
+    }
+
+    /*-------------------------------------------------------------------
+     * @public getTextareaProperties: Return the global textarea
+     *   properties (NOT by Line).  This returns the a Map containing:
+     *   { text, cursorStart, cursorEnd, selectionLength, scroll }
+     *------------------------------------------------------------------ */
+    this.getTextareaProperties = function() {
+        var text = ""               // Global Textarea Value
+        var cursorStart = 0;        // Cursor Start Position (chars from beginning of textarea)
+        var cursorEnd = 0;          // Cursor End Position (chars from beginning of textarea)
+        var selectionLength = 0;    // Selection Length (total chars)
+        var lineStart = 0;
+	for (var i=0; i<lines.length; i++) {
+            text += lines[i].text;
+            if (i == cursor.start.line)
+                cursorStart = lineStart + cursor.start.position;
+            if (i == cursor.end.line)
+                cursorEnd = lineStart + cursor.end.position;
+            lineStart += lines[i].text.length;
+        }
+        var selectionLength = cursorEnd - cursorStart;
+        return { text:text, cursorStart:cursorStart, cursorEnd:cursorEnd,
+            selectionLength:selectionLength, scroll:cursor.scroll }
+    }
+
+    /*-------------------------------------------------------------------
+     * @public update: Update the HTML textarea to reflect all changes
+     *   made within this class.  This is generally the last step in
+     *   defining a markup button.
+     *------------------------------------------------------------------ */
+    this.update = function() {
+        var textProperties = this.getTextareaProperties();
+        // Update the Textarea
+        $(textarea).val(textProperties.text);
+        if (textarea.createTextRange) {
+            range = textarea.createTextRange();
+            range.collapse(true);
+            range.moveStart('character', textProperties.cursorStart);
+            range.moveEnd('character', textProperties.selectionLength);
+            range.select();
+        } else if (textarea.setSelectionRange) {
+            textarea.setSelectionRange(textProperties.cursorStart, textProperties.cursorEnd);
+        }
+        textarea.scrollTop = textProperties.scroll;
+        textarea.focus();
+    }
+
+    /*-------------------------------------------------------------------
+     * @private getCursorProperties: Returns a Map object containing the
+     *   current the current cursor properties for scroll position,
+     *   cursor position and selection length.
+     *------------------------------------------------------------------ */
+    function getCursorProperties() {
+        textarea.focus();
+        var scroll = textarea.scrollTop;   // Current Scroll Position
+        var position = 0;                  // Current Cursor Position
+        var selection = "";                // Current Selection Length
+        if (document.selection) {
+            // Internet Explorer
+            selection = document.selection.createRange().text;
+            if ($.browser.msie) {
+                var range = document.selection.createRange();
+                var rangeCopy = range.duplicate();
+                rangeCopy.moveToElementText(textarea);
+                position = -1;
+                while(rangeCopy.inRange(range)) {
+                    rangeCopy.moveStart('character');
+                    position++;
+                }
+            } else {
+                // Opera
+                position = textarea.selectionStart;
+            }
+        } else {
+            // Mozilla
+            position = textarea.selectionStart;
+            selection = $(textarea).val().substring(position, textarea.selectionEnd);
+        }
+        return {scroll: scroll, position: position, length: selection.length}
+    }
+
+    /*-------------------------------------------------------------------
+     * @private setupPropertiesByLine: Updates the class variables for
+     *   lines in the Textarea and cursor start and end position
+     *   (in terms of lines).
+     *------------------------------------------------------------------ */
+    function setupPropertiesByLine() {
+        var text = $(textarea).val();                              // Initial Textarea Value
+        var cursorInfo = getCursorProperties();                    // Cursor Properties
+        var cursorStart = cursorInfo.position;                     // Global Cursor Start Position
+        var cursorEnd = cursorInfo.position + cursorInfo.length;   // Global Cursor End Position
+        // Parse the Current Textarea
+        var num = 0;               // Iter Line Number
+        var lineStart = 0;         // Iter Position
+        var lastLine = false;      // Flag Set on Last Line
+        while (!lastLine) {
+            // Find the Line Ending Position
+            var lineEnd = text.indexOf("\n", lineStart);
+            var charEnd = lineEnd;
+            if (lineEnd >= 0) { lineEnd += 1; }
+            else { lastLine = true; lineEnd = charEnd = text.length; }
+            // Get the Line Properties
+            var lineText = text.substring(lineStart, lineEnd);
+            var selected = (cursorStart <= charEnd) && (cursorEnd >= lineStart)
+            lines[num] = { text:lineText, selected:selected };
+            // Update the Cursor Information
+            if ((cursorStart >= lineStart) && (cursorStart <= charEnd))
+                cursor.start = { line: num, position:cursorStart-lineStart };
+            if ((cursorEnd >= lineStart) && (cursorEnd <= lineEnd))
+                cursor.end = { line:num, position:cursorEnd-lineStart};
+            // Update Properties for Next Line
+            lineStart = lineEnd;
+            num += 1;
+        }
+        // Save the Scroll Position
+        cursor.scroll = cursorInfo.scroll;
+    }
+
+    /*--- Main ---*/
+    setupPropertiesByLine();
+
+}
+
+/*----------------------------------------------------------------------
+ * Additional Javascript Prototypes
+ *-------------------------------------------------------------------- */
+String.prototype.trim  = function() { return this.replace(/^\s+|\s+$/g,""); }
+String.prototype.ltrim = function() { return this.replace(/^\s+/,""); }
+String.prototype.rtrim = function() { return this.replace(/\s+$/,""); }