/** * 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: '
{dispName}
', emoticon: '{key}', fontOpt: '{font}', sizeOpt: '{size}', pastetext: '
' + '
' + '
', table: '
' + '
' + '
', image: '
' + '
' + '
' + '
', email: '
' + '
', link: '
' + '
' + '
', youtubeMenu: '
', youtube: '' }; /** *

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()); 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 = $('').attr('tabindex', $textarea.attr('tabindex')).hide(); $wysiwygEditor = $('').attr('tabindex', $textarea.attr('tabindex')); 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) || $.sceditor.ie) $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 (url == '') 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 = $('