/** * SCEditor * http://www.samclarke.com/2011/07/sceditor/ * * Copyright (C) 2011-2012, Sam Clarke (samclarke.com) * * SCEditor is licensed under the MIT license: * http://www.opensource.org/licenses/mit-license.php * * @fileoverview SCEditor - A lightweight WYSIWYG BBCode and HTML editor * @author Sam Clarke * @version 1.3.7 * @requires jQuery */ // ==ClosureCompiler== // @output_file_name jquery.sceditor.min.js // @compilation_level SIMPLE_OPTIMIZATIONS // ==/ClosureCompiler== /*jshint smarttabs: true, scripturl: true, jquery: true, devel:true, eqnull:true, curly: false */ /*global XMLSerializer: true*/ ;(function ($, window, document) { 'use strict'; var _templates = { html: '' + '' + '
' + '' + '' + '' + '' + '' + '', toolbarButton: 'Replaces any params in a template with the passed params.
* *If createHTML is passed it will use jQuery to create the HTML. The * same as doing: $(editor.tmpl("html", {params...}));
* * @param {string} templateName * @param {Object} params * @param {Boolean} createHTML * @private */ var _tmpl = function(name, params, createHTML) { var template = _templates[name]; $.each(params, function(name, val) { template = template.replace(new RegExp('\\{' + name + '\\}', 'g'), val); }); if(createHTML) template = $(template); return template; }; /** * SCEditor - A lightweight WYSIWYG editor * * @param {Element} el The textarea to be converted * @return {Object} options * @class sceditor * @name jQuery.sceditor */ $.sceditor = function (el, options) { /** * Alias of this * @private */ var base = this; /** * The textarea element being replaced * @private */ var $textarea = $(el); var textarea = el; /** * The div which contains the editor and toolbar * @private */ var $editorContainer; /** * The editors toolbar * @private */ var $toolbar; /** * The editors iframe which should be in design mode * @private */ var $wysiwygEditor; var wysiwygEditor; /** * The editors textarea for viewing source * @private */ var $textEditor; var textEditor; /** * The current dropdown * @private */ var $dropdown; /** * Array of all the commands key press functions * @private */ var keyPressFuncs = []; /** * Store the last cursor position. Needed for IE because it forgets * @private */ var lastRange; /** * The editors locale * @private */ var locale; /** * Stores a cache of preloaded images * @private */ var preLoadCache = []; var rangeHelper; var $blurElm; var init, replaceEmoticons, handleCommand, saveRange, handlePasteEvt, handlePasteData, handleKeyPress, handleFormReset, handleMouseDown, initEditor, initToolBar, initKeyPressFuncs, initResize, documentClickHandler, formSubmitHandler, initEmoticons, getWysiwygDoc, handleWindowResize, initLocale, updateToolBar, textEditorSelectedText, autofocus; /** * All the commands supported by the editor */ base.commands = $.extend({}, (options.commands || $.sceditor.commands)); /** * Initializer. Creates the editor iframe and textarea * @private * @name sceditor.init */ init = function () { $textarea.data("sceditor", base); base.options = $.extend({}, $.sceditor.defaultOptions, options); // Load locale if(base.options.locale && base.options.locale !== "en") initLocale(); // if either width or height are % based, add the resize handler to update the editor // when the window is resized var h = base.options.height, w = base.options.width; if((h && (h + "").indexOf("%") > -1) || (w && (w + "").indexOf("%") > -1)) $(window).resize(handleWindowResize); $editorContainer = $('').insertAfter($textarea); // create the editor initToolBar(); initEditor(); initKeyPressFuncs(); if(base.options.resizeEnabled) initResize(); if(base.options.id) $editorContainer.attr('id', base.options.id); $(document).click(documentClickHandler); $(textarea.form) .attr('novalidate','novalidate') .bind("reset", handleFormReset) .submit(formSubmitHandler); // load any textarea value into the editor base.val($textarea.hide().val()); /* // Pass the value though the getTextHandler if it is set so that // BBCode, ect. can be converted if(base.options.getTextHandler && base.options.supportedWysiwyg) { val = base.options.getTextHandler(val); base.setWysiwygEditorValue(val); } else { base.toggleTextMode(); base.setTextareaValue(val); } */ if(base.options.autofocus) autofocus(); // force into source mode if is a browser that can't handle // full editing if(!$.sceditor.isWysiwygSupported()) base.toggleTextMode(); if(base.options.toolbar.indexOf('emoticon') !== -1) initEmoticons(); // Can't use load event as it gets fired before CSS is loaded // in some browsers if(base.options.autoExpand) var interval = setInterval(function() { if (!document.readyState || document.readyState === "complete") { base.expandToContent(); clearInterval(interval); } }, 10); }; /** * Creates the editor iframe and textarea * @private */ initEditor = function () { var $doc, $body; $textEditor = $('').hide(); $wysiwygEditor = $(''); if(window.location.protocol === "https:") $wysiwygEditor.attr("src", "javascript:false"); // add the editor to the HTML and store the editors element $editorContainer.append($wysiwygEditor).append($textEditor); wysiwygEditor = $wysiwygEditor[0]; textEditor = $textEditor[0]; base.width(base.options.width || $textarea.width()); base.height(base.options.height || $textarea.height()); getWysiwygDoc().open(); getWysiwygDoc().write(_tmpl("html", { charset: base.options.charset, style: base.options.style })); getWysiwygDoc().close(); base.readOnly(!!base.options.readOnly); $doc = $(getWysiwygDoc()); $body = $doc.find("body"); // Add IE version class to the HTML element so can apply // conditional styling without CSS hacks if($.sceditor.ie) $doc.find("html").addClass('ie' + $.sceditor.ie); // iframe overflow fix if(/iPhone|iPod|iPad| wosbrowser\//i.test(navigator.userAgent)) $body.height('100%'); // set the key press event $body.keypress(handleKeyPress); $doc.keypress(handleKeyPress) .mousedown(handleMouseDown) .bind("beforedeactivate keyup", saveRange) .focus(function() { lastRange = null; }); if(base.options.rtl) { $body.attr('dir', 'rtl'); $textEditor.attr('dir', 'rtl'); } if(base.options.enablePasteFiltering) $body.bind("paste", handlePasteEvt); if(base.options.autoExpand) $doc.bind("keyup", base.expandToContent); rangeHelper = new $.sceditor.rangeHelper(wysiwygEditor.contentWindow); }; /** * Creates the toolbar and appends it to the container * @private */ initToolBar = function () { var $group, $button, buttons, i, x, buttonClick, groups = base.options.toolbar.split("|"); buttonClick = function () { var self = $(this); if(!self.hasClass('disabled')) handleCommand(self, base.commands[self.data("sceditor-command")]); return false; }; $toolbar = $(''); var rows = base.options.toolbar.split("||"); for (var r=0; r < rows.length; r++) { var row = $(''); var groups = rows[r].split("|"), buttons, accessibilityName, button, i; for (i=0; i < groups.length; i++) { $group = $(''); buttons = groups[i].split(","); for (x=0; x < buttons.length; x++) { // the button must be a valid command otherwise ignore it if(!base.commands[buttons[x]]) continue; $button = _tmpl("toolbarButton", { name: buttons[x], dispName: base.commands[buttons[x]].tooltip || buttons[x] }, true).click(buttonClick); if(base.commands[buttons[x]].hasOwnProperty("tooltip")) $button.attr('title', base._(base.commands[buttons[x]].tooltip)); if(base.commands[buttons[x]].exec) $button.data('sceditor-wysiwygmode', true); else $button.addClass('disabled'); if(base.commands[buttons[x]].txtExec) $button.data('sceditor-txtmode', true); $group.append($button); } row.append($group); } $toolbar.append(row); } // append the toolbar to the toolbarContainer option if given if(base.options.toolbarContainer) $(base.options.toolbarContainer).append($toolbar); else $editorContainer.append($toolbar); }; /** * Autofocus the editor * @private */ autofocus = function() { var doc = wysiwygEditor.contentWindow.document, body = doc.body, rng; if(!doc.createRange) return base.focus(); if(!body.firstChild) return; rng = doc.createRange(); rng.setStart(body.firstChild, 0); rng.setEnd(body.firstChild, 0); rangeHelper.selectRange(rng); body.focus(); }; /** * Gets the readOnly property of the editor * * @since 1.3.5 * @function * @memberOf jQuery.sceditor.prototype * @name readOnly * @return {boolean} */ /** * Sets the readOnly property of the editor * * @param {boolean} readOnly * @since 1.3.5 * @function * @memberOf jQuery.sceditor.prototype * @name readOnly^2 * @return {this} */ base.readOnly = function(readOnly) { if(typeof readOnly !== 'boolean') return $textEditor.attr('readonly') === 'readonly'; getWysiwygDoc().body.contentEditable = !readOnly; if(!readOnly) $textEditor.removeAttr('readonly'); else $textEditor.attr('readonly', 'readonly'); updateToolBar(readOnly); return this; }; /** * Updates the toolbar to disable/enable the appropriate buttons * @private */ updateToolBar = function(disable) { $toolbar.find('.sceditor-button').removeClass('disabled'); $toolbar.find('.sceditor-button').each(function () { var button = $(this); if(disable === true) button.addClass('disabled'); else if(base.inSourceMode() && !button.data('sceditor-txtmode')) button.addClass('disabled'); else if (!base.inSourceMode() && !button.data('sceditor-wysiwygmode')) button.addClass('disabled'); }); }; /** * Creates an array of all the key press functions * like emoticons, ect. * @private */ initKeyPressFuncs = function () { $.each(base.commands, function (command, values) { if(values.keyPress) keyPressFuncs.push(values.keyPress); }); }; /** * Gets the width of the editor in px * * @since 1.3.5 * @function * @memberOf jQuery.sceditor.prototype * @name width * @return {int} */ /** * Sets the width of the editor * * @param {int} width Width in px * @since 1.3.5 * @function * @memberOf jQuery.sceditor.prototype * @name width^2 * @return {this} */ base.width = function (width) { if(!width) return $editorContainer.width(); $editorContainer.width(width); // fix the height and width of the textarea/iframe $wysiwygEditor.width(width); $wysiwygEditor.width(width + (width - $wysiwygEditor.outerWidth(true))); $textEditor.width(width); $textEditor.width(width + (width - $textEditor.outerWidth(true))); return this; }; /** * Gets the height of the editor in px * * @since 1.3.5 * @function * @memberOf jQuery.sceditor.prototype * @name height * @return {int} */ /** * Sets the height of the editor * * @param {int} height Height in px * @since 1.3.5 * @function * @memberOf jQuery.sceditor.prototype * @name height^2 * @return {this} */ base.height = function (height) { if(!height) return $editorContainer.height(); $editorContainer.height(height); height -= !base.options.toolbarContainer ? $toolbar.outerHeight(true) : 0; // fix the height and width of the textarea/iframe $wysiwygEditor.height(height); $wysiwygEditor.height(height + (height - $wysiwygEditor.outerHeight(true))); $textEditor.height(height); $textEditor.height(height + (height - $textEditor.outerHeight(true))); return this; }; /** * Expands the editor to the size of it's content * * @since 1.3.5 * @param {Boolean} [ignoreMaxHeight=false] * @function * @name expandToContent * @memberOf jQuery.sceditor.prototype * @see #resizeToContent */ base.expandToContent = function(ignoreMaxHeight) { var doc = getWysiwygDoc(), currentHeight = $editorContainer.height(), height = doc.body.scrollHeight || doc.documentElement.scrollHeight, padding = (currentHeight - $wysiwygEditor.height()), maxHeight = base.options.resizeMaxHeight || ((base.options.height || $textarea.height()) * 2); height += padding; if(ignoreMaxHeight !== true && height > maxHeight) height = maxHeight; if(height > currentHeight) base.height(height); }; /** * Creates the resizer. * @private */ initResize = function () { var $grip = $(''), // cover is used to cover the editor iframe so document still gets mouse move events $cover = $(''), startX = 0, startY = 0, startWidth = 0, startHeight = 0, origWidth = $editorContainer.width(), origHeight = $editorContainer.height(), dragging = false, minHeight, maxHeight, minWidth, maxWidth, mouseMoveFunc; minHeight = base.options.resizeMinHeight || origHeight / 1.5; maxHeight = base.options.resizeMaxHeight || origHeight * 2.5; minWidth = base.options.resizeMinWidth || origWidth / 1.25; maxWidth = base.options.resizeMaxWidth || origWidth * 1.25; mouseMoveFunc = function (e) { var newHeight = startHeight + (e.pageY - startY), newWidth = startWidth + (e.pageX - startX); if (newWidth >= minWidth && (maxWidth < 0 || newWidth <= maxWidth)) base.width(newWidth); if (newHeight >= minHeight && (maxHeight < 0 || newHeight <= maxHeight)) base.height(newHeight); e.preventDefault(); }; $editorContainer.append($grip); $editorContainer.append($cover.hide()); $grip.mousedown(function (e) { startX = e.pageX; startY = e.pageY; startWidth = $editorContainer.width(); startHeight = $editorContainer.height(); dragging = true; $editorContainer.addClass('resizing'); $cover.show(); $(document).bind('mousemove', mouseMoveFunc); e.preventDefault(); }); $(document).mouseup(function (e) { if(!dragging) return; dragging = false; $cover.hide(); $editorContainer.removeClass('resizing'); $(document).unbind('mousemove', mouseMoveFunc); e.preventDefault(); }); }; /** * Handles the forms submit event * @private */ formSubmitHandler = function(e) { base.updateTextareaValue(); $(this).removeAttr('novalidate'); if(this.checkValidity && !this.checkValidity()) e.preventDefault(); $(this).attr('novalidate','novalidate'); base.blur(); }; /** * Destroys the editor, removing all elements and * event handlers. * * @function * @name destory * @memberOf jQuery.sceditor.prototype */ base.destory = function () { $(document).unbind('click', documentClickHandler); $(window).unbind('resize', handleWindowResize); $(textarea.form).removeAttr('novalidate') .unbind('submit', formSubmitHandler) .unbind("reset", handleFormReset); $(getWysiwygDoc()).find('*').remove(); $(getWysiwygDoc()).unbind("keypress mousedown beforedeactivate keyup focus paste keypress"); $editorContainer.find('*').remove(); $editorContainer.remove(); $textarea.removeData("sceditor").removeData("sceditorbbcode").show(); }; /** * Preloads the emoticon images * Idea from: http://engineeredweb.com/blog/09/12/preloading-images-jquery-and-javascript * @private */ initEmoticons = function () { // prefix emoticon root to emoticon urls if(base.options.emoticonsRoot && base.options.emoticons) { $.each(base.options.emoticons, function (idx, emoticons) { $.each(emoticons, function (key, url) { base.options.emoticons[idx][key] = base.options.emoticonsRoot + url; }); }); } var emoticons = $.extend({}, base.options.emoticons.more, base.options.emoticons.dropdown, base.options.emoticons.hidden), emoticon; $.each(emoticons, function (key, url) { // In SMF an empty entry means a new line if (key == '') emoticon = document.createElement('br'); else { emoticon = document.createElement('img'); emoticon.src = url; } preLoadCache.push(emoticon); }); }; /** * Creates a menu item drop down * * @param HTMLElement menuItem The button to align the drop down with * @param string dropDownName Used for styling the dropown, will be a class sceditor-dropDownName * @param string content The HTML content of the dropdown * @param bool ieUnselectable If to add the unselectable attribute to all the contents elements. Stops IE from deselecting the text in the editor * @function * @name createDropDown * @memberOf jQuery.sceditor.prototype */ base.createDropDown = function (menuItem, dropDownName, content, ieUnselectable) { base.closeDropDown(); // IE needs unselectable attr to stop it from unselecting the text in the editor. // The editor can cope if IE does unselect the text it's just not nice. if(ieUnselectable !== false) { $(content).find(':not(input,textarea)') .filter(function() { return this.nodeType===1; }) .attr('unselectable', 'on'); } var css = { top: menuItem.offset().top, left: menuItem.offset().left }; $.extend(css, base.options.dropDownCss); $dropdown = $('') .css(css) .append(content) .appendTo($('body')) .click(function (e) { // stop clicks within the dropdown from being handled e.stopPropagation(); }); }; /** * Handles any document click and closes the dropdown if open * @private */ documentClickHandler = function (e) { // ignore right clicks if(e.which !== 3) base.closeDropDown(); }; handlePasteEvt = function(e) { var elm = getWysiwygDoc().body, checkCount = 0, pastearea = elm.ownerDocument.createElement('div'), prePasteContent = elm.ownerDocument.createDocumentFragment(); rangeHelper.saveRange(); document.body.appendChild(pastearea); if (e && e.clipboardData && e.clipboardData.getData) { var html, handled=true; if ((html = e.clipboardData.getData('text/html')) || (html = e.clipboardData.getData('text/plain'))) pastearea.innerHTML = html; else handled = false; if(handled) { handlePasteData(elm, pastearea); if (e.preventDefault) { e.stopPropagation(); e.preventDefault(); } return false; } } while(elm.firstChild) prePasteContent.appendChild(elm.firstChild); function handlePaste(elm, pastearea) { if (elm.childNodes.length > 0) { while(elm.firstChild) pastearea.appendChild(elm.firstChild); while(prePasteContent.firstChild) elm.appendChild(prePasteContent.firstChild); handlePasteData(elm, pastearea); } else { // Allow max 25 checks before giving up. // Needed inscase empty input is posted or // something gose wrong. if(checkCount > 25) { while(prePasteContent.firstChild) elm.appendChild(prePasteContent.firstChild); return; } ++checkCount; setTimeout(function () { handlePaste(elm, pastearea); }, 20); } } handlePaste(elm, pastearea); base.focus(); return true; }; /** * @param {Element} elm * @param {Element} pastearea * @private */ handlePasteData = function(elm, pastearea) { // fix any invalid nesting $.sceditor.dom.fixNesting(pastearea); var pasteddata = pastearea.innerHTML; if(base.options.getHtmlHandler) pasteddata = base.options.getHtmlHandler(pasteddata, $(pastearea)); pastearea.parentNode.removeChild(pastearea); if(base.options.getTextHandler) pasteddata = base.options.getTextHandler(pasteddata, true); rangeHelper.restoreRange(); rangeHelper.insertHTML(pasteddata); }; /** * Closes the current drop down * * @param bool focus If to focus the editor on close * @function * @name closeDropDown * @memberOf jQuery.sceditor.prototype */ base.closeDropDown = function (focus) { if($dropdown) { $dropdown.remove(); $dropdown = null; } if(focus === true) base.focus(); }; /** * Gets the WYSIWYG editors document * @private */ getWysiwygDoc = function () { if (wysiwygEditor.contentDocument) return wysiwygEditor.contentDocument; if (wysiwygEditor.contentWindow && wysiwygEditor.contentWindow.document) return wysiwygEditor.contentWindow.document; if (wysiwygEditor.document) return wysiwygEditor.document; return null; }; /** *Inserts HTML into WYSIWYG editor.
* *If endHtml is specified instead of the inserted HTML replacing the selected * text the selected text will be placed between html and endHtml. If there is * no selected text html and endHtml will be concated together.
* * @param {string} html * @param {string} [endHtml=null] * @param {boolean} [overrideCodeBlocking=false] * @function * @name wysiwygEditorInsertHtml * @memberOf jQuery.sceditor.prototype */ base.wysiwygEditorInsertHtml = function (html, endHtml, overrideCodeBlocking) { base.focus(); // don't apply to code elements if(!overrideCodeBlocking && ($(rangeHelper.parentNode()).is('code') || $(rangeHelper.parentNode()).parents('code').length !== 0)) return; rangeHelper.insertHTML(html, endHtml); }; /** * Like wysiwygEditorInsertHtml except it will convert any HTML into text * before inserting it. * * @param {String} text * @param {String} [endText=null] * @function * @name wysiwygEditorInsertText * @memberOf jQuery.sceditor.prototype */ base.wysiwygEditorInsertText = function (text, endText) { var escape = function(str) { if(!str) return str; return str.replace(/&/g, "&") .replace(//g, ">") .replace(/ /g, " ") .replace(/\r\n|\r/g, "\n") .replace(/\n/g, "Inserts text into either WYSIWYG or textEditor depending on which * mode the editor is in.
* *If endText is specified any selected text will be placed between * text and endText. If no text is selected text and endText will * just be concated together.
* * @param {String} text * @param {String} [endText=null] * @since 1.3.5 * @function * @name insertText * @memberOf jQuery.sceditor.prototype */ base.insertText = function (text, endText) { if(base.inSourceMode()) base.textEditorInsertText(text, endText); else base.wysiwygEditorInsertText(text, endText); return this; }; /** * Like wysiwygEditorInsertHtml but inserts text into the text * (source mode) editor instead * * @param {String} text * @param {String} [endText=null] * @function * @name textEditorInsertText * @memberOf jQuery.sceditor.prototype */ base.textEditorInsertText = function (text, endText) { var range, start, end, txtLen; textEditor.focus(); if(typeof textEditor.selectionStart !== "undefined") { start = textEditor.selectionStart; end = textEditor.selectionEnd; txtLen = text.length; if(endText) text += textEditor.value.substring(start, end) + endText; textEditor.value = textEditor.value.substring(0, start) + text + textEditor.value.substring(end, textEditor.value.length); if(endText) textEditor.selectionStart = (start + text.length) - endText.length; else textEditor.selectionStart = start + text.length; textEditor.selectionEnd = textEditor.selectionStart; } else if(typeof document.selection.createRange !== "undefined") { range = document.selection.createRange(); if(endText) text += range.text + endText; range.text = text; if(endText) range.moveEnd('character', 0-endText.length); range.moveStart('character', range.End - range.Start); range.select(); } else textEditor.value += text + endText; textEditor.focus(); }; /** * Gets the current rangeHelper instance * * @return jQuery.sceditor.rangeHelper * @function * @name getRangeHelper * @memberOf jQuery.sceditor.prototype */ base.getRangeHelper = function () { return rangeHelper; }; /** * Gets the value of the editor * * @since 1.3.5 * @return {string} * @function * @name val * @memberOf jQuery.sceditor.prototype */ /** * Sets the value of the editor * * @param {String} val * @param {Boolean} [filter] * @return {this} * @since 1.3.5 * @function * @name val^2 * @memberOf jQuery.sceditor.prototype */ base.val = function (val, filter) { if(typeof val === "string") { if(base.inSourceMode()) base.setTextareaValue(val); else { if(filter !== false && base.options.getTextHandler) val = base.options.getTextHandler(val); base.setWysiwygEditorValue(val); } return this; } return base.inSourceMode() ? base.getTextareaValue(false) : base.getWysiwygEditorValue(); }; /** *Inserts HTML/BBCode into the editor
* *If end is supplied any slected text will be placed between * start and end. If there is no selected text start and end * will be concated together.
* * @param {String} start * @param {String} [end=null] * @param {Boolean} [filter=true] * @param {Boolean} [convertEmoticons=true] * @return {this} * @since 1.3.5 * @function * @name insert * @memberOf jQuery.sceditor.prototype */ base.insert = function (start, end, filter, convertEmoticons) { if(base.inSourceMode()) base.textEditorInsertText(start, end); else { if(end) { var html = base.getRangeHelper().selectedHtml(), frag = $('' + ($.sceditor.ie ? '' : '
') + '
' + ($.sceditor.ie ? '' : ' ') + ' | ';
html += '
', '
');
},
tooltip: "Code"
},
// END_COMMAND
// START_COMMAND: Image
image: {
exec: function (caller) {
var editor = this,
content = _tmpl("image", {
url: editor._("URL:"),
width: editor._("Width (optional):"),
height: editor._("Height (optional):"),
insert: editor._("Insert")
}, true);
content.find('.button').click(function (e) {
var val = content.find("#image").val(),
attrs = '',
width, height;
if((width = content.find("#width").val()))
attrs += ' width="' + width + '"';
if((height = content.find("#height").val()))
attrs += ' height="' + height + '"';
if(val && val !== "http://")
editor.wysiwygEditorInsertHtml('');
editor.closeDropDown(true);
e.preventDefault();
});
editor.createDropDown(caller, "insertimage", content);
},
tooltip: "Insert an image"
},
// END_COMMAND
// START_COMMAND: E-mail
email: {
exec: function (caller) {
var editor = this,
content = _tmpl("email", {
label: editor._("E-mail:"),
insert: editor._("Insert")
}, true);
content.find('.button').click(function (e) {
var val = content.find("#email").val();
if(val)
{
// needed for IE to reset the last range
editor.focus();
if(!editor.getRangeHelper().selectedHtml())
editor.wysiwygEditorInsertHtml('' + val + '');
else
editor.execCommand("createlink", 'mailto:' + val);
}
editor.closeDropDown(true);
e.preventDefault();
});
editor.createDropDown(caller, "insertemail", content);
},
tooltip: "Insert an email"
},
// END_COMMAND
// START_COMMAND: Link
link: {
exec: function (caller) {
var editor = this,
content = _tmpl("link", {
url: editor._("URL:"),
desc: editor._("Description (optional):"),
ins: editor._("Insert")
}, true);
content.find('.button').click(function (e) {
var val = content.find("#link").val(),
description = content.find("#des").val();
if(val !== "" && val !== "http://") {
// needed for IE to reset the last range
editor.focus();
if(!editor.getRangeHelper().selectedHtml() || description)
{
if(!description)
description = val;
editor.wysiwygEditorInsertHtml('' + description + '');
}
else
editor.execCommand("createlink", val);
}
editor.closeDropDown(true);
e.preventDefault();
});
editor.createDropDown(caller, "insertlink", content);
},
tooltip: "Insert a link"
},
// END_COMMAND
// START_COMMAND: Unlink
unlink: {
exec: "unlink",
tooltip: "Unlink"
},
// END_COMMAND
// START_COMMAND: Quote
quote: {
exec: function (caller, html, author) {
var before = '', end = ''; // if there is HTML passed set end to null so any selected // text is replaced if(html) { author = (author ? '' + author + '' : ''); before = before + author + html + end + '
Inserts HTML into the current range replacing any selected * text.
* *If endHTML is specified the selected contents will be put between * html and endHTML. If there is nothing selected html and endHTML are * just concated together.
* * @param {string} html * @param {string} endHTML * @function * @name insertHTML * @memberOf jQuery.sceditor.rangeHelper.prototype */ base.insertHTML = function(html, endHTML) { var node, div; if(endHTML) html += base.selectedHtml() + endHTML; if(isW3C) { div = doc.createElement('div'); node = doc.createDocumentFragment(); div.innerHTML = html; while(div.firstChild) node.appendChild(div.firstChild); base.insertNode(node); } else base.selectedRange().pasteHTML(html); }; /** *The same as insertHTML except with DOM nodes instead
* *Warning: the nodes must belong to the * document they are being inserted into. Some browsers * will throw exceptions if they don't.
* * @param {Node} node * @param {Node} endNode * * @function * @name insertNode * @memberOf jQuery.sceditor.rangeHelper.prototype */ base.insertNode = function(node, endNode) { if(isW3C) { var toInsert = doc.createDocumentFragment(), range = base.selectedRange(), selection, selectAfter; toInsert.appendChild(node); if(endNode) { toInsert.appendChild(range.extractContents()); toInsert.appendChild(endNode); } selectAfter = toInsert.lastChild; range.deleteContents(); range.insertNode(toInsert); selection = doc.createRange(); selection.setStartAfter(selectAfter); base.selectRange(selection); } else base.insertHTML(node.outerHTML, endNode?endNode.outerHTML:null); }; /** *Clones the selected Range
* *IE <= 8 will return a TextRange, all other browsers * will return a Range object.
* * @return {Range|TextRange} * @function * @name cloneSelected * @memberOf jQuery.sceditor.rangeHelper.prototype */ base.cloneSelected = function() { if(!isW3C) return base.selectedRange().duplicate(); return base.selectedRange().cloneRange(); }; /** *Gets the selected Range
* *IE <= 8 will return a TextRange, all other browsers * will return a Range object.
* * @return {Range|TextRange} * @function * @name selectedRange * @memberOf jQuery.sceditor.rangeHelper.prototype */ base.selectedRange = function() { var sel; if(win.getSelection) sel = win.getSelection(); else sel = doc.selection; if(sel.getRangeAt && sel.rangeCount <= 0) sel.addRange(doc.createRange()); if(!isW3C) return sel.createRange(); return sel.getRangeAt(0); }; /** * Gets the currently selected HTML * * @return {string} * @function * @name selectedHtml * @memberOf jQuery.sceditor.rangeHelper.prototype */ base.selectedHtml = function() { var range = base.selectedRange(); if(!range) return ''; // IE9+ and all other browsers if (window.XMLSerializer) return new XMLSerializer().serializeToString(range.cloneContents()); // IE < 9 if(!isW3C) { if(range.text !== '' && range.htmlText) return range.htmlText; } return ''; }; /** * Gets the parent node of the selected contents in the range * * @return {HTMLElement} * @function * @name parentNode * @memberOf jQuery.sceditor.rangeHelper.prototype */ base.parentNode = function() { var range = base.selectedRange(); if(isW3C) return range.commonAncestorContainer; else return range.parentElement(); }; /** * Gets the first block level parent of the selected * contents of the range. * * @return {HTMLElement} * @function * @name getFirstBlockParent * @memberOf jQuery.sceditor.rangeHelper.prototype */ base.getFirstBlockParent = function() { var func = function(node) { if(!$.sceditor.dom.isInline(node)) return node; var p = node.parentNode; if(p) return func(p); return null; }; return func(base.parentNode()); }; /** * Inserts a node at either the start or end of the current selection * * @param {Bool} start * @param {Node} node * @function * @name insertNodeAt * @memberOf jQuery.sceditor.rangeHelper.prototype */ base.insertNodeAt = function(start, node) { var range = base.cloneSelected(); range.collapse(start); if(range.insertNode) range.insertNode(node); else range.pasteHTML(node.outerHTML); }; /** * Creates a marker node * * @param {String} id * @return {Node} * @private */ _createMarker = function(id) { base.removeMarker(id); var marker = doc.createElement("span"); marker.id = id; marker.style.lineHeight = "0"; marker.style.display = "none"; marker.className = "sceditor-selection"; return marker; }; /** * Inserts start/end markers for the current selection * which can be used by restoreRange to re-select the * range. * * @memberOf jQuery.sceditor.rangeHelper.prototype * @function * @name insertMarkers */ base.insertMarkers = function() { base.insertNodeAt(true, _createMarker(startMarker)); base.insertNodeAt(false, _createMarker(endMarker)); }; /** * Gets the marker with the specified ID * * @param {String} id * @return {Node} * @function * @name getMarker * @memberOf jQuery.sceditor.rangeHelper.prototype */ base.getMarker = function(id) { return doc.getElementById(id); }; /** * Removes the marker with the specified ID * * @param {String} id * @function * @name removeMarker * @memberOf jQuery.sceditor.rangeHelper.prototype */ base.removeMarker = function(id) { var marker = base.getMarker(id); if(marker) marker.parentNode.removeChild(marker); }; /** * Removes the start/end markers * * @function * @name removeMarkers * @memberOf jQuery.sceditor.rangeHelper.prototype */ base.removeMarkers = function() { base.removeMarker(startMarker); base.removeMarker(endMarker); }; /** * Saves the current range location. Alias of insertMarkers() * * @function * @name saveRage * @memberOf jQuery.sceditor.rangeHelper.prototype */ base.saveRange = function() { base.insertMarkers(); }; /** * Select the specified range * * @param {Range|TextRange} range * @function * @name selectRange * @memberOf jQuery.sceditor.rangeHelper.prototype */ base.selectRange = function(range) { if(!isW3C) range.select(); else { win.getSelection().removeAllRanges(); win.getSelection().addRange(range); } }; /** * Restores the last range saved by saveRange() or insertMarkers() * * @function * @name restoreRange * @memberOf jQuery.sceditor.rangeHelper.prototype */ base.restoreRange = function() { var range = base.selectedRange(), start = base.getMarker(startMarker), end = base.getMarker(endMarker); if(!start || !end) return false; if(!isW3C) { range = doc.body.createTextRange(); var marker = doc.body.createTextRange(); marker.moveToElementText(start); range.setEndPoint('StartToStart', marker); range.moveStart('character', 0); marker.moveToElementText(end); range.setEndPoint('EndToStart', marker); range.moveEnd('character', 0); base.selectRange(range); } else { range = doc.createRange(); range.setStartBefore(start); range.setEndAfter(end); base.selectRange(range); } base.removeMarkers(); }; /** * Selects the text left and right of the current selection * @param int left * @param int right * @private */ _selectOuterText = function(left, right) { var range = base.cloneSelected(); range.collapse(false); if(!isW3C) { range.moveStart('character', 0-left); range.moveEnd('character', right); } else { range.setStart(range.startContainer, range.startOffset-left); range.setEnd(range.endContainer, range.endOffset+right); //range.deleteContents(); } base.selectRange(range); }; /** * Gets the text left or right of the current selection * @param bool before * @param int length * @private */ _getOuterText = function(before, length) { var ret = "", range = base.cloneSelected(); range.collapse(false); if(before) { if(!isW3C) { range.moveStart('character', 0-length); ret = range.text; } else { ret = range.startContainer.textContent.substr(0, range.startOffset); ret = ret.substr(Math.max(0, ret.length - length)); } } else { if(!isW3C) { range.moveEnd('character', length); ret = range.text; } else ret = range.startContainer.textContent.substr(range.startOffset, length); } return ret; }; /** * Replaces keys with values based on the current range * * @param {Array} rep * @param {Bool} includePrev If to include text before or just text after * @param {Bool} repSorted If the keys array is pre sorted * @param {Int} longestKey Length of the longest key * @param {Bool} requireWhiteSpace If the key must be surrounded by whitespace * @function * @name raplaceKeyword * @memberOf jQuery.sceditor.rangeHelper.prototype */ base.raplaceKeyword = function(rep, includeAfter, repSorted, longestKey, requireWhiteSpace, curChar) { if(!repSorted) rep.sort(function(a, b){ return a.length - b.length; }); var maxKeyLen = longestKey || rep[rep.length-1][0].length, before, after, str, i, start, left, pat, lookStart; before = after = str = ""; if(requireWhiteSpace) { // forcing spaces around doesn't work with textRanges as they will select text // on the other side of an image causing space-img-key to be returned as // space-key which would be valid when it's not. if(!isW3C) return false; ++maxKeyLen; } before = _getOuterText(true, maxKeyLen); if(includeAfter) after = _getOuterText(false, maxKeyLen); str = before + (curChar!=null?curChar:"") + after; i = rep.length; while(i--) { pat = new RegExp("(?:[\\s\xA0\u2002\u2003\u2009])" + $.sceditor.regexEscape(rep[i][0]) + "(?=[\\s\xA0\u2002\u2003\u2009])"); lookStart = before.length - 1 - rep[i][0].length; if(requireWhiteSpace) --lookStart; lookStart = Math.max(0, lookStart); if((!requireWhiteSpace && (start = str.indexOf(rep[i][0], lookStart)) > -1) || (requireWhiteSpace && (start = str.substr(lookStart).search(pat)) > -1)) { if(requireWhiteSpace) start += lookStart + 1; // make sure the substr is between before and after not entierly in one // or the other if(start > before.length || start+rep[i][0].length + (requireWhiteSpace?1:0) < before.length) continue; left = before.length - start; _selectOuterText(left, rep[i][0].length-left-(curChar!=null&&/^\S/.test(curChar)?1:0)); base.insertHTML(rep[i][1]); return true; } } return false; }; }; /** * Static DOM helper class * @class dom * @name jQuery.sceditor.dom */ $.sceditor.dom = /** @lends jQuery.sceditor.dom */ { /** * Loop all child nodes of the passed node * * The function should accept 1 parameter being the node. * If the function returns false the loop will be exited. * * @param {HTMLElement} node * @param {function} func Function that is called for every node, should accept 1 param for the node * @param {bool} innermostFirst If the innermost node should be passed to the function before it's parents * @param {bool} siblingsOnly If to only traverse the nodes siblings * @param {bool} reverse If to traverse the nodes in reverse */ traverse: function(node, func, innermostFirst, siblingsOnly, reverse) { if(node) { node = reverse ? node.lastChild : node.firstChild; while(node != null) { var next = reverse ? node.previousSibling : node.nextSibling; if(!innermostFirst && func(node) === false) return false; // traverse all children if(!siblingsOnly && this.traverse(node, func, innermostFirst, siblingsOnly, reverse) === false) return false; if(innermostFirst && func(node) === false) return false; // move to next child node = next; } } }, /** * Like traverse but loops in reverse * @see traverse */ rTraverse: function(node, func, innermostFirst, siblingsOnly) { this.traverse(node, func, innermostFirst, siblingsOnly, true); }, /** * List of block level elements seperated by bars (|) * @type {string} */ blockLevelList: "|body|hr|p|div|h1|h2|h3|h4|h5|h6|address|pre|form|table|tbody|thead|tfoot|th|tr|td|li|ol|ul|blockquote|center|", /** * Checks if an element is inline * * @return {bool} */ isInline: function(elm, includeCodeAsBlock) { if(!elm || elm.nodeType !== 1) return true; if(includeCodeAsBlock && elm.tagName.toLowerCase() === 'code') return false; return $.sceditor.dom.blockLevelList.indexOf("|" + elm.tagName.toLowerCase() + "|") < 0; }, /** * Copys the CSS from 1 node to another * * @param {HTMLElement} from * @param {HTMLElement} to */ copyCSS: function(from, to) { to.style.cssText = from.style.cssText; }, /** * Fixes block level elements inside in inline elements. * * @param {HTMLElement} node */ fixNesting: function(node) { var base = this, getLastInlineParent = function(node) { while(base.isInline(node.parentNode, true)) node = node.parentNode; return node; }; base.traverse(node, function(node) { // if node is an element, and it is blocklevel and the parent isn't block level // then it needs fixing if(node.nodeType === 1 && !base.isInline(node, true) && base.isInline(node.parentNode, true)) { var parent = getLastInlineParent(node), rParent = parent.parentNode, before = base.extractContents(parent, node), middle = node; // copy current styling so when moved out of the parent // it still has the same styling base.copyCSS(middle, middle); rParent.insertBefore(before, parent); rParent.insertBefore(middle, parent); } }); }, /** * Finds the common parent of two nodes * * @param {HTMLElement} node1 * @param {HTMLElement} node2 * @return {HTMLElement} */ findCommonAncestor: function(node1, node2) { // not as fast as making two arrays of parents and comparing // but is a lot smaller and as it's currently only used with // fixing invalid nesting it doesn't need to be very fast return $(node1).parents().has($(node2)).first(); }, /** * Removes unused whitespace from the root and all it's children * * @param HTMLElement root * @return void */ removeWhiteSpace: function(root) { // 00A0 is non-breaking space which should not be striped var regex = /[^\S|\u00A0]+/g; this.traverse(root, function(node) { if(node.nodeType === 3 && $(node).parents('code, pre').length === 0 && node.nodeValue) { // new lines in text nodes are always ignored in normal handling node.nodeValue = node.nodeValue.replace(/[\r\n]/, ""); //remove empty nodes if(!node.nodeValue.length) { node.parentNode.removeChild(node); return; } if(!/\S|\u00A0/.test(node.nodeValue)) node.nodeValue = " "; else if(regex.test(node.nodeValue)) node.nodeValue = node.nodeValue.replace(regex, " "); } }); }, /** * Extracts all the nodes between the start and end nodes * * @param {HTMLElement} startNode The node to start extracting at * @param {HTMLElement} endNode The node to stop extracting at * @return {DocumentFragment} */ extractContents: function(startNode, endNode) { var base = this, $commonAncestor = base.findCommonAncestor(startNode, endNode), commonAncestor = !$commonAncestor?null:$commonAncestor.get(0), startReached = false, endReached = false; return (function extract(root) { var df = startNode.ownerDocument.createDocumentFragment(); base.traverse(root, function(node) { // if end has been reached exit loop if(endReached || (node === endNode && startReached)) { endReached = true; return false; } if(node === startNode) startReached = true; var c, n; if(startReached) { // if the start has been reached and this elm contains // the end node then clone it if(jQuery.contains(node, endNode) && node.nodeType === 1) { c = extract(node); n = node.cloneNode(false); n.appendChild(c); df.appendChild(n); } // otherwise just move it else df.appendChild(node); } // if this node contains the start node then add it else if(jQuery.contains(node, startNode) && node.nodeType === 1) { c = extract(node); n = node.cloneNode(false); n.appendChild(c); df.appendChild(n); } }, false); return df; }(commonAncestor)); } }; /** * Static command helper class * @class command * @name jQuery.sceditor.command */ $.sceditor.command = /** @lends jQuery.sceditor.command */ { /** * Gets a command * * @param {String} name * @return {Object|null} * @since v1.3.5 */ get: function(name) { return $.sceditor.commands[name] || null; }, /** *Adds a command to the editor or updates an exisiting * command if a command with the specified name already exists.
* *Once a command is add it can be included in the toolbar by * adding it's name to the toolbar option in the constructor. It * can also be executed manually by calling {@link jQuery.sceditor.execCommand}
* * @example * $.sceditor.command.set("hello", * { * exec: function() { * alert("Hello World!"); * } * }); * * @param {String} name * @param {Object} cmd * @return {this|false} Returns false if name or cmd is false * @since v1.3.5 */ set: function(name, cmd) { if(!name || !cmd) return false; // merge any existing command properties cmd = $.extend($.sceditor.commands[name] || {}, cmd); cmd.remove = function() { $.sceditor.command.remove(name); }; $.sceditor.commands[name] = cmd; return this; }, /** * Removes a command * * @param {String} name * @return {this} * @since v1.3.5 */ remove: function(name) { if($.sceditor.commands[name]) delete $.sceditor.commands[name]; return this; } }; /** * Checks if a command with the specified name exists * * @param {String} name * @return {Bool} * @deprecated Since v1.3.5 * @memberOf jQuery.sceditor */ $.sceditor.commandExists = function(name) { return !!$.sceditor.command.get(name); }; /** * Adds/updates a command. * * Only name and exec are required. Exec is only required if * the command dose not already exist. * * @param {String} name The commands name * @param {String|Function} exec The commands exec function or string for the native execCommand * @param {String} tooltip The commands tooltip text * @param {Function} keypress Function that gets called every time a key is pressed * @param {Function|Array} txtExec Called when the command is executed in source mode or array containing prepend and optional append * @return {Bool} * @deprecated Since v1.3.5 * @memberOf jQuery.sceditor */ $.sceditor.setCommand = function(name, exec, tooltip, keypress, txtExec) { return !!$.sceditor.command.set(name, { exec: exec, tooltip: tooltip, keypress: keypress, txtExec: txtExec }); }; $.sceditor.defaultOptions = { // Toolbar buttons order and groups. Should be comma seperated and have a bar | to seperate groups toolbar: "bold,italic,underline,strike,subscript,superscript|left,center,right,justify|" + "font,size,color,removeformat|cut,copy,paste,pastetext|bulletlist,orderedlist|" + "table|code,quote|horizontalrule,image,email,link,unlink|emoticon,youtube,date,time|" + "ltr,rtl|print,source", // Stylesheet to include in the WYSIWYG editor. Will style the WYSIWYG elements style: "jquery.sceditor.default.css", // Comma seperated list of fonts for the font selector fonts: "Arial,Arial Black,Comic Sans MS,Courier New,Georgia,Impact,Sans-serif,Serif,Times New Roman,Trebuchet MS,Verdana", // Colors should be comma seperated and have a bar | to signal a new column. If null the colors will be auto generated. colors: null, locale: "en", charset: "utf-8", // compatibility mode for if you have emoticons such as :/ This mode requires // emoticons to be surrounded by whitespace or end of line chars. This mode // has limited As You Type emoticon converstion support (end of line chars) // are not accepted as whitespace so only emoticons surrounded by whitespace // will work emoticonsCompat: false, emoticonsRoot: '', emoticons: { dropdown: { ":)": "emoticons/smile.png", ":angel:": "emoticons/angel.png", ":angry:": "emoticons/angry.png", "8-)": "emoticons/cool.png", ":'(": "emoticons/cwy.png", ":ermm:": "emoticons/ermm.png", ":D": "emoticons/grin.png", "<3": "emoticons/heart.png", ":(": "emoticons/sad.png", ":O": "emoticons/shocked.png", ":P": "emoticons/tongue.png", ";)": "emoticons/wink.png" }, more: { ":alien:": "emoticons/alien.png", ":blink:": "emoticons/blink.png", ":blush:": "emoticons/blush.png", ":cheerful:": "emoticons/cheerful.png", ":devil:": "emoticons/devil.png", ":dizzy:": "emoticons/dizzy.png", ":getlost:": "emoticons/getlost.png", ":happy:": "emoticons/happy.png", ":kissing:": "emoticons/kissing.png", ":ninja:": "emoticons/ninja.png", ":pinch:": "emoticons/pinch.png", ":pouty:": "emoticons/pouty.png", ":sick:": "emoticons/sick.png", ":sideways:": "emoticons/sideways.png", ":silly:": "emoticons/silly.png", ":sleeping:": "emoticons/sleeping.png", ":unsure:": "emoticons/unsure.png", ":woot:": "emoticons/w00t.png", ":wassat:": "emoticons/wassat.png" }, hidden: { ":whistling:": "emoticons/whistling.png", ":love:": "emoticons/wub.png" } }, // Width of the editor. Set to null for automatic with width: null, // Height of the editor including toolbat. Set to null for automatic height height: null, // If to allow the editor to be resized resizeEnabled: true, // Min resize to width, set to null for half textarea width or -1 for unlimited resizeMinWidth: null, // Min resize to height, set to null for half textarea height or -1 for unlimited resizeMinHeight: null, // Max resize to height, set to null for double textarea height or -1 for unlimited resizeMaxHeight: null, // Max resize to width, set to null for double textarea width or -1 for unlimited resizeMaxWidth: null, getHtmlHandler: null, getTextHandler: null, // date format. year, month and day will be replaced with the users current year, month and day. dateFormat: "year-month-day", toolbarContainer: null, // Curently experimental enablePasteFiltering: false, readOnly: false, rtl: false, autofocus: false, autoExpand: false, // If to run the editor without WYSIWYG support runWithoutWysiwygSupport: false, id: null, //add css to dropdown menu (eg. z-index) dropDownCss: { } }; $.fn.sceditor = function (options) { if((!options || !options.runWithoutWysiwygSupport) && !$.sceditor.isWysiwygSupported()) return; return this.each(function () { (new $.sceditor(this, options)); }); }; })(jQuery, window, document); (function($) { var extensionMethods = { InsertText: function(text, bClear) { var bIsSource = this.inSourceMode(); // @TODO make it put the quote close to the current selection if (!bIsSource) this.toggleTextMode(); var current_value = bClear ? text + "\n" : this.getTextareaValue(false) + "\n" + text + "\n"; this.setTextareaValue(current_value); if (!bIsSource) this.toggleTextMode(); }, getText: function(filter) { var current_value = ''; if(this.inSourceMode()) current_value = this.getTextareaValue(false); else current_value = this.getWysiwygEditorValue(filter); return current_value; }, appendEmoticon: function (code, emoticon) { if (code == '') line.append($('', ''); }, 'Pre' );