orkui/template/default/script/js/SCEditor/jquery.sceditor.js
/**
* SCEditor v1.3.3
* http://www.samclarke.com/2011/07/sceditor/
*
* Copyright (C) 2011-2012, Sam Clarke (samclarke.com)
*
* SCEditor is dual licensed under the MIT and GPL licenses:
* http://www.opensource.org/licenses/mit-license.php
* http://www.gnu.org/licenses/gpl.html
*/
// ==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 ($) {
'use strict';
$.sceditor = function (el, options) {
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 = null;
/**
* The editors toolbar
* @private
*/
var $toolbar = null;
/**
* The editors iframe which should be in design mode
* @private
*/
var $wysiwygEditor = null;
var wysiwygEditor = null;
/**
* The editors textarea for viewing source
* @private
*/
var $textEditor = null;
var textEditor = null;
/**
* The current dropdown
* @private
*/
var $dropdown = null;
var dropdownIgnoreLastClick = false;
/**
* 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 = null;
/**
* The editors locale
* @private
*/
var locale = null;
/**
* Stores a cache of preloaded images
* @private
*/
var preLoadCache = [];
var rangeHelper = null;
var init,
replaceEmoticons,
handleCommand,
saveRange,
handlePasteEvt,
handlePasteData,
handleKeyPress,
handleKeyUp,
handleMouseDown,
initEditor,
initToolBar,
initKeyPressFuncs,
initResize,
documentClickHandler,
formSubmitHandler,
preLoadEmoticons,
getWysiwygDoc,
handleWindowResize,
setHeight,
setWidth,
initLocale,
isTextMode;
/**
* All the commands supported by the editor
*/
base.commands = $.extend({}, (options.commands || $.sceditor.commands));
/**
* Initializer. Creates the editor iframe and textarea
* @private
*/
init = function () {
$textarea.data("sceditor", base);
base.options = $.extend({}, $.sceditor.defaultOptions, options);
// Load locale
if(base.options.locale !== null && base.options.locale !== "en")
initLocale();
if(base.options.height !== null)
$textarea.height(base.options.height);
if(base.options.width !== null)
$textarea.width(base.options.width);
// if either width or height are % based, add the resize handler to update the editor
// when the window is resized
if((base.options.height !== null && base.options.height.toString().indexOf("%") > -1) ||
(base.options.width !== null && base.options.width.toString().indexOf("%") > -1))
$(window).resize(handleWindowResize);
editorContainer = $('<div class="sceditor-container" />')
.width($textarea.outerWidth())
.height($textarea.outerHeight());
$textarea.after(editorContainer);
// create the editor
initToolBar();
initEditor();
initKeyPressFuncs();
if(base.options.resizeEnabled)
initResize();
$(document).click(documentClickHandler);
(textarea.form ? $(textarea.form) : $textarea.parents("form"))
.attr('novalidate','novalidate')
.submit(formSubmitHandler);
// 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;
});
});
}
// load any textarea value into the editor
var 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)
val = base.options.getTextHandler(val);
base.setWysiwygEditorValue(val);
if(base.options.toolbar.indexOf('emoticon') !== -1)
preLoadEmoticons();
};
/**
* Creates the editor iframe and textarea
* @private
*/
initEditor = function () {
var contentEditable = $('<div contenteditable="true">')[0].contentEditable,
contentEditableSupported = typeof contentEditable !== 'undefined' && contentEditable !== 'inherit',
$doc, $body;
$textEditor = $('<textarea></textarea>').hide();
$wysiwygEditor = $('<iframe frameborder="0"></iframe>');
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];
setWidth($textarea.width());
setHeight($textarea.height());
// turn on design mode if contenteditable not supported
if(!contentEditableSupported)
getWysiwygDoc().designMode = 'On';
getWysiwygDoc().open();
getWysiwygDoc().write(
'<html><head><!--[if gte IE 9]><style>* {min-height: auto !important}</style><![endif]-->' +
'<meta http-equiv="Content-Type" content="text/html;charset=' + base.options.charset + '" />' +
'<link rel="stylesheet" type="text/css" href="' + base.options.style + '" />' +
'</head><body contenteditable="true"></body></html>'
);
getWysiwygDoc().close();
// turn on design mode if contenteditable not supported
if(!contentEditableSupported)
getWysiwygDoc().designMode = 'On';
$doc = $(getWysiwygDoc());
$body = $doc.find("body");
// set the key press event
$body.keypress(handleKeyPress);
$body.keyup(handleKeyUp);
$doc.keypress(handleKeyPress);
$doc.keyup(handleKeyUp);
$doc.mousedown(handleMouseDown);
$doc.bind("beforedeactivate keyup", saveRange);
$doc.focus(function() {
lastRange = null;
});
if(base.options.enablePasteFiltering)
$body.bind("paste", handlePasteEvt);
rangeHelper = new $.sceditor.rangeHelper(wysiwygEditor.contentWindow);
};
/**
* Creates the toolbar and appends it to the container
* @private
*/
initToolBar = function () {
var buttonClick = function (e) {
handleCommand($(this), base.commands[$(this).data("sceditor-command")]);
e.preventDefault();
};
$toolbar = $('<div class="sceditor-toolbar" />');
var groups = base.options.toolbar.split("|"),
group, buttons, accessibilityName, button, i;
for (i=0; i < groups.length; i++) {
group = $('<div class="sceditor-group" />');
buttons = groups[i].split(",");
for (var x=0; x < buttons.length; x++) {
// the button must be a valid command otherwise ignore it
if(!base.commands.hasOwnProperty(buttons[x]))
continue;
accessibilityName = base.commands[buttons[x]].tooltip ? base._(base.commands[buttons[x]].tooltip) : buttons[x];
button = $('<a class="sceditor-button sceditor-button-' + buttons[x] +
' " unselectable="on"><div unselectable="on">' + accessibilityName + '</div></a>');
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);
// add the click handler for the button
button.data("sceditor-command", buttons[x]);
button.click(buttonClick);
group.append(button);
}
$toolbar.append(group);
}
// append the toolbar to the toolbarContainer option if given
if(base.options.toolbarContainer === null)
editorContainer.append($toolbar);
else
$(base.options.toolbarContainer).append($toolbar);
};
/**
* Creates an array of all the key press functions
* like emoticons, ect.
* @private
*/
initKeyPressFuncs = function () {
$.each(base.commands, function (command, values) {
if(typeof values.keyPress !== "undefined")
keyPressFuncs.push(values.keyPress);
});
};
setWidth = function (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)));
};
setHeight = function (height) {
editorContainer.height(height);
height = height - (base.options.toolbarContainer === null?$toolbar.outerHeight():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)));
};
/**
* Creates the resizer.
* @private
*/
initResize = function () {
var $grip = $('<div class="sceditor-grip" />'),
// cover is used to cover the editor iframe so document still gets mouse move events
$cover = $('<div class="sceditor-resize-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 == null ?
origHeight / 2 :
base.options.resizeMinHeight);
maxHeight = (base.options.resizeMaxHeight == null ?
origHeight * 2 :
base.options.resizeMaxHeight);
minWidth = (base.options.resizeMinWidth == null ?
origWidth / 2 :
base.options.resizeMinWidth);
maxWidth = (base.options.resizeMaxWidth == null ?
origWidth * 2 :
base.options.resizeMaxWidth);
mouseMoveFunc = function (e) {
var newHeight = startHeight + (e.pageY - startY),
newWidth = startWidth + (e.pageX - startX);
if (newWidth >= minWidth && (maxWidth < 0 || newWidth <= maxWidth))
setWidth(newWidth);
if (newHeight >= minHeight && (maxHeight < 0 || newHeight <= maxHeight))
setHeight(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;
$cover.show();
$(document).bind('mousemove', mouseMoveFunc);
e.preventDefault();
});
$(document).mouseup(function (e) {
if(!dragging)
return;
dragging = false;
$cover.hide();
$(document).unbind('mousemove', mouseMoveFunc);
e.preventDefault();
});
};
formSubmitHandler = function(e) {
base.updateTextareaValue();
$(this).removeAttr('novalidate');
if(this.checkValidity && !this.checkValidity())
e.preventDefault();
$(this).attr('novalidate','novalidate');
};
/**
* Destroys the editor, removing all elements and
* event handlers.
*/
base.destory = function () {
$(document).unbind('click', documentClickHandler);
$textarea.removeAttr('novalidate').unbind('submit', formSubmitHandler);
$(window).unbind('resize', handleWindowResize);
editorContainer.remove();
editorContainer = null;
$textarea.removeData("sceditor").show();
};
/**
* Preloads the emoticon images
* Idea from: http://engineeredweb.com/blog/09/12/preloading-images-jquery-and-javascript
* @private
*/
preLoadEmoticons = function () {
var emoticons = $.extend({}, base.options.emoticons.more, base.options.emoticons.dropdown, base.options.emoticons.hidden),
emoticon;
$.each(emoticons, function (key, url) {
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
*/
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 = $(content);
content.find(':not(input,textarea)').filter(function() { return this.nodeType===1; }).attr('unselectable', 'on');
}
var o_css = {
top: menuItem.offset().top,
left: menuItem.offset().left
};
$.extend(o_css, base.options.dropDownCss);
$dropdown = $('<div class="sceditor-dropdown sceditor-' + dropDownName + '" />').css(o_css).append(content);
//editorContainer.after($dropdown);
$dropdown.appendTo($('body'));
dropdownIgnoreLastClick = true;
// stop clicks within the dropdown from being handled
$dropdown.click(function (e) {
e.stopPropagation();
});
};
/**
* Handles any document click and closes the dropdown if open
* @private
*/
documentClickHandler = function (e) {
// ignore right clicks
if(!dropdownIgnoreLastClick && e.which !== 3)
base.closeDropDown();
dropdownIgnoreLastClick = false;
};
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
*/
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
*/
base.closeDropDown = function (focus) {
if($dropdown !== null) {
$dropdown.remove();
$dropdown = null;
}
if(focus === true)
base.focus();
};
/**
* Gets the WYSIWYG editors document
*/
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 defined and some text is selected the
* selected text will be put inbetween html and endHtml. If endHtml isn't defined and some
* text is selected it will be replaced by the HTML
*
* The HTML can have only one root node, if it has more than one only the first will be used.
* e.g. with: <b>test</b><i>test2</i> only <b>test</b> will be inserted. To fix this you could
* do: <span><b>test</b><i>test2</i></span>
*
* @param string html The HTML to insert
* @param string endHtml If 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.
*/
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 converts any HTML to text
* @private
*/
base.wysiwygEditorInsertText = function (text) {
text = text.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/ /g, " ")
.replace(/\r\n|\r/g, "\n")
.replace(/\n/g, "<br />");
base.wysiwygEditorInsertHtml(text);
};
/**
* Like wysiwygEditorInsertHtml but works on the
* text editor instead
*
* @param {String} text
* @param {String} endText
*/
base.textEditorInsertText = function (text, endText) {
var range, start, end, txtLen;
textEditor.focus();
if(textEditor.selectionStart != null)
{
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(document.selection.createRange)
{
range = document.selection.createRange();
if(endText)
text += range.text + endText;
range.text = text;
}
else
textEditor.value += text + endText;
textEditor.focus();
};
/**
* Gets the current rangeHelper instance
*/
base.getRangeHelper = function () {
return rangeHelper;
};
/**
* Gets the WYSIWYG editors HTML which is between the body tags
*/
base.getWysiwygEditorValue = function (filter) {
var $body = $wysiwygEditor.contents().find("body"),
html;
// fix any invalid nesting
$.sceditor.dom.fixNesting($body.get(0));
html = $body.html();
if(filter !== false && base.options.getHtmlHandler)
html = base.options.getHtmlHandler(html, $body);
return html;
};
/**
* Gets the text editor value
* @param bool filter If to run the returned string through the filter or if to return the raw value. Defaults to filter.
*/
base.getTextareaValue = function (filter) {
var val = $textEditor.val();
if(filter !== false && base.options.getTextHandler)
val = base.options.getTextHandler(val);
return val;
};
/**
* Sets the WYSIWYG HTML editor value. Should only be the HTML contained within the body tags
* @param bool filter If to run the returned string through the filter or if to return the raw value. Defaults to filter.
*/
base.setWysiwygEditorValue = function (value) {
getWysiwygDoc().body.innerHTML = replaceEmoticons(value);
};
/**
* Sets the text editor value
*/
base.setTextareaValue = function (value) {
$textEditor.val(value);
};
/**
* Updates the forms textarea value
*/
base.updateTextareaValue = function () {
if(isTextMode())
$textarea.val(base.getTextareaValue(false));
else
$textarea.val(base.getWysiwygEditorValue());
};
/**
* Replaces any emoticon codes in the passed HTML with their emoticon images
* @private
*/
replaceEmoticons = function (html) {
if(base.options.toolbar.indexOf('emoticon') === -1)
return html;
var emoticons = $.extend({}, base.options.emoticons.more, base.options.emoticons.dropdown, base.options.emoticons.hidden);
$.each(emoticons, function (key, url) {
// escape the key before using it as a regex
// and append the regex to only find emoticons outside
// of HTML tags
var reg = $.sceditor.regexEscape(key) + "(?=([^\\<\\>]*?<(?!/code)|[^\\<\\>]*?$))",
group = '';
// Make sure the emoticon is surrounded by whitespace or is at the start/end of a string or html tag
if(base.options.emoticonsCompat)
{
reg = "((>|^|\\s|\xA0|\u2002|\u2003|\u2009| ))" + reg + "(?=(\\s|$|<|\xA0|\u2002|\u2003|\u2009| ))";
group = '$1';
}
html = html.replace(
new RegExp(reg, 'gm'),
group + '<img src="' + url + '" data-sceditor-emoticon="' + key + '" alt="' + key + '" />'
);
});
return html;
};
isTextMode = function () {
return $textEditor.is(':visible');
};
/**
* Switches between the WYSIWYG and plain text modes
*/
base.toggleTextMode = function () {
if(isTextMode())
base.setWysiwygEditorValue(base.getTextareaValue());
else
base.setTextareaValue(base.getWysiwygEditorValue());
// enable all the buttons
$toolbar.find('.sceditor-button').removeClass('disabled');
lastRange = null;
$textEditor.toggle();
$wysiwygEditor.toggle();
// diable any buttons that are not allowed for this mode
$toolbar.find('.sceditor-button').each(function () {
var button = $(this);
if(isTextMode() && !button.data('sceditor-txtmode'))
button.addClass('disabled');
else if (!isTextMode() && !button.data('sceditor-wysiwygmode'))
button.addClass('disabled');
});
};
/**
* Handles the passed command
* @private
*/
handleCommand = function (caller, command) {
// check if in text mode and handle text commands
if(isTextMode())
{
if(command.txtExec)
{
if($.isArray(command.txtExec))
base.textEditorInsertText.apply(base, command.txtExec);
else
command.txtExec.call(base, caller);
}
return;
}
if(!command.hasOwnProperty("exec"))
return;
if($.isFunction(command.exec))
command.exec.call(base, caller);
else
base.execCommand (command.exec, command.hasOwnProperty("execParam") ? command.execParam : null);
};
/**
* Fucuses the editors input area
*/
base.focus = function () {
wysiwygEditor.contentWindow.focus();
// Needed for IE < 9
if(lastRange !== null) {
rangeHelper.selectRange(lastRange);
// remove the stored range after being set.
// If the editor loses focus it should be
// saved again.
lastRange = null;
}
};
/**
* Saves the current range. Needed for IE because it forgets
* where the cursor was and what was selected
* @private
*/
saveRange = function () {
/* this is only needed for IE */
if(!$.browser.msie)
return;
lastRange = rangeHelper.selectedRange();
};
/**
* Executes a command on the WYSIWYG editor
*
* @param string|function command
* @param mixed param
*/
base.execCommand = function (command, param) {
var executed = false;
base.focus();
// don't apply any comannds to code elements
if($(rangeHelper.parentNode()).is('code') ||
$(rangeHelper.parentNode()).parents('code').length !== 0)
return;
if(getWysiwygDoc())
{
try
{
executed = getWysiwygDoc().execCommand(command, false, param);
}
catch (e){}
}
// show error if execution failed and an error message exists
if(!executed && typeof base.commands[command] !== "undefined" &&
typeof base.commands[command].errorMessage !== "undefined")
alert(base._(base.commands[command].errorMessage));
};
/**
* Handles any key press in the WYSIWYG editor
*
* @private
*/
handleKeyPress = function (e) {
base.closeDropDown();
var selectedContainer = rangeHelper.parentNode(),
$selectedContainer = $(selectedContainer);
// "Fix" (ok it's a hack) for blocklevel elements being duplicated in some browsers when
// enter is pressed instead of inserting a newline
if(e.which === 13)
{
if($selectedContainer.is('code, blockquote') || $selectedContainer.parents('code, blockquote').length !== 0)
{
lastRange = null;
base.wysiwygEditorInsertHtml('<br />', null, true);
return false;
}
}
// make sure there is always a newline after code or quote tags
var d = getWysiwygDoc();
$.sceditor.dom.rTraverse(d.body, function(node) {
if((node.nodeType === 3 && node.nodeValue !== "") ||
node.nodeName.toLowerCase() === 'br') {
// this is the last text or br node, if its in a code or quote tag
// then add a newline after it
if($(node).parents('code, blockquote').length > 0)
$(d.body).append(d.createElement('br'));
return false;
}
}, true);
// don't apply to code elements
if($selectedContainer.is('code') || $selectedContainer.parents('code').length !== 0)
return;
var i = keyPressFuncs.length;
while(i--)
keyPressFuncs[i].call(base, e, wysiwygEditor, $textEditor);
};
handleKeyUp = function (e) {
};
/**
* Handles any mousedown press in the WYSIWYG editor
* @private
*/
handleMouseDown = function (e) {
base.closeDropDown();
lastRange = null;
};
/**
* Handles the window resize event. Needed to resize then editor
* when the window size changes in fluid deisgns.
*/
handleWindowResize = function () {
if(base.options.height !== null && base.options.height.toString().indexOf("%") > -1)
setHeight(editorContainer.parent().height() *
(parseFloat(base.options.height) / 100));
if(base.options.width !== null && base.options.width.toString().indexOf("%") > -1)
setWidth(editorContainer.parent().width() *
(parseFloat(base.options.width) / 100));
};
/**
* Translates the string into the locale language.
*
* Replaces any {0}, {1}, {2}, ect. with the params provided.
* @public
* @return string
*/
base._ = function() {
var args = arguments;
if(locale !== null && locale[args[0]])
args[0] = locale[args[0]];
return args[0].replace(/\{(\d+)\}/g, function(str, p1) {
return typeof args[p1-0+1] !== 'undefined'?
args[p1-0+1] :
'{' + p1 + '}';
});
};
/**
* Init the locale variable with the specified locale if possible
* @private
* @return void
*/
initLocale = function () {
if($.sceditor.locale[base.options.locale])
locale = $.sceditor.locale[base.options.locale];
else
{
var lang = base.options.locale.split("-");
if($.sceditor.locale[lang[0]])
locale = $.sceditor.locale[lang[0]];
}
if(locale !== null && locale.dateFormat)
base.options.dateFormat = locale.dateFormat;
};
// run the initializer
init();
};
// ----------------------------------------------------------
// A short snippet for detecting versions of IE in JavaScript
// without resorting to user-agent sniffing
// ----------------------------------------------------------
// If you're not in IE (or IE version is less than 5) then:
// ie === undefined
// If you're in IE (>=5) then you can determine which version:
// ie === 7; // IE7
// Thus, to detect IE:
// if (ie) {}
// And to detect the version:
// ie === 6 // IE6
// ie > 7 // IE8, IE9 ...
// ie < 9 // Anything less than IE9
// ----------------------------------------------------------
// UPDATE: Now using Live NodeList idea from @jdalton
// Source: https://gist.github.com/527683
$.sceditor.ie = (function(){
var undef,
v = 3,
div = document.createElement('div'),
all = div.getElementsByTagName('i');
while (
div.innerHTML = '<!--[if gt IE ' + (++v) + ']><i></i><![endif]-->',
all[0]
);
return v > 4 ? v : undef;
}());
/**
* Escapes a string so it's safe to use in regex
* @param string str The strong to escape
* @return string
*/
$.sceditor.regexEscape = function (str) {
return str.replace(/[\$\?\[\]\.\*\(\)\|]/g, "\\$&")
.replace("<", "<")
.replace(">", ">");
};
$.sceditor.locale = {};
$.sceditor.commands = {
// START_COMMAND: Bold
bold: {
exec: "bold",
tooltip: "Bold"
},
// END_COMMAND
// START_COMMAND: Italic
italic: {
exec: "italic",
tooltip: "Italic"
},
// END_COMMAND
// START_COMMAND: Underline
underline: {
exec: "underline",
tooltip: "Underline"
},
// END_COMMAND
// START_COMMAND: Strikethrough
strike: {
exec: "strikethrough",
tooltip: "Strikethrough"
},
// END_COMMAND
// START_COMMAND: Subscript
subscript: {
exec: "subscript",
tooltip: "Subscript"
},
// END_COMMAND
// START_COMMAND: Superscript
superscript: {
exec: "superscript",
tooltip: "Superscript"
},
// END_COMMAND
// START_COMMAND: Left
left: {
exec: "justifyleft",
tooltip: "Align left"
},
// END_COMMAND
// START_COMMAND: Centre
center: {
exec: "justifycenter",
tooltip: "Center"
},
// END_COMMAND
// START_COMMAND: Right
right: {
exec: "justifyright",
tooltip: "Align right"
},
// END_COMMAND
// START_COMMAND: Justify
justify: {
exec: "justifyfull",
tooltip: "Justify"
},
// END_COMMAND
// START_COMMAND: Font
font: {
exec: function (caller) {
var editor = this,
fonts = editor.options.fonts.split(","),
content = $("<div />"),
clickFunc = function (e) {
editor.execCommand("fontname", $(this).data('sceditor-font'));
editor.closeDropDown(true);
e.preventDefault();
};
for (var i=0; i < fonts.length; i++) {
content.append(
$('<a class="sceditor-font-option" href="#"><font face="' + fonts[i] + '">' + fonts[i] + '</font></a>')
.data('sceditor-font', fonts[i])
.click(clickFunc));
}
editor.createDropDown(caller, "font-picker", content);
},
tooltip: "Font Name"
},
// END_COMMAND
// START_COMMAND: Size
size: {
exec: function (caller) {
var editor = this,
content = $("<div />"),
clickFunc = function (e) {
editor.execCommand("fontsize", $(this).data('sceditor-fontsize'));
editor.closeDropDown(true);
e.preventDefault();
};
for (var i=1; i<= 7; i++) {
content.append(
$('<a class="sceditor-fontsize-option" href="#"><font size="' + i + '">' + i + '</font></a>')
.data('sceditor-fontsize', i)
.click(clickFunc));
}
editor.createDropDown(caller, "fontsize-picker", content);
},
tooltip: "Font Size"
},
// END_COMMAND
// START_COMMAND: Colour
color: {
exec: function (caller) {
var editor = this,
genColor = {r: 255, g: 255, b: 255},
content = $("<div />"),
colorColumns = this.options.colors?this.options.colors.split("|"):new Array(21),
// IE is slow at string concation so use an array
html = [],
htmlIndex = 0;
for (var i=0; i < colorColumns.length; ++i) {
var colors = (typeof colorColumns[i] !== "undefined")?colorColumns[i].split(","):new Array(21);
html[htmlIndex++] = '<div class="sceditor-color-column">';
for (var x=0; x < colors.length; ++x) {
// use pre defined colour if can otherwise use the generated color
var color = (typeof colors[x] !== "undefined")?colors[x]:"#" + genColor.r.toString(16) + genColor.g.toString(16) + genColor.b.toString(16);
html[htmlIndex++] = '<a href="#" class="sceditor-color-option" style="background-color: '+color+'" data-color="'+color+'"></a>';
// calculate the next generated color
if(x%5===0)
genColor = {r: genColor.r, g: genColor.g-51, b: 255};
else
genColor = {r: genColor.r, g: genColor.g, b: genColor.b-51};
}
html[htmlIndex++] = '</div>';
// calculate the next generated color
if(i%5===0)
genColor = {r: genColor.r-51, g: 255, b: 255};
else
genColor = {r: genColor.r, g: 255, b: 255};
}
content.append(html.join(''))
.find('a')
.click(function (e) {
editor.execCommand("forecolor", $(this).attr('data-color'));
editor.closeDropDown(true);
e.preventDefault();
});
editor.createDropDown(caller, "color-picker", content);
},
tooltip: "Font Color"
},
// END_COMMAND
// START_COMMAND: Remove Format
removeformat: {
exec: "removeformat",
tooltip: "Remove Formatting"
},
// END_COMMAND
// START_COMMAND: Cut
cut: {
exec: "cut",
tooltip: "Cut",
errorMessage: "Your browser does not allow the cut command. Please use the keyboard shortcut Ctrl/Cmd-X"
},
// END_COMMAND
// START_COMMAND: Copy
copy: {
exec: "copy",
tooltip: "Copy",
errorMessage: "Your browser does not allow the copy command. Please use the keyboard shortcut Ctrl/Cmd-C"
},
// END_COMMAND
// START_COMMAND: Paste
paste: {
exec: "paste",
tooltip: "Paste",
errorMessage: "Your browser does not allow the paste command. Please use the keyboard shortcut Ctrl/Cmd-V"
},
// END_COMMAND
// START_COMMAND: Paste Text
pastetext: {
exec: function (caller) {
var editor = this,
content = $(this._('<form><div><label for="txt">{0}</label> <textarea cols="20" rows="7" id="txt">' +
'</textarea></div></form>',
this._("Paste your text inside the following box:")
))
.submit(function () {return false;});
content.append($(this._('<div><input type="button" class="button" value="{0}" /></div>',
this._("Insert")
)).click(function (e) {
editor.wysiwygEditorInsertText($(this).parent("form").find("#txt").val());
editor.closeDropDown(true);
e.preventDefault();
}));
editor.createDropDown(caller, "pastetext", content);
},
tooltip: "Paste Text"
},
// END_COMMAND
// START_COMMAND: Bullet List
bulletlist: {
exec: "insertunorderedlist",
tooltip: "Bullet list"
},
// END_COMMAND
// START_COMMAND: Ordered List
orderedlist: {
exec: "insertorderedlist",
tooltip: "Numbered list"
},
// END_COMMAND
// START_COMMAND: Table
table: {
exec: function (caller) {
var editor = this,
content = $(this._(
'<form><div><label for="rows">{0}</label><input type="text" id="rows" value="2" /></div>' +
'<div><label for="cols">{1}</label><input type="text" id="cols" value="2" /></div></form>',
this._("Rows:"),
this._("Cols:")
))
.submit(function () {return false;});
content.append($(this._('<div><input type="button" class="button" value="{0}" /></div>',
this._("Insert")
)).click(function (e) {
var rows = $(this).parent("form").find("#rows").val() - 0,
cols = $(this).parent("form").find("#cols").val() - 0,
html = '<table>';
if(rows < 1 || cols < 1)
return;
for (var row=0; row < rows; row++) {
html += '<tr>';
for (var col=0; col < cols; col++) {
if($.browser.msie)
html += '<td></td>';
else
html += '<td><br class="sceditor-ignore" /></td>';
}
html += '</tr>';
}
html += '</table>';
editor.wysiwygEditorInsertHtml(html);
editor.closeDropDown(true);
e.preventDefault();
}));
editor.createDropDown(caller, "inserttable", content);
},
tooltip: "Insert a table"
},
// END_COMMAND
// START_COMMAND: Horizontal Rule
horizontalrule: {
exec: "inserthorizontalrule",
tooltip: "Insert a horizontal rule"
},
// END_COMMAND
// START_COMMAND: Code
code: {
exec: function () {
this.wysiwygEditorInsertHtml('<code>', '<br /></code>');
},
tooltip: "Code"
},
// END_COMMAND
// START_COMMAND: Image
image: {
exec: function (caller) {
var editor = this,
content = $(this._('<form><div><label for="link">{0}</label> <input type="text" id="image" value="http://" /></div>' +
'<div><label for="width">{1}</label> <input type="text" id="width" size="2" /></div>' +
'<div><label for="height">{2}</label> <input type="text" id="height" size="2" /></div></form>',
this._("URL:"),
this._("Width (optional):"),
this._("Height (optional):")
))
.submit(function () {return false;});
content.append($(this._('<div><input type="button" class="button" value="Insert" /></div>',
this._("Insert")
)).click(function (e) {
var $form = $(this).parent("form"),
val = $form.find("#image").val(),
attrs = '',
width,
height;
if((width = $form.find("#width").val()))
attrs += ' width="' + width + '"';
if((height = $form.find("#height").val()))
attrs += ' height="' + height + '"';
if(val && val !== "http://")
editor.wysiwygEditorInsertHtml('<img' + attrs + ' src="' + val + '" />');
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 = $(this._('<form><div><label for="email">{0}</label> <input type="text" id="email" value="" /></div></form>',
this._("E-mail:")
))
.submit(function () {return false;});
content.append($('<div><input type="button" class="button" value="Insert" /></div>').click(function (e) {
var val = $(this).parent("form").find("#email").val();
if(val)
{
// needed for IE to reset the last range
editor.focus();
if(!editor.getRangeHelper().selectedHtml())
editor.wysiwygEditorInsertHtml('<a href="' + 'mailto:' + val + '">' + val + '</a>');
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 = $(this._('<form><div><label for="link">{0}</label> <input type="text" id="link" value="http://" /></div>' +
'<div><label for="des">{1}</label> <input type="text" id="des" value="" /></div></form>',
this._("URL:"),
this._("Description (optional):")
))
.submit(function () {return false;});
content.append($(
this._('<div><input type="button" class="button" value="{0}" /></div>',
this._("Insert")
)).click(function (e) {
var val = $(this).parent("form").find("#link").val(),
description = $(this).parent("form").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('<a href="' + val + '">' + description + '</a>');
}
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 = '<blockquote>',
end = '</blockquote>';
// if there is HTML passed set end to null so any selected
// text is replaced
if(html)
{
author = (author ? '<cite>' + author + '</cite>' : '');
before = before + author + html + end + '<br />';
end = null;
}
// if not add a newline to the end of the inserted quote
else if(this.getRangeHelper().selectedHtml() === "")
end = '<br />' + end;
this.wysiwygEditorInsertHtml(before, end);
},
tooltip: "Insert a Quote"
},
// END_COMMAND
// START_COMMAND: Emoticons
emoticon: {
exec: function (caller) {
var appendEmoticon,
editor = this,
content = $('<div />'),
line = $('<div />');
appendEmoticon = function (code, emoticon) {
line.append($('<img />')
.attr({
src: emoticon,
alt: code
})
.click(function (e) {
var start = '', end = '';
if(editor.options.emoticonsCompat)
{
start = '<span> ';
end = ' </span>';
}
editor.wysiwygEditorInsertHtml(start + '<img src="' + $(this).attr("src") +
'" data-sceditor-emoticon="' + $(this).attr('alt') + '" />' + end);
editor.closeDropDown(true);
e.preventDefault();
})
);
if(line.children().length > 3) {
content.append(line);
line = $('<div />');
}
};
$.each(editor.options.emoticons.dropdown, appendEmoticon);
if(line.children().length > 0)
content.append(line);
if(typeof editor.options.emoticons.more !== "undefined") {
var more = $(this._('<a class="sceditor-more">{0}</a>', this._("More"))).click(function () {
var emoticons = $.extend({}, editor.options.emoticons.dropdown, editor.options.emoticons.more);
content = $('<div />');
line = $('<div />');
$.each(emoticons, appendEmoticon);
if(line.children().length > 0)
content.append(line);
editor.createDropDown(caller, "insertemoticon", content);
});
content.append(more);
}
editor.createDropDown(caller, "insertemoticon", content);
},
keyPress: function (e, wysiwygEditor)
{
// make sure emoticons command is in the toolbar before running
if(this.options.toolbar.indexOf('emoticon') === -1)
return;
var editor = this,
pos = 0,
curChar = String.fromCharCode(e.which);
if(!editor.EmoticonsCache) {
editor.EmoticonsCache = [];
$.each($.extend({}, editor.options.emoticons.more, editor.options.emoticons.dropdown, editor.options.emoticons.hidden), function(key, url) {
editor.EmoticonsCache[pos++] = [
key,
'<img src="' + url + '" data-sceditor-emoticon="' + key + '" alt="' + key + '" />'
];
});
editor.EmoticonsCache.sort(function(a, b){
return a[0].length - b[0].length;
});
}
if(!editor.longestEmoticonCode)
editor.longestEmoticonCode = editor.EmoticonsCache[editor.EmoticonsCache.length - 1][0].length;
if(editor.getRangeHelper().raplaceKeyword(editor.EmoticonsCache, true, true, editor.longestEmoticonCode, editor.options.emoticonsCompat, curChar))
{
if(/^\s$/.test(curChar) && editor.options.emoticonsCompat)
return true;
e.preventDefault();
e.stopPropagation();
return false;
}
},
tooltip: "Insert an emoticon"
},
// END_COMMAND
// START_COMMAND: YouTube
youtube: {
exec: function (caller) {
var editor = this;
var content = $(
this._('<form><div><label for="link">{0}</label> <input type="text" id="link" value="http://" /></div></form>',
this._("Video URL:")
))
.submit(function () {return false;});
content.append(
$(this._('<div><input type="button" class="button" value="{0}" /></div>',
this._("Insert")
))
.click(function (e) {
var val = $(this).parent("form").find("#link").val();
if(val !== "" && val !== "http://") {
// See http://www.abovetopsecret.com/forum/thread270269/pg1
val = val.replace(/^[^v]+v.(.{11}).*/,"$1");
editor.wysiwygEditorInsertHtml('<iframe width="560" height="315" src="http://www.youtube.com/embed/' + val +
'?wmode=opaque" data-youtube-id="' + val + '" frameborder="0" allowfullscreen></iframe>');
}
editor.closeDropDown(true);
e.preventDefault();
}));
editor.createDropDown(caller, "insertlink", content);
},
tooltip: "Insert a YouTube video"
},
// END_COMMAND
// START_COMMAND: Date
date: {
exec: function () {
var now = new Date(),
year = now.getYear(),
month = now.getMonth()+1,
day = now.getDate();
if(year < 2000)
year = 1900 + year;
if(month < 10)
month = "0" + month;
if(day < 10)
day = "0" + day;
this.wysiwygEditorInsertHtml('<span>' +
this.options.dateFormat.replace(/year/i, year).replace(/month/i, month).replace(/day/i, day) +
'</span>');
},
tooltip: "Insert current date"
},
// END_COMMAND
// START_COMMAND: Time
time: {
exec: function () {
var now = new Date(),
hours = now.getHours(),
mins = now.getMinutes(),
secs = now.getSeconds();
if(hours < 10)
hours = "0" + hours;
if(mins < 10)
mins = "0" + mins;
if(secs < 10)
secs = "0" + secs;
this.wysiwygEditorInsertHtml('<span>' + hours + ':' + mins + ':' + secs + '</span>');
},
tooltip: "Insert current time"
},
// END_COMMAND
// START_COMMAND: Print
print: {
exec: "print",
tooltip: "Print"
},
// END_COMMAND
// START_COMMAND: Source
source: {
exec: function () {
this.toggleTextMode();
},
txtExec: function () {
this.toggleTextMode();
},
tooltip: "View source"
},
// END_COMMAND
// this is here so that commands above can be removed
// without having to remove the , after the last one.
// Needed for IE.
ignore: {}
};
/**
* Range helper class
*/
$.sceditor.rangeHelper = function(w, d) {
var win, doc,
isW3C = true,
startMarker = "sceditor-start-marker",
endMarker = "sceditor-end-marker",
base = this,
init, _createMarker, _getOuterText, _selectOuterText;
/**
* @constructor
* @param Window window
* @param Document document
* @private
*/
init = function (window, document) {
doc = document || window.contentDocument || window.document;
win = window;
isW3C = !!window.getSelection;
}(w, d);
/**
* Inserts HTML.
*
* If endHTML is specified the selected contents will be put between
* html and endHTML.
* @param string html
* @param string endHTML
*/
base.insertHTML = function(html, endHTML) {
var node, endNode, 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);
};
/**
* Inserts a DOM node.
*
* If endNode is specified the selected contents will be put between
* node and endNode.
* @param Node node
* @param Node endNode
*/
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
* @return Range|TextRange
*/
base.cloneSelected = function() {
if(!isW3C)
return base.selectedRange().duplicate();
return base.selectedRange().cloneRange();
};
/**
* Gets the selected Range
* @return Range|TextRange
*/
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 selected HTML
* @return string
*/
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 '';
};
base.parentNode = function() {
var range = base.selectedRange();
if(isW3C)
return range.commonAncestorContainer;
else
return range.parentElement();
};
/**
* Inserts a node at either the start or end of the current selection
* @param Bool start
* @param Node node
*/
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
*/
_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
*/
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
*/
base.getMarker = function(id) {
return doc.getElementById(id);
};
/**
* Removes the marker with the specified ID
* @param String id
*/
base.removeMarker = function(id) {
var marker = base.getMarker(id);
if(marker)
marker.parentNode.removeChild(marker);
};
/**
* Removes the start/end markers
*/
base.removeMarkers = function() {
base.removeMarker(startMarker);
base.removeMarker(endMarker);
};
/**
* Saves the current range location
*/
base.saveRange = function() {
base.insertMarkers();
};
/**
* Selected the specified range
* @param Range|TextRange range
*/
base.selectRange = function(range) {
if(!isW3C)
range.select();
else
{
win.getSelection().removeAllRanges();
win.getSelection().addRange(range);
}
};
/**
* Restores the last saved range if possible
*/
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(),
node;
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
*/
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)" + $.sceditor.regexEscape(rep[i][0]) + "(?=\\s|$)");
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
*/
$.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)
{
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 = reverse ? node.previousSibling : node.nextSibling;
}
}
},
rTraverse: function(node, func, innermostFirst, siblingsOnly) {
this.traverse(node, func, innermostFirst, siblingsOnly, true);
},
/**
* Checks if an element is inline
*
* @param bool includeInlineBlock If passed inline-block will count as an inline element instead of block
* @return bool
*/
isInline: function(elm, includeInlineBlock) {
if(elm == null || elm.nodeType !== 1)
return true;
var d = (window.getComputedStyle ? window.getComputedStyle(elm) : elm.currentStyle).display;
if(includeInlineBlock)
return d !== "block";
return d === "inline";
},
/**
* Gets the next node. If the node has no siblings
* it gets the parents next sibling, and so on untill
* another element is found. If none are found
* it returns null.
*
* @param HTMLElement node
* @return HTMLElement
*/
/*getNext: function(node) {
if(!node)
return null;
var n = node.nextSibling;
if(n)
return n;
return getNext(node.parentNode);
},*/
copyCSS: function(from, to) {
to.style.cssText = from.style.cssText;
},
/**
* Fixes block level elements in inline elements.
*
* @param HTMLElement The node to fix
*/
fixNesting: function(node) {
var base = this,
getLastInlineParent = function(node) {
while(base.isInline(node.parentNode))
node = node.parentNode;
return node;
};
base.traverse(node, function(node) {
// if node is an element, and is blocklevel and the parent isn't block level
// then it needs fixing
if(node.nodeType === 1 && !base.isInline(node) && base.isInline(node.parentNode))
{
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 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)
{
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?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);
}
});
return df;
}(commonAncestor));
}
};
/**
* Checks if a command with the specified name exists
*
* @param {String} name
* @return Bool
*/
$.sceditor.commandExists = function(name) {
return typeof $.sceditor.commands[name] !== "undefined";
};
/**
* 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
*/
$.sceditor.setCommand = function(name, exec, tooltip, keypress, txtExec) {
if(!name || !($.sceditor.commandExists(name) || exec))
return false;
if(!$.sceditor.commandExists(name))
$.sceditor.commands[name] = {};
$.sceditor.commands[name].exec = exec;
if(tooltip)
$.sceditor.commands[name].tooltip = tooltip;
if(keypress)
$.sceditor.commands[name].keyPress = keypress;
if(txtExec)
$.sceditor.commands[name].txtExec = txtExec;
return true;
};
$.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|" +
"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,
enablePasteFiltering: false,
//add css to dropdown menu (eg. z-index)
dropDownCss: { }
};
$.fn.sceditor = function (options) {
return this.each(function () {
(new $.sceditor(this, options));
});
};
})(jQuery);