From 9e2f9f49ec9803adfa0249161c8d2188ee0d40ca Mon Sep 17 00:00:00 2001 From: Benjamin Southall Date: Sun, 3 Sep 2017 23:15:56 +0900 Subject: Add sceditor WYSIWIG post editor support --- js/sceditor/development/jquery.sceditor.xhtml.js | 8548 ++++++++++++++++++++++ 1 file changed, 8548 insertions(+) create mode 100644 js/sceditor/development/jquery.sceditor.xhtml.js (limited to 'js/sceditor/development/jquery.sceditor.xhtml.js') diff --git a/js/sceditor/development/jquery.sceditor.xhtml.js b/js/sceditor/development/jquery.sceditor.xhtml.js new file mode 100644 index 00000000..948e648f --- /dev/null +++ b/js/sceditor/development/jquery.sceditor.xhtml.js @@ -0,0 +1,8548 @@ +/******/ (function(modules) { // webpackBootstrap +/******/ // The module cache +/******/ var installedModules = {}; + +/******/ // The require function +/******/ function __webpack_require__(moduleId) { + +/******/ // Check if module is in cache +/******/ if(installedModules[moduleId]) +/******/ return installedModules[moduleId].exports; + +/******/ // Create a new module (and put it into the cache) +/******/ var module = installedModules[moduleId] = { +/******/ exports: {}, +/******/ id: moduleId, +/******/ loaded: false +/******/ }; + +/******/ // Execute the module function +/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); + +/******/ // Flag the module as loaded +/******/ module.loaded = true; + +/******/ // Return the exports of the module +/******/ return module.exports; +/******/ } + + +/******/ // expose the modules object (__webpack_modules__) +/******/ __webpack_require__.m = modules; + +/******/ // expose the module cache +/******/ __webpack_require__.c = installedModules; + +/******/ // __webpack_public_path__ +/******/ __webpack_require__.p = ""; + +/******/ // Load entry module and return exports +/******/ return __webpack_require__(0); +/******/ }) +/************************************************************************/ +/******/ ([ +/* 0 */ +/***/ function(module, exports, __webpack_require__) { + + var __WEBPACK_AMD_DEFINE_RESULT__;/** + * SCEditor + * http://www.sceditor.com/ + * + * Copyright (C) 2014, 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 + * @requires jQuery + */ + !(__WEBPACK_AMD_DEFINE_RESULT__ = function (require) { + 'use strict'; + + var $ = __webpack_require__(1); + var SCEditor = __webpack_require__(2); + var PluginManager = __webpack_require__(3); + var browser = __webpack_require__(6); + var escape = __webpack_require__(7); + + + // For backwards compatibility + $.sceditor = SCEditor; + + SCEditor.commands = __webpack_require__(9); + SCEditor.defaultOptions = __webpack_require__(10); + SCEditor.RangeHelper = __webpack_require__(4); + SCEditor.dom = __webpack_require__(5); + + SCEditor.ie = browser.ie; + SCEditor.ios = browser.ios; + SCEditor.isWysiwygSupported = browser.isWysiwygSupported; + + SCEditor.regexEscape = escape.regex; + SCEditor.escapeEntities = escape.entities; + SCEditor.escapeUriScheme = escape.uriScheme; + + SCEditor.PluginManager = PluginManager; + SCEditor.plugins = PluginManager.plugins; + + + /** + * Creates an instance of sceditor on all textareas + * matched by the jQuery selector. + * + * If options is set to "state" it will return bool value + * indicating if the editor has been initialised on the + * matched textarea(s). If there is only one textarea + * it will return the bool value for that textarea. + * If more than one textarea is matched it will + * return an array of bool values for each textarea. + * + * If options is set to "instance" it will return the + * current editor instance for the textarea(s). Like the + * state option, if only one textarea is matched this will + * return just the instance for that textarea. If more than + * one textarea is matched it will return an array of + * instances each textarea. + * + * @param {Object|String} options Should either be an Object of options or + * the strings "state" or "instance" + * @return {this|Array|jQuery.sceditor|Bool} + */ + $.fn.sceditor = function (options) { + var $this, instance, + ret = []; + + options = options || {}; + + if (!options.runWithoutWysiwygSupport && !browser.isWysiwygSupported) { + return; + } + + this.each(function () { + $this = this.jquery ? this : $(this); + instance = $this.data('sceditor'); + + // Don't allow the editor to be initialised + // on it's own source editor + if ($this.parents('.sceditor-container').length > 0) { + return; + } + + // Add state of instance to ret if that is what options is set to + if (options === 'state') { + ret.push(!!instance); + } else if (options === 'instance') { + ret.push(instance); + } else if (!instance) { + /*jshint -W031*/ + (new SCEditor(this, options)); + } + }); + + // If nothing in the ret array then must be init so return this + if (!ret.length) { + return this; + } + + return ret.length === 1 ? ret[0] : $(ret); + }; + }.call(exports, __webpack_require__, exports, module), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__)); + + +/***/ }, +/* 1 */ +/***/ function(module, exports) { + + module.exports = jQuery; + +/***/ }, +/* 2 */ +/***/ function(module, exports, __webpack_require__) { + + var __WEBPACK_AMD_DEFINE_RESULT__;!(__WEBPACK_AMD_DEFINE_RESULT__ = function (require) { + 'use strict'; + + var $ = __webpack_require__(1); + var PluginManager = __webpack_require__(3); + var RangeHelper = __webpack_require__(4); + var dom = __webpack_require__(5); + var escape = __webpack_require__(7); + var browser = __webpack_require__(6); + var _tmpl = __webpack_require__(8); + + var globalWin = window; + var globalDoc = document; + var $globalWin = $(globalWin); + var $globalDoc = $(globalDoc); + + var IE_VER = browser.ie; + + // In IE < 11 a BR at the end of a block level element + // causes a line break. In all other browsers it's collapsed. + var IE_BR_FIX = IE_VER && IE_VER < 11; + + + /** + * SCEditor - A lightweight WYSIWYG editor + * + * @param {Element} el The textarea to be converted + * @return {Object} options + * @class sceditor + * @name jQuery.sceditor + */ + var SCEditor = function (el, options) { + /** + * Alias of this + * + * @private + */ + var base = this; + + /** + * The textarea element being replaced + * + * @private + */ + var original = el.get ? el.get(0) : el; + var $original = $(original); + + /** + * 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 WYSIWYG editors body element + * + * @private + */ + var $wysiwygBody; + + /** + * The WYSIWYG editors document + * + * @private + */ + var $wysiwygDoc; + + /** + * The editors textarea for viewing source + * + * @private + */ + var $sourceEditor; + var sourceEditor; + + /** + * The current dropdown + * + * @private + */ + var $dropdown; + + /** + * 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 + * @type {Array} + */ + var preLoadCache = []; + + /** + * The editors rangeHelper instance + * + * @type {jQuery.sceditor.rangeHelper} + * @private + */ + var rangeHelper; + + /** + * Tags which require the new line fix + * + * @type {Array} + * @private + */ + var requireNewLineFix = []; + + /** + * An array of button state handlers + * + * @type {Array} + * @private + */ + var btnStateHandlers = []; + + /** + * Plugin manager instance + * + * @type {jQuery.sceditor.PluginManager} + * @private + */ + var pluginManager; + + /** + * The current node containing the selection/caret + * + * @type {Node} + * @private + */ + var currentNode; + + /** + * The first block level parent of the current node + * + * @type {node} + * @private + */ + var currentBlockNode; + + /** + * The current node selection/caret + * + * @type {Object} + * @private + */ + var currentSelection; + + /** + * Used to make sure only 1 selection changed + * check is called every 100ms. + * + * Helps improve performance as it is checked a lot. + * + * @type {Boolean} + * @private + */ + var isSelectionCheckPending; + + /** + * If content is required (equivalent to the HTML5 required attribute) + * + * @type {Boolean} + * @private + */ + var isRequired; + + /** + * The inline CSS style element. Will be undefined + * until css() is called for the first time. + * + * @type {HTMLElement} + * @private + */ + var inlineCss; + + /** + * Object containing a list of shortcut handlers + * + * @type {Object} + * @private + */ + var shortcutHandlers = {}; + + /** + * An array of all the current emoticons. + * + * Only used or populated when emoticonsCompat is enabled. + * + * @type {Array} + * @private + */ + var currentEmoticons = []; + + /** + * Cache of the current toolbar buttons + * + * @type {Object} + * @private + */ + var toolbarButtons = {}; + + /** + * If the current autoUpdate action is canceled. + * + * @type {Boolean} + * @private + */ + var autoUpdateCanceled; + + /** + * Private functions + * @private + */ + var init, + replaceEmoticons, + handleCommand, + saveRange, + initEditor, + initPlugins, + initLocale, + initToolBar, + initOptions, + initEvents, + initCommands, + initResize, + initEmoticons, + getWysiwygDoc, + handlePasteEvt, + handlePasteData, + handleKeyDown, + handleBackSpace, + handleKeyPress, + handleFormReset, + handleMouseDown, + handleEvent, + handleDocumentClick, + handleWindowResize, + updateToolBar, + updateActiveButtons, + sourceEditorSelectedText, + appendNewLine, + checkSelectionChanged, + checkNodeChanged, + autofocus, + emoticonsKeyPress, + emoticonsCheckWhitespace, + currentStyledBlockNode, + triggerValueChanged, + valueChangedBlur, + valueChangedKeyUp, + autoUpdate; + + /** + * All the commands supported by the editor + * @name commands + * @memberOf jQuery.sceditor.prototype + */ + base.commands = $.extend( + true, + {}, + (options.commands || SCEditor.commands) + ); + + /** + * Options for this editor instance + * @name opts + * @memberOf jQuery.sceditor.prototype + */ + base.opts = options = $.extend({}, SCEditor.defaultOptions, options); + + + /** + * Creates the editor iframe and textarea + * @private + */ + init = function () { + $original.data('sceditor', base); + + // Clone any objects in options + $.each(options, function (key, val) { + if ($.isPlainObject(val)) { + options[key] = $.extend(true, {}, val); + } + }); + + // Load locale + if (options.locale && options.locale !== 'en') { + initLocale(); + } + + $editorContainer = $('
') + .insertAfter($original) + .css('z-index', options.zIndex); + + // Add IE version to the container to allow IE specific CSS + // fixes without using CSS hacks or conditional comments + if (IE_VER) { + $editorContainer.addClass('ie ie' + IE_VER); + } + + isRequired = !!$original.attr('required'); + $original.removeAttr('required'); + + // create the editor + initPlugins(); + initEmoticons(); + initToolBar(); + initEditor(!!options.startInSourceMode); + initCommands(); + initOptions(); + initEvents(); + + // force into source mode if is a browser that can't handle + // full editing + if (!browser.isWysiwygSupported) { + base.toggleSourceMode(); + } + + updateActiveButtons(); + + var loaded = function () { + $globalWin.off('load', loaded); + + if (options.autofocus) { + autofocus(); + } + + if (options.autoExpand) { + base.expandToContent(); + } + + // Page width might have changed after CSS is loaded so + // call handleWindowResize to update any % based dimensions + handleWindowResize(); + + pluginManager.call('ready'); + }; + $globalWin.on('load', loaded); + if (globalDoc.readyState && globalDoc.readyState === 'complete') { + loaded(); + } + }; + + initPlugins = function () { + var plugins = options.plugins; + + plugins = plugins ? plugins.toString().split(',') : []; + pluginManager = new PluginManager(base); + + $.each(plugins, function (idx, plugin) { + pluginManager.register($.trim(plugin)); + }); + }; + + /** + * Init the locale variable with the specified locale if possible + * @private + * @return void + */ + initLocale = function () { + var lang; + + locale = SCEditor.locale[options.locale]; + + if (!locale) { + lang = options.locale.split('-'); + locale = SCEditor.locale[lang[0]]; + } + + // Locale DateTime format overrides any specified in the options + if (locale && locale.dateFormat) { + options.dateFormat = locale.dateFormat; + } + }; + + /** + * Creates the editor iframe and textarea + * @param {boolean} startInSourceMode Force loading the editor in this + * mode + * @private + */ + initEditor = function (startInSourceMode) { + var doc, tabIndex; + + $sourceEditor = $(''); + $wysiwygEditor = $( + '' + ); + + /* This needs to be done right after they are created because, + * for any reason, the user may not want the value to be tinkered + * by any filters. + */ + if (startInSourceMode) { + $editorContainer.addClass('sourceMode'); + $wysiwygEditor.hide(); + } else { + $editorContainer.addClass('wysiwygMode'); + $sourceEditor.hide(); + } + + if (!options.spellcheck) { + $sourceEditor.attr('spellcheck', 'false'); + } + + /*jshint scripturl: true*/ + if (globalWin.location.protocol === 'https:') { + $wysiwygEditor.attr('src', 'javascript:false'); + } + + // Add the editor to the container + $editorContainer.append($wysiwygEditor).append($sourceEditor); + wysiwygEditor = $wysiwygEditor[0]; + sourceEditor = $sourceEditor[0]; + + base.dimensions( + options.width || $original.width(), + options.height || $original.height() + ); + + doc = getWysiwygDoc(); + doc.open(); + doc.write(_tmpl('html', { + // Add IE version class to the HTML element so can apply + // conditional styling without CSS hacks + attrs: IE_VER ? ' class="ie ie' + IE_VER + '"' : '', + spellcheck: options.spellcheck ? '' : 'spellcheck="false"', + charset: options.charset, + style: options.style + })); + doc.close(); + + $wysiwygDoc = $(doc); + $wysiwygBody = $(doc.body); + + base.readOnly(!!options.readOnly); + + // iframe overflow fix for iOS, also fixes an IE issue with the + // editor not getting focus when clicking inside + if (browser.ios || IE_VER) { + $wysiwygBody.height('100%'); + + if (!IE_VER) { + $wysiwygBody.on('touchend', base.focus); + } + } + + tabIndex = $original.attr('tabindex'); + $sourceEditor.attr('tabindex', tabIndex); + $wysiwygEditor.attr('tabindex', tabIndex); + + rangeHelper = new RangeHelper(wysiwygEditor.contentWindow); + + // load any textarea value into the editor + base.val($original.hide().val()); + }; + + /** + * Initialises options + * @private + */ + initOptions = function () { + // auto-update original textbox on blur if option set to true + if (options.autoUpdate) { + $wysiwygBody.on('blur', autoUpdate); + $sourceEditor.on('blur', autoUpdate); + } + + if (options.rtl === null) { + options.rtl = $sourceEditor.css('direction') === 'rtl'; + } + + base.rtl(!!options.rtl); + + if (options.autoExpand) { + $wysiwygDoc.on('keyup', base.expandToContent); + } + + if (options.resizeEnabled) { + initResize(); + } + + $editorContainer.attr('id', options.id); + base.emoticons(options.emoticonsEnabled); + }; + + /** + * Initialises events + * @private + */ + initEvents = function () { + var CHECK_SELECTION_EVENTS = IE_VER ? + 'selectionchange' : + 'keyup focus blur contextmenu mouseup touchend click'; + + var EVENTS_TO_FORWARD = 'keydown keyup keypress ' + + 'focus blur contextmenu'; + + $globalDoc.click(handleDocumentClick); + + $(original.form) + .on('reset', handleFormReset) + .submit(base.updateOriginal); + + $globalWin.on('resize orientationChanged', handleWindowResize); + + $wysiwygBody + .keypress(handleKeyPress) + .keydown(handleKeyDown) + .keydown(handleBackSpace) + .keyup(appendNewLine) + .blur(valueChangedBlur) + .keyup(valueChangedKeyUp) + .on('paste', handlePasteEvt) + .on(CHECK_SELECTION_EVENTS, checkSelectionChanged) + .on(EVENTS_TO_FORWARD, handleEvent); + + if (options.emoticonsCompat && globalWin.getSelection) { + $wysiwygBody.keyup(emoticonsCheckWhitespace); + } + + $sourceEditor + .blur(valueChangedBlur) + .keyup(valueChangedKeyUp) + .keydown(handleKeyDown) + .on(EVENTS_TO_FORWARD, handleEvent); + + $wysiwygDoc + .mousedown(handleMouseDown) + .blur(valueChangedBlur) + .on(CHECK_SELECTION_EVENTS, checkSelectionChanged) + .on('beforedeactivate keyup mouseup', saveRange) + .keyup(appendNewLine) + .focus(function () { + lastRange = null; + }); + + $editorContainer + .on('selectionchanged', checkNodeChanged) + .on('selectionchanged', updateActiveButtons) + .on('selectionchanged valuechanged nodechanged', handleEvent); + }; + + /** + * Creates the toolbar and appends it to the container + * @private + */ + initToolBar = function () { + var $group, + commands = base.commands, + exclude = (options.toolbarExclude || '').split(','), + groups = options.toolbar.split('|'); + + $toolbar = $('
'); + + $.each(groups, function (idx, group) { + $group = $('
'); + + $.each(group.split(','), function (idx, commandName) { + var $button, shortcut, + command = commands[commandName]; + + // The commandName must be a valid command and not excluded + if (!command || $.inArray(commandName, exclude) > -1) { + return; + } + + shortcut = command.shortcut; + $button = _tmpl('toolbarButton', { + name: commandName, + dispName: base._(command.name || + command.tooltip || commandName) + }, true); + + $button + .data('sceditor-txtmode', !!command.txtExec) + .data('sceditor-wysiwygmode', !!command.exec) + .toggleClass('disabled', !command.exec) + .mousedown(function () { + // IE < 8 supports unselectable attribute + // so don't need this + if (!IE_VER || IE_VER < 9) { + autoUpdateCanceled = true; + } + }) + .click(function () { + var $this = $(this); + + if (!$this.hasClass('disabled')) { + handleCommand($this, command); + } + + updateActiveButtons(); + return false; + }); + + if (command.tooltip) { + $button.attr( + 'title', + base._(command.tooltip) + + (shortcut ? ' (' + shortcut + ')' : '') + ); + } + + if (shortcut) { + base.addShortcut(shortcut, commandName); + } + + if (command.state) { + btnStateHandlers.push({ + name: commandName, + state: command.state + }); + // exec string commands can be passed to queryCommandState + } else if (typeof command.exec === 'string') { + btnStateHandlers.push({ + name: commandName, + state: command.exec + }); + } + + $group.append($button); + toolbarButtons[commandName] = $button; + }); + + // Exclude empty groups + if ($group[0].firstChild) { + $toolbar.append($group); + } + }); + + // Append the toolbar to the toolbarContainer option if given + $(options.toolbarContainer || $editorContainer).append($toolbar); + }; + + /** + * Creates an array of all the key press functions + * like emoticons, ect. + * @private + */ + initCommands = function () { + $.each(base.commands, function (name, cmd) { + if (cmd.forceNewLineAfter && $.isArray(cmd.forceNewLineAfter)) { + requireNewLineFix = $.merge( + requireNewLineFix, + cmd.forceNewLineAfter + ); + } + }); + + appendNewLine(); + }; + + /** + * Creates the resizer. + * @private + */ + initResize = function () { + var minHeight, maxHeight, minWidth, maxWidth, + mouseMoveFunc, mouseUpFunc, + $grip = $('
'), + // Cover is used to cover the editor iframe so document + // still gets mouse move events + $cover = $('
'), + moveEvents = 'touchmove mousemove', + endEvents = 'touchcancel touchend mouseup', + startX = 0, + startY = 0, + newX = 0, + newY = 0, + startWidth = 0, + startHeight = 0, + origWidth = $editorContainer.width(), + origHeight = $editorContainer.height(), + isDragging = false, + rtl = base.rtl(); + + minHeight = options.resizeMinHeight || origHeight / 1.5; + maxHeight = options.resizeMaxHeight || origHeight * 2.5; + minWidth = options.resizeMinWidth || origWidth / 1.25; + maxWidth = options.resizeMaxWidth || origWidth * 1.25; + + mouseMoveFunc = function (e) { + // iOS uses window.event + if (e.type === 'touchmove') { + e = globalWin.event; + newX = e.changedTouches[0].pageX; + newY = e.changedTouches[0].pageY; + } else { + newX = e.pageX; + newY = e.pageY; + } + + var newHeight = startHeight + (newY - startY), + newWidth = rtl ? + startWidth - (newX - startX) : + startWidth + (newX - startX); + + if (maxWidth > 0 && newWidth > maxWidth) { + newWidth = maxWidth; + } + if (minWidth > 0 && newWidth < minWidth) { + newWidth = minWidth; + } + if (!options.resizeWidth) { + newWidth = false; + } + + if (maxHeight > 0 && newHeight > maxHeight) { + newHeight = maxHeight; + } + if (minHeight > 0 && newHeight < minHeight) { + newHeight = minHeight; + } + if (!options.resizeHeight) { + newHeight = false; + } + + if (newWidth || newHeight) { + base.dimensions(newWidth, newHeight); + + // The resize cover will not fill the container + // in IE6 unless a height is specified. + if (IE_VER < 7) { + $editorContainer.height(newHeight); + } + } + + e.preventDefault(); + }; + + mouseUpFunc = function (e) { + if (!isDragging) { + return; + } + + isDragging = false; + + $cover.hide(); + $editorContainer.removeClass('resizing').height('auto'); + $globalDoc.off(moveEvents, mouseMoveFunc); + $globalDoc.off(endEvents, mouseUpFunc); + + e.preventDefault(); + }; + + $editorContainer.append($grip); + $editorContainer.append($cover.hide()); + + $grip.on('touchstart mousedown', function (e) { + // iOS uses window.event + if (e.type === 'touchstart') { + e = globalWin.event; + startX = e.touches[0].pageX; + startY = e.touches[0].pageY; + } else { + startX = e.pageX; + startY = e.pageY; + } + + startWidth = $editorContainer.width(); + startHeight = $editorContainer.height(); + isDragging = true; + + $editorContainer.addClass('resizing'); + $cover.show(); + $globalDoc.on(moveEvents, mouseMoveFunc); + $globalDoc.on(endEvents, mouseUpFunc); + + // The resize cover will not fill the container in + // IE6 unless a height is specified. + if (IE_VER < 7) { + $editorContainer.height(startHeight); + } + + e.preventDefault(); + }); + }; + + /** + * Prefixes and preloads the emoticon images + * @private + */ + initEmoticons = function () { + var emoticon, + emoticons = options.emoticons, + root = options.emoticonsRoot; + + if (!$.isPlainObject(emoticons) || !options.emoticonsEnabled) { + return; + } + + $.each(emoticons, function (idx, val) { + $.each(val, function (key, url) { + // Prefix emoticon root to emoticon urls + if (root) { + url = { + url: root + (url.url || url), + tooltip: url.tooltip || key + }; + + emoticons[idx][key] = url; + } + + // Preload the emoticon + emoticon = globalDoc.createElement('img'); + emoticon.src = url.url || url; + preLoadCache.push(emoticon); + }); + }); + }; + + /** + * Autofocus the editor + * @private + */ + autofocus = function () { + var range, txtPos, + doc = $wysiwygDoc[0], + body = $wysiwygBody[0], + node = body.firstChild, + focusEnd = !!options.autofocusEnd; + + // Can't focus invisible elements + if (!$editorContainer.is(':visible')) { + return; + } + + if (base.sourceMode()) { + txtPos = focusEnd ? sourceEditor.value.length : 0; + + if (sourceEditor.setSelectionRange) { + sourceEditor.setSelectionRange(txtPos, txtPos); + } else { + range = sourceEditor.createTextRange(); + range.moveEnd('character', txtPos); + range.collapse(false); + range.select(); + } + + return; + } + + dom.removeWhiteSpace(body); + + if (focusEnd) { + if (!(node = body.lastChild)) { + node = doc.createElement('p'); + $wysiwygBody.append(node); + } + + while (node.lastChild) { + node = node.lastChild; + + // IE < 11 should place the cursor after the
as + // it will show it as a newline. IE >= 11 and all + // other browsers should place the cursor before. + if (!IE_BR_FIX && $(node).is('br') && + node.previousSibling) { + node = node.previousSibling; + } + } + } + + if (doc.createRange) { + range = doc.createRange(); + + if (!dom.canHaveChildren(node)) { + range.setStartBefore(node); + + if (focusEnd) { + range.setStartAfter(node); + } + } else { + range.selectNodeContents(node); + } + } else { + range = body.createTextRange(); + range.moveToElementText(node.nodeType !== 3 ? + node : node.parentNode); + } + + range.collapse(!focusEnd); + rangeHelper.selectRange(range); + currentSelection = range; + + if (focusEnd) { + $wysiwygDoc.scrollTop(body.scrollHeight); + $wysiwygBody.scrollTop(body.scrollHeight); + } + + base.focus(); + }; + + /** + * Gets if the editor is read only + * + * @since 1.3.5 + * @function + * @memberOf jQuery.sceditor.prototype + * @name readOnly + * @return {Boolean} + */ + /** + * Sets if the editor is read only + * + * @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 $sourceEditor.attr('readonly') === 'readonly'; + } + + $wysiwygBody[0].contentEditable = !readOnly; + + if (!readOnly) { + $sourceEditor.removeAttr('readonly'); + } else { + $sourceEditor.attr('readonly', 'readonly'); + } + + updateToolBar(readOnly); + + return base; + }; + + /** + * Gets if the editor is in RTL mode + * + * @since 1.4.1 + * @function + * @memberOf jQuery.sceditor.prototype + * @name rtl + * @return {Boolean} + */ + /** + * Sets if the editor is in RTL mode + * + * @param {boolean} rtl + * @since 1.4.1 + * @function + * @memberOf jQuery.sceditor.prototype + * @name rtl^2 + * @return {this} + */ + base.rtl = function (rtl) { + var dir = rtl ? 'rtl' : 'ltr'; + + if (typeof rtl !== 'boolean') { + return $sourceEditor.attr('dir') === 'rtl'; + } + + $wysiwygBody.attr('dir', dir); + $sourceEditor.attr('dir', dir); + + $editorContainer + .removeClass('rtl') + .removeClass('ltr') + .addClass(dir); + + return base; + }; + + /** + * Updates the toolbar to disable/enable the appropriate buttons + * @private + */ + updateToolBar = function (disable) { + var mode = base.inSourceMode() ? 'txtmode' : 'wysiwygmode'; + + $.each(toolbarButtons, function (idx, $button) { + if (disable === true || !$button.data('sceditor-' + mode)) { + $button.addClass('disabled'); + } else { + $button.removeClass('disabled'); + } + }); + }; + + /** + * Gets the width of the editor in pixels + * + * @since 1.3.5 + * @function + * @memberOf jQuery.sceditor.prototype + * @name width + * @return {int} + */ + /** + * Sets the width of the editor + * + * @param {int} width Width in pixels + * @since 1.3.5 + * @function + * @memberOf jQuery.sceditor.prototype + * @name width^2 + * @return {this} + */ + /** + * Sets the width of the editor + * + * The saveWidth specifies if to save the width. The stored width can be + * used for things like restoring from maximized state. + * + * @param {int} width Width in pixels + * @param {boolean} [saveWidth=true] If to store the width + * @since 1.4.1 + * @function + * @memberOf jQuery.sceditor.prototype + * @name width^3 + * @return {this} + */ + base.width = function (width, saveWidth) { + if (!width && width !== 0) { + return $editorContainer.width(); + } + + base.dimensions(width, null, saveWidth); + + return base; + }; + + /** + * Returns an object with the properties width and height + * which are the width and height of the editor in px. + * + * @since 1.4.1 + * @function + * @memberOf jQuery.sceditor.prototype + * @name dimensions + * @return {object} + */ + /** + *

Sets the width and/or height of the editor.

+ * + *

If width or height is not numeric it is ignored.

+ * + * @param {int} width Width in px + * @param {int} height Height in px + * @since 1.4.1 + * @function + * @memberOf jQuery.sceditor.prototype + * @name dimensions^2 + * @return {this} + */ + /** + *

Sets the width and/or height of the editor.

+ * + *

If width or height is not numeric it is ignored.

+ * + *

The save argument specifies if to save the new sizes. + * The saved sizes can be used for things like restoring from + * maximized state. This should normally be left as true.

+ * + * @param {int} width Width in px + * @param {int} height Height in px + * @param {boolean} [save=true] If to store the new sizes + * @since 1.4.1 + * @function + * @memberOf jQuery.sceditor.prototype + * @name dimensions^3 + * @return {this} + */ + base.dimensions = function (width, height, save) { + // IE6 & IE7 add 2 pixels to the source mode textarea + // height which must be ignored. + // Doesn't seem to be any way to fix it with only CSS + var ieBorder = IE_VER < 8 || globalDoc.documentMode < 8 ? 2 : 0; + var undef; + + // set undefined width/height to boolean false + width = (!width && width !== 0) ? false : width; + height = (!height && height !== 0) ? false : height; + + if (width === false && height === false) { + return { width: base.width(), height: base.height() }; + } + + if ($wysiwygEditor.data('outerWidthOffset') === undef) { + base.updateStyleCache(); + } + + if (width !== false) { + if (save !== false) { + options.width = width; + } + // This is the problem + if (height === false) { + height = $editorContainer.height(); + save = false; + } + + $editorContainer.width(width); + if (width && width.toString().indexOf('%') > -1) { + width = $editorContainer.width(); + } + + $wysiwygEditor.width( + width - $wysiwygEditor.data('outerWidthOffset') + ); + + $sourceEditor.width( + width - $sourceEditor.data('outerWidthOffset') + ); + + // Fix overflow issue with iOS not + // breaking words unless a width is set + if (browser.ios && $wysiwygBody) { + $wysiwygBody.width( + width - $wysiwygEditor.data('outerWidthOffset') - + ($wysiwygBody.outerWidth(true) - $wysiwygBody.width()) + ); + } + } + + if (height !== false) { + if (save !== false) { + options.height = height; + } + + // Convert % based heights to px + if (height && height.toString().indexOf('%') > -1) { + height = $editorContainer.height(height).height(); + $editorContainer.height('auto'); + } + + height -= !options.toolbarContainer ? + $toolbar.outerHeight(true) : 0; + + $wysiwygEditor.height( + height - $wysiwygEditor.data('outerHeightOffset') + ); + + $sourceEditor.height( + height - ieBorder - $sourceEditor.data('outerHeightOffset') + ); + } + + return base; + }; + + /** + * Updates the CSS styles cache. + * + * This shouldn't be needed unless changing the editors theme. + *F + * @since 1.4.1 + * @function + * @memberOf jQuery.sceditor.prototype + * @name updateStyleCache + * @return {int} + */ + base.updateStyleCache = function () { + // caching these improves FF resize performance + $wysiwygEditor.data( + 'outerWidthOffset', + $wysiwygEditor.outerWidth(true) - $wysiwygEditor.width() + ); + $sourceEditor.data( + 'outerWidthOffset', + $sourceEditor.outerWidth(true) - $sourceEditor.width() + ); + + $wysiwygEditor.data( + 'outerHeightOffset', + $wysiwygEditor.outerHeight(true) - $wysiwygEditor.height() + ); + $sourceEditor.data( + 'outerHeightOffset', + $sourceEditor.outerHeight(true) - $sourceEditor.height() + ); + }; + + /** + * 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} + */ + /** + * Sets the height of the editor + * + * The saveHeight specifies if to save the height. + * + * The stored height can be used for things like + * restoring from maximized state. + * + * @param {int} height Height in px + * @param {boolean} [saveHeight=true] If to store the height + * @since 1.4.1 + * @function + * @memberOf jQuery.sceditor.prototype + * @name height^3 + * @return {this} + */ + base.height = function (height, saveHeight) { + if (!height && height !== 0) { + return $editorContainer.height(); + } + + base.dimensions(null, height, saveHeight); + + return base; + }; + + /** + * Gets if the editor is maximised or not + * + * @since 1.4.1 + * @function + * @memberOf jQuery.sceditor.prototype + * @name maximize + * @return {boolean} + */ + /** + * Sets if the editor is maximised or not + * + * @param {boolean} maximize If to maximise the editor + * @since 1.4.1 + * @function + * @memberOf jQuery.sceditor.prototype + * @name maximize^2 + * @return {this} + */ + base.maximize = function (maximize) { + if (typeof maximize === 'undefined') { + return $editorContainer.is('.sceditor-maximize'); + } + + maximize = !!maximize; + + // IE 6 fix + if (IE_VER < 7) { + $('html, body').toggleClass('sceditor-maximize', maximize); + } + + $editorContainer.toggleClass('sceditor-maximize', maximize); + base.width(maximize ? '100%' : options.width, false); + base.height(maximize ? '100%' : options.height, false); + + return base; + }; + + /** + * Expands the editors height to the height of it's content + * + * Unless ignoreMaxHeight is set to true it will not expand + * higher than the maxHeight option. + * + * @since 1.3.5 + * @param {Boolean} [ignoreMaxHeight=false] + * @function + * @name expandToContent + * @memberOf jQuery.sceditor.prototype + * @see #resizeToContent + */ + base.expandToContent = function (ignoreMaxHeight) { + var currentHeight = $editorContainer.height(), + padding = (currentHeight - $wysiwygEditor.height()), + height = $wysiwygBody[0].scrollHeight || + $wysiwygDoc[0].documentElement.scrollHeight, + maxHeight = options.resizeMaxHeight || + ((options.height || $original.height()) * 2); + + height += padding; + + if ((ignoreMaxHeight === true || height <= maxHeight) && + height > currentHeight) { + base.height(height); + } + }; + + /** + * Destroys the editor, removing all elements and + * event handlers. + * + * Leaves only the original textarea. + * + * @function + * @name destroy + * @memberOf jQuery.sceditor.prototype + */ + base.destroy = function () { + // Don't destroy if the editor has already been destroyed + if (!pluginManager) { + return; + } + + pluginManager.destroy(); + + rangeHelper = null; + lastRange = null; + pluginManager = null; + + if ($dropdown) { + $dropdown.off().remove(); + } + + $globalDoc.off('click', handleDocumentClick); + $globalWin.off('resize orientationChanged', handleWindowResize); + + $(original.form) + .off('reset', handleFormReset) + .off('submit', base.updateOriginal); + + $wysiwygBody.off(); + $wysiwygDoc.off().find('*').remove(); + + $sourceEditor.off().remove(); + $toolbar.remove(); + $editorContainer.off().find('*').off().remove(); + $editorContainer.remove(); + + $original + .removeData('sceditor') + .removeData('sceditorbbcode') + .show(); + + if (isRequired) { + $original.attr('required', 'required'); + } + }; + + + /** + * Creates a menu item drop down + * + * @param {HTMLElement} menuItem The button to align the dropdown with + * @param {string} name Used for styling the dropdown, will be + * a class sceditor-name + * @param {HTMLElement} content The HTML content of the dropdown + * @param {bool} ieFix 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, name, content, ieFix) { + // first click for create second click for close + var dropDownCss, + cssClass = 'sceditor-' + name, + onlyclose = $dropdown && $dropdown.is('.' + cssClass); + + // Will re-focus the editor. This is needed for IE + // as it has special logic to save/restore the selection + base.closeDropDown(true); + + if (onlyclose) { + return; + } + + // IE needs unselectable attr to stop it from + // unselecting the text in the editor. + // SCEditor can cope if IE does unselect the + // text it's just not nice. + if (ieFix !== false) { + $(content) + .find(':not(input,textarea)') + .filter(function () { + return this.nodeType === 1; + }) + .attr('unselectable', 'on'); + } + + dropDownCss = { + top: menuItem.offset().top, + left: menuItem.offset().left, + marginTop: menuItem.outerHeight() + }; + $.extend(dropDownCss, options.dropDownCss); + + $dropdown = $('
') + .css(dropDownCss) + .append(content) + .appendTo($('body')) + .on('click focusin', function (e) { + // stop clicks within the dropdown from being handled + e.stopPropagation(); + }); + + // If try to focus the first input immediately IE will + // place the cursor at the start of the editor instead + // of focusing on the input. + setTimeout(function () { + if ($dropdown) { + $dropdown.find('input,textarea').first().focus(); + } + }); + }; + + /** + * Handles any document click and closes the dropdown if open + * @private + */ + handleDocumentClick = function (e) { + // ignore right clicks + if (e.which !== 3 && $dropdown) { + autoUpdate(); + + base.closeDropDown(); + } + }; + + /** + * Handles the WYSIWYG editors paste event + * @private + */ + handlePasteEvt = function (e) { + var html, handlePaste, scrollTop, + elm = $wysiwygBody[0], + doc = $wysiwygDoc[0], + checkCount = 0, + pastearea = globalDoc.createElement('div'), + prePasteContent = doc.createDocumentFragment(), + clipboardData = e ? e.clipboardData : false; + + if (options.disablePasting) { + return false; + } + + if (!options.enablePasteFiltering) { + return; + } + + rangeHelper.saveRange(); + globalDoc.body.appendChild(pastearea); + + if (clipboardData && clipboardData.getData) { + if ((html = clipboardData.getData('text/html')) || + (html = clipboardData.getData('text/plain'))) { + pastearea.innerHTML = html; + handlePasteData(elm, pastearea); + + return false; + } + } + + // Save the scroll position so can be restored + // when contents is restored + scrollTop = $wysiwygBody.scrollTop() || $wysiwygDoc.scrollTop(); + + while (elm.firstChild) { + prePasteContent.appendChild(elm.firstChild); + } + + // try make pastearea contenteditable and redirect to that? Might work. + // Check the tests if still exist, if not re-0create + handlePaste = function (elm, pastearea) { + if (elm.childNodes.length > 0 || checkCount > 25) { + while (elm.firstChild) { + pastearea.appendChild(elm.firstChild); + } + + while (prePasteContent.firstChild) { + elm.appendChild(prePasteContent.firstChild); + } + + $wysiwygBody.scrollTop(scrollTop); + $wysiwygDoc.scrollTop(scrollTop); + + if (pastearea.childNodes.length > 0) { + handlePasteData(elm, pastearea); + } else { + rangeHelper.restoreRange(); + } + } else { + // Allow max 25 checks before giving up. + // Needed in case an empty string is pasted or + // something goes wrong. + checkCount++; + setTimeout(function () { + handlePaste(elm, pastearea); + }, 20); + } + }; + handlePaste(elm, pastearea); + + base.focus(); + return true; + }; + + /** + * Gets the pasted data, filters it and then inserts it. + * @param {Element} elm + * @param {Element} pastearea + * @private + */ + handlePasteData = function (elm, pastearea) { + // fix any invalid nesting + dom.fixNesting(pastearea); + // TODO: Trigger custom paste event to allow filtering + // (pre and post converstion?) + var pasteddata = pastearea.innerHTML; + + if (pluginManager.hasHandler('toSource')) { + pasteddata = pluginManager.callOnlyFirst( + 'toSource', pasteddata, $(pastearea) + ); + } + + pastearea.parentNode.removeChild(pastearea); + + if (pluginManager.hasHandler('toWysiwyg')) { + pasteddata = pluginManager.callOnlyFirst( + 'toWysiwyg', pasteddata, true + ); + } + + rangeHelper.restoreRange(); + base.wysiwygEditorInsertHtml(pasteddata, null, true); + }; + + /** + * Closes any currently open drop down + * + * @param {bool} [focus=false] If to focus the editor + * after closing the drop down + * @function + * @name closeDropDown + * @memberOf jQuery.sceditor.prototype + */ + base.closeDropDown = function (focus) { + if ($dropdown) { + $dropdown.off().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; + } + + return wysiwygEditor.document; + }; + + + /** + *

Inserts HTML into WYSIWYG editor.

+ * + *

If endHtml is specified, any selected text will be placed + * between html and endHtml. If there is no selected text html + * and endHtml will just be concatenate together.

+ * + * @param {string} html + * @param {string} [endHtml=null] + * @param {boolean} [overrideCodeBlocking=false] If to insert the html + * into code tags, by + * default code tags only + * support text. + * @function + * @name wysiwygEditorInsertHtml + * @memberOf jQuery.sceditor.prototype + */ + base.wysiwygEditorInsertHtml = function ( + html, endHtml, overrideCodeBlocking + ) { + var $marker, scrollTop, scrollTo, + editorHeight = $wysiwygEditor.height(); + + base.focus(); + + // TODO: This code tag should be configurable and + // should maybe convert the HTML into text instead + // Don't apply to code elements + if (!overrideCodeBlocking && ($(currentBlockNode).is('code') || + $(currentBlockNode).parents('code').length !== 0)) { + return; + } + + // Insert the HTML and save the range so the editor can be scrolled + // to the end of the selection. Also allows emoticons to be replaced + // without affecting the cursor position + rangeHelper.insertHTML(html, endHtml); + rangeHelper.saveRange(); + replaceEmoticons($wysiwygBody[0]); + + // Scroll the editor after the end of the selection + $marker = $wysiwygBody.find('#sceditor-end-marker').show(); + scrollTop = $wysiwygBody.scrollTop() || $wysiwygDoc.scrollTop(); + scrollTo = (dom.getOffset($marker[0]).top + + ($marker.outerHeight(true) * 1.5)) - editorHeight; + $marker.hide(); + + // Only scroll if marker isn't already visible + if (scrollTo > scrollTop || scrollTo + editorHeight < scrollTop) { + $wysiwygBody.scrollTop(scrollTo); + $wysiwygDoc.scrollTop(scrollTo); + } + + triggerValueChanged(false); + rangeHelper.restoreRange(); + + // Add a new line after the last block element + // so can always add text after it + appendNewLine(); + }; + + /** + * 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) { + base.wysiwygEditorInsertHtml( + escape.entities(text), escape.entities(endText) + ); + }; + + /** + *

Inserts text into the WYSIWYG or source editor 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 concatenate 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.sourceEditorInsertText(text, endText); + } else { + base.wysiwygEditorInsertText(text, endText); + } + + return base; + }; + + /** + *

Like wysiwygEditorInsertHtml but inserts text into the + * source mode editor instead.

+ * + *

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 concatenate together.

+ * + *

The cursor will be placed after the text param. If endText is + * specified the cursor will be placed before endText, so passing:
+ * + * '[b]', '[/b]'

+ * + *

Would cause the cursor to be placed:
+ * + * [b]Selected text|[/b]

+ * + * @param {String} text + * @param {String} [endText=null] + * @since 1.4.0 + * @function + * @name sourceEditorInsertText + * @memberOf jQuery.sceditor.prototype + */ + base.sourceEditorInsertText = function (text, endText) { + var range, scrollTop, currentValue, + startPos = sourceEditor.selectionStart, + endPos = sourceEditor.selectionEnd; + + scrollTop = sourceEditor.scrollTop; + sourceEditor.focus(); + + // All browsers except IE < 9 + if (typeof startPos !== 'undefined') { + currentValue = sourceEditor.value; + + if (endText) { + text += currentValue.substring(startPos, endPos) + endText; + } + + sourceEditor.value = currentValue.substring(0, startPos) + + text + + currentValue.substring(endPos, currentValue.length); + + sourceEditor.selectionStart = (startPos + text.length) - + (endText ? endText.length : 0); + sourceEditor.selectionEnd = sourceEditor.selectionStart; + // IE < 9 + } else { + range = globalDoc.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(); + } + + sourceEditor.scrollTop = scrollTop; + sourceEditor.focus(); + + triggerValueChanged(); + }; + + /** + * Gets the current instance of the rangeHelper class + * for the editor. + * + * @return jQuery.sceditor.rangeHelper + * @function + * @name getRangeHelper + * @memberOf jQuery.sceditor.prototype + */ + base.getRangeHelper = function () { + return rangeHelper; + }; + + /** + * Gets the source editors textarea. + * + * This shouldn't be used to insert text + * + * @return {jQuery} + * @function + * @since 1.4.5 + * @name sourceEditorCaret + * @memberOf jQuery.sceditor.prototype + */ + base.sourceEditorCaret = function (position) { + var range, + ret = {}; + + sourceEditor.focus(); + + // All browsers except IE <= 8 + if (typeof sourceEditor.selectionStart !== 'undefined') { + if (position) { + sourceEditor.selectionStart = position.start; + sourceEditor.selectionEnd = position.end; + } else { + ret.start = sourceEditor.selectionStart; + ret.end = sourceEditor.selectionEnd; + } + + // IE8 and below + } else { + range = globalDoc.selection.createRange(); + + if (position) { + range.moveEnd('character', position.end); + range.moveStart('character', position.start); + range.select(); + } else { + ret.start = range.Start; + ret.end = range.End; + } + } + + return position ? this : ret; + }; + + /** + *

Gets the value of the editor.

+ * + *

If the editor is in WYSIWYG mode it will return the filtered + * HTML from it (converted to BBCode if using the BBCode plugin). + * It it's in Source Mode it will return the unfiltered contents + * of the source editor (if using the BBCode plugin this will be + * BBCode again).

+ * + * @since 1.3.5 + * @return {string} + * @function + * @name val + * @memberOf jQuery.sceditor.prototype + */ + /** + *

Sets the value of the editor.

+ * + *

If filter set true the val will be passed through the filter + * function. If using the BBCode plugin it will pass the val to + * the BBCode filter to convert any BBCode into HTML.

+ * + * @param {String} val + * @param {Boolean} [filter=true] + * @return {this} + * @since 1.3.5 + * @function + * @name val^2 + * @memberOf jQuery.sceditor.prototype + */ + base.val = function (val, filter) { + if (typeof val !== 'string') { + return base.inSourceMode() ? + base.getSourceEditorValue(false) : + base.getWysiwygEditorValue(filter); + } + + if (!base.inSourceMode()) { + if (filter !== false && + pluginManager.hasHandler('toWysiwyg')) { + val = pluginManager.callOnlyFirst('toWysiwyg', val); + } + + base.setWysiwygEditorValue(val); + } else { + base.setSourceEditorValue(val); + } + + return base; + }; + + /** + *

Inserts HTML/BBCode into the editor

+ * + *

If end is supplied any selected text will be placed between + * start and end. If there is no selected text start and end + * will be concatenate together.

+ * + *

If the filter param is set to true, the HTML/BBCode will be + * passed through any plugin filters. If using the BBCode plugin + * this will convert any BBCode into HTML.

+ * + * @param {String} start + * @param {String} [end=null] + * @param {Boolean} [filter=true] + * @param {Boolean} [convertEmoticons=true] If to convert emoticons + * @return {this} + * @since 1.3.5 + * @function + * @name insert + * @memberOf jQuery.sceditor.prototype + */ + /** + *

Inserts HTML/BBCode into the editor

+ * + *

If end is supplied any selected text will be placed between + * start and end. If there is no selected text start and end + * will be concatenate together.

+ * + *

If the filter param is set to true, the HTML/BBCode will be + * passed through any plugin filters. If using the BBCode plugin + * this will convert any BBCode into HTML.

+ * + *

If the allowMixed param is set to true, HTML any will not be + * escaped

+ * + * @param {String} start + * @param {String} [end=null] + * @param {Boolean} [filter=true] + * @param {Boolean} [convertEmoticons=true] If to convert emoticons + * @param {Boolean} [allowMixed=false] + * @return {this} + * @since 1.4.3 + * @function + * @name insert^2 + * @memberOf jQuery.sceditor.prototype + */ + base.insert = function ( + /*jshint maxparams: false */ + start, end, filter, convertEmoticons, allowMixed + ) { + if (base.inSourceMode()) { + base.sourceEditorInsertText(start, end); + return base; + } + + // Add the selection between start and end + if (end) { + var html = rangeHelper.selectedHtml(), + $div = $('
').appendTo($('body')).hide().html(html); + + if (filter !== false && pluginManager.hasHandler('toSource')) { + html = pluginManager.callOnlyFirst('toSource', html, $div); + } + + $div.remove(); + + start += html + end; + } + // TODO: This filter should allow empty tags as it's inserting. + if (filter !== false && pluginManager.hasHandler('toWysiwyg')) { + start = pluginManager.callOnlyFirst('toWysiwyg', start, true); + } + + // Convert any escaped HTML back into HTML if mixed is allowed + if (filter !== false && allowMixed === true) { + start = start.replace(/</g, '<') + .replace(/>/g, '>') + .replace(/&/g, '&'); + } + + base.wysiwygEditorInsertHtml(start); + + return base; + }; + + /** + * Gets the WYSIWYG editors HTML value. + * + * If using a plugin that filters the Ht Ml like the BBCode plugin + * it will return the result of the filtering (BBCode) unless the + * filter param is set to false. + * + * @param {bool} [filter=true] + * @return {string} + * @function + * @name getWysiwygEditorValue + * @memberOf jQuery.sceditor.prototype + */ + base.getWysiwygEditorValue = function (filter) { + var html; + // Create a tmp node to store contents so it can be modified + // without affecting anything else. + var $tmp = $('
') + .appendTo(document.body) + .append($($wysiwygBody[0].childNodes).clone()); + + dom.fixNesting($tmp[0]); + + html = $tmp.html(); + + // filter the HTML and DOM through any plugins + if (filter !== false && pluginManager.hasHandler('toSource')) { + html = pluginManager.callOnlyFirst('toSource', html, $tmp); + } + + $tmp.remove(); + + return html; + }; + + /** + * Gets the WYSIWYG editor's iFrame Body. + * + * @return {jQuery} + * @function + * @since 1.4.3 + * @name getBody + * @memberOf jQuery.sceditor.prototype + */ + base.getBody = function () { + return $wysiwygBody; + }; + + /** + * Gets the WYSIWYG editors container area (whole iFrame). + * + * @return {Node} + * @function + * @since 1.4.3 + * @name getContentAreaContainer + * @memberOf jQuery.sceditor.prototype + */ + base.getContentAreaContainer = function () { + return $wysiwygEditor; + }; + + /** + * Gets the text editor value + * + * If using a plugin that filters the text like the BBCode plugin + * it will return the result of the filtering which is BBCode to + * HTML so it will return HTML. If filter is set to false it will + * just return the contents of the source editor (BBCode). + * + * @param {bool} [filter=true] + * @return {string} + * @function + * @since 1.4.0 + * @name getSourceEditorValue + * @memberOf jQuery.sceditor.prototype + */ + base.getSourceEditorValue = function (filter) { + var val = $sourceEditor.val(); + + if (filter !== false && pluginManager.hasHandler('toWysiwyg')) { + val = pluginManager.callOnlyFirst('toWysiwyg', val); + } + + return val; + }; + + /** + * Sets the WYSIWYG HTML editor value. Should only be the HTML + * contained within the body tags + * + * @param {string} value + * @function + * @name setWysiwygEditorValue + * @memberOf jQuery.sceditor.prototype + */ + base.setWysiwygEditorValue = function (value) { + if (!value) { + value = '

' + (IE_VER ? '' : '
') + '

'; + } + + $wysiwygBody[0].innerHTML = value; + replaceEmoticons($wysiwygBody[0]); + + appendNewLine(); + triggerValueChanged(); + }; + + /** + * Sets the text editor value + * + * @param {string} value + * @function + * @name setSourceEditorValue + * @memberOf jQuery.sceditor.prototype + */ + base.setSourceEditorValue = function (value) { + $sourceEditor.val(value); + + triggerValueChanged(); + }; + + /** + * Updates the textarea that the editor is replacing + * with the value currently inside the editor. + * + * @function + * @name updateOriginal + * @since 1.4.0 + * @memberOf jQuery.sceditor.prototype + */ + base.updateOriginal = function () { + $original.val(base.val()); + }; + + /** + * Replaces any emoticon codes in the passed HTML + * with their emoticon images + * @private + */ + replaceEmoticons = function (node) { + // TODO: Make this tag configurable. + if (!options.emoticonsEnabled || $(node).parents('code').length) { + return; + } + + var doc = node.ownerDocument, + whitespace = '\\s|\xA0|\u2002|\u2003|\u2009| ', + emoticonCodes = [], + emoticonRegex = [], + emoticons = $.extend( + {}, + options.emoticons.more, + options.emoticons.dropdown, + options.emoticons.hidden + ); + // TODO: cache the emoticonCodes and emoticonCodes objects and share them with + // the AYT converstion + $.each(emoticons, function (key) { + if (options.emoticonsCompat) { + emoticonRegex[key] = new RegExp( + '(>|^|' + whitespace + ')' + + escape.regex(key) + + '($|<|' + whitespace + ')' + ); + } + + emoticonCodes.push(key); + }); + // TODO: tidy below + var convertEmoticons = function (node) { + node = node.firstChild; + + while (node) { + var parts, key, emoticon, parsedHtml, + emoticonIdx, nextSibling, matchPos, + nodeParent = node.parentNode, + nodeValue = node.nodeValue; + + // All none textnodes + if (node.nodeType !== 3) { + // TODO: Make this tag configurable. + if (!$(node).is('code')) { + convertEmoticons(node); + } + } else if (nodeValue) { + emoticonIdx = emoticonCodes.length; + while (emoticonIdx--) { + key = emoticonCodes[emoticonIdx]; + matchPos = options.emoticonsCompat ? + nodeValue.search(emoticonRegex[key]) : + nodeValue.indexOf(key); + + if (matchPos > -1) { + nextSibling = node.nextSibling; + emoticon = emoticons[key]; + parts = nodeValue + .substr(matchPos).split(key); + nodeValue = nodeValue + .substr(0, matchPos) + parts.shift(); + node.nodeValue = nodeValue; + + parsedHtml = dom.parseHTML(_tmpl('emoticon', { + key: key, + url: emoticon.url || emoticon, + tooltip: emoticon.tooltip || key + }), doc); + + nodeParent.insertBefore( + parsedHtml[0], + nextSibling + ); + + nodeParent.insertBefore( + doc.createTextNode(parts.join(key)), + nextSibling + ); + } + } + } + + node = node.nextSibling; + } + }; + + convertEmoticons(node); + + if (options.emoticonsCompat) { + currentEmoticons = $wysiwygBody + .find('img[data-sceditor-emoticon]'); + } + }; + + /** + * If the editor is in source code mode + * + * @return {bool} + * @function + * @name inSourceMode + * @memberOf jQuery.sceditor.prototype + */ + base.inSourceMode = function () { + return $editorContainer.hasClass('sourceMode'); + }; + + /** + * Gets if the editor is in sourceMode + * + * @return boolean + * @function + * @name sourceMode + * @memberOf jQuery.sceditor.prototype + */ + /** + * Sets if the editor is in sourceMode + * + * @param {bool} enable + * @return {this} + * @function + * @name sourceMode^2 + * @memberOf jQuery.sceditor.prototype + */ + base.sourceMode = function (enable) { + var inSourceMode = base.inSourceMode(); + + if (typeof enable !== 'boolean') { + return inSourceMode; + } + + if ((inSourceMode && !enable) || (!inSourceMode && enable)) { + base.toggleSourceMode(); + } + + return base; + }; + + /** + * Switches between the WYSIWYG and source modes + * + * @function + * @name toggleSourceMode + * @since 1.4.0 + * @memberOf jQuery.sceditor.prototype + */ + base.toggleSourceMode = function () { + var sourceMode = base.inSourceMode(); + + // don't allow switching to WYSIWYG if doesn't support it + if (!browser.isWysiwygSupported && sourceMode) { + return; + } + + if (!sourceMode) { + rangeHelper.saveRange(); + rangeHelper.clear(); + } + + base.blur(); + + if (sourceMode) { + base.setWysiwygEditorValue(base.getSourceEditorValue()); + } else { + base.setSourceEditorValue(base.getWysiwygEditorValue()); + } + + lastRange = null; + $sourceEditor.toggle(); + $wysiwygEditor.toggle(); + $editorContainer + .toggleClass('wysiwygMode', sourceMode) + .toggleClass('sourceMode', !sourceMode); + + updateToolBar(); + updateActiveButtons(); + }; + + /** + * Gets the selected text of the source editor + * @return {String} + * @private + */ + sourceEditorSelectedText = function () { + sourceEditor.focus(); + + if (typeof sourceEditor.selectionStart !== 'undefined') { + return sourceEditor.value.substring( + sourceEditor.selectionStart, + sourceEditor.selectionEnd + ); + } else { + return globalDoc.selection.createRange().text; + } + }; + + /** + * Handles the passed command + * @private + */ + handleCommand = function (caller, cmd) { + // check if in text mode and handle text commands + if (base.inSourceMode()) { + if (cmd.txtExec) { + if ($.isArray(cmd.txtExec)) { + base.sourceEditorInsertText.apply(base, cmd.txtExec); + } else { + cmd.txtExec.call( + base, + caller, + sourceEditorSelectedText() + ); + } + } + } else if (cmd.exec) { + if ($.isFunction(cmd.exec)) { + cmd.exec.call(base, caller); + } else { + base.execCommand( + cmd.exec, + cmd.hasOwnProperty('execParam') ? + cmd.execParam : 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 (IE_VER) { + lastRange = rangeHelper.selectedRange(); + } + }; + + /** + * Executes a command on the WYSIWYG editor + * + * @param {String} command + * @param {String|Boolean} [param] + * @function + * @name execCommand + * @memberOf jQuery.sceditor.prototype + */ + base.execCommand = function (command, param) { + var executed = false, + commandObj = base.commands[command], + $parentNode = $(rangeHelper.parentNode()); + + base.focus(); + + // TODO: make configurable + // don't apply any commands to code elements + if ($parentNode.is('code') || + $parentNode.parents('code').length !== 0) { + return; + } + + try { + executed = $wysiwygDoc[0].execCommand(command, false, param); + } catch (ex) {} + + // show error if execution failed and an error message exists + if (!executed && commandObj && commandObj.errorMessage) { + /*global alert:false*/ + alert(base._(commandObj.errorMessage)); + } + + updateActiveButtons(); + }; + + /** + * Checks if the current selection has changed and triggers + * the selectionchanged event if it has. + * + * In browsers other than IE, it will check at most once every 100ms. + * This is because only IE has a selection changed event. + * @private + */ + checkSelectionChanged = function () { + function check () { + // rangeHelper could be null if editor was destroyed + // before the timeout had finished + if (rangeHelper && !rangeHelper.compare(currentSelection)) { + currentSelection = rangeHelper.cloneSelected(); + $editorContainer.trigger($.Event('selectionchanged')); + } + + isSelectionCheckPending = false; + } + + if (isSelectionCheckPending) { + return; + } + + isSelectionCheckPending = true; + + // In IE, this is only called on the selectionchange event so no + // need to limit checking as it should always be valid to do. + if (IE_VER) { + check(); + } else { + setTimeout(check, 100); + } + }; + + /** + * Checks if the current node has changed and triggers + * the nodechanged event if it has + * @private + */ + checkNodeChanged = function () { + // check if node has changed + var oldNode, + node = rangeHelper.parentNode(); + + if (currentNode !== node) { + oldNode = currentNode; + currentNode = node; + currentBlockNode = rangeHelper.getFirstBlockParent(node); + + $editorContainer.trigger($.Event('nodechanged', { + oldNode: oldNode, + newNode: currentNode + })); + } + }; + + /** + *

Gets the current node that contains the selection/caret in + * WYSIWYG mode.

+ * + *

Will be null in sourceMode or if there is no selection.

+ * @return {Node} + * @function + * @name currentNode + * @memberOf jQuery.sceditor.prototype + */ + base.currentNode = function () { + return currentNode; + }; + + /** + *

Gets the first block level node that contains the + * selection/caret in WYSIWYG mode.

+ * + *

Will be null in sourceMode or if there is no selection.

+ * @return {Node} + * @function + * @name currentBlockNode + * @memberOf jQuery.sceditor.prototype + * @since 1.4.4 + */ + base.currentBlockNode = function () { + return currentBlockNode; + }; + + /** + * Updates if buttons are active or not + * @private + */ + updateActiveButtons = function (e) { + var firstBlock, parent; + var activeClass = 'active'; + var doc = $wysiwygDoc[0]; + var isSource = base.sourceMode(); + + if (base.readOnly()) { + $toolbar.find(activeClass).removeClass(activeClass); + return; + } + + if (!isSource) { + parent = e ? e.newNode : rangeHelper.parentNode(); + firstBlock = rangeHelper.getFirstBlockParent(parent); + } + + for (var i = 0; i < btnStateHandlers.length; i++) { + var state = 0; + var $btn = toolbarButtons[btnStateHandlers[i].name]; + var stateFn = btnStateHandlers[i].state; + var isDisabled = (isSource && !$btn.data('sceditor-txtmode')) || + (!isSource && !$btn.data('sceditor-wysiwygmode')); + + if (typeof stateFn === 'string') { + if (!isSource) { + try { + state = doc.queryCommandEnabled(stateFn) ? 0 : -1; + + /*jshint maxdepth: false*/ + if (state > -1) { + state = doc.queryCommandState(stateFn) ? 1 : 0; + } + } catch (ex) {} + } + } else if (!isDisabled) { + state = stateFn.call(base, parent, firstBlock); + } + + $btn + .toggleClass('disabled', isDisabled || state < 0) + .toggleClass(activeClass, state > 0); + } + }; + + /** + * Handles any key press in the WYSIWYG editor + * + * @private + */ + handleKeyPress = function (e) { + var $closestTag, br, brParent, lastChild; + + // TODO: improve this so isn't set list, probably should just use + // dom.hasStyling to all block parents and if one does insert a br + var DUPLICATED_TAGS = 'code,blockquote,pre'; + var LIST_TAGS = 'li,ul,ol'; + + // FF bug: https://bugzilla.mozilla.org/show_bug.cgi?id=501496 + if (e.originalEvent.defaultPrevented) { + return; + } + + base.closeDropDown(); + + $closestTag = $(currentBlockNode) + .closest(DUPLICATED_TAGS + ',' + LIST_TAGS) + .first(); + + // "Fix" (OK it's a cludge) for blocklevel elements being + // duplicated in some browsers when enter is pressed instead + // of inserting a newline + if (e.which === 13 && $closestTag.length && + !$closestTag.is(LIST_TAGS)) { + lastRange = null; + + br = $wysiwygDoc[0].createElement('br'); + rangeHelper.insertNode(br); + + // Last
of a block will be collapsed unless it is + // IE < 11 so need to make sure the
that was inserted + // isn't the last node of a block. + if (!IE_BR_FIX) { + brParent = br.parentNode; + lastChild = brParent.lastChild; + + // Sometimes an empty next node is created after the
+ if (lastChild && lastChild.nodeType === 3 && + lastChild.nodeValue === '') { + brParent.removeChild(lastChild); + lastChild = brParent.lastChild; + } + + // If this is the last BR of a block and the previous + // sibling is inline then will need an extra BR. This + // is needed because the last BR of a block will be + // collapsed. Fixes issue #248 + if (!dom.isInline(brParent, true) && lastChild === br && + dom.isInline(br.previousSibling)) { + rangeHelper.insertHTML('
'); + } + } + + return false; + } + }; + + /** + * Makes sure that if there is a code or quote tag at the + * end of the editor, that there is a new line after it. + * + * If there wasn't a new line at the end you wouldn't be able + * to enter any text after a code/quote tag + * @return {void} + * @private + */ + appendNewLine = function () { + var name, requiresNewLine, paragraph, + body = $wysiwygBody[0]; + + dom.rTraverse(body, function (node) { + name = node.nodeName.toLowerCase(); + // TODO: Replace requireNewLineFix with just a block level fix for any + // block that has styling and any block that isn't a plain

or

+ if ($.inArray(name, requireNewLineFix) > -1) { + requiresNewLine = true; + } + // TODO: tidy this up + // find the last non-empty text node or line break. + if ((node.nodeType === 3 && !/^\s*$/.test(node.nodeValue)) || + name === 'br' || (IE_BR_FIX && !node.firstChild && + !dom.isInline(node, false))) { + + // this is the last text or br node, if its in a code or + // quote tag then add a newline to the end of the editor + if (requiresNewLine) { + paragraph = $wysiwygDoc[0].createElement('p'); + paragraph.className = 'sceditor-nlf'; + paragraph.innerHTML = !IE_BR_FIX ? '
' : ''; + body.appendChild(paragraph); + } + + return false; + } + }); + }; + + /** + * Handles form reset event + * @private + */ + handleFormReset = function () { + base.val($original.val()); + }; + + /** + * Handles any mousedown press in the WYSIWYG editor + * @private + */ + handleMouseDown = function () { + base.closeDropDown(); + lastRange = null; + }; + + /** + * Handles the window resize event. Needed to resize then editor + * when the window size changes in fluid designs. + * @ignore + */ + handleWindowResize = function () { + var height = options.height, + width = options.width; + + if (!base.maximize()) { + if ((height && height.toString().indexOf('%') > -1) || + (width && width.toString().indexOf('%') > -1)) { + base.dimensions(width, height); + } + } else { + base.dimensions('100%', '100%', false); + } + }; + + /** + * Translates the string into the locale language. + * + * Replaces any {0}, {1}, {2}, ect. with the params provided. + * + * @param {string} str + * @param {...String} args + * @return {string} + * @function + * @name _ + * @memberOf jQuery.sceditor.prototype + */ + base._ = function () { + var undef, + args = arguments; + + if (locale && locale[args[0]]) { + args[0] = locale[args[0]]; + } + + return args[0].replace(/\{(\d+)\}/g, function (str, p1) { + return args[p1 - 0 + 1] !== undef ? + args[p1 - 0 + 1] : + '{' + p1 + '}'; + }); + }; + + /** + * Passes events on to any handlers + * @private + * @return void + */ + handleEvent = function (e) { + // Send event to all plugins + pluginManager.call(e.type + 'Event', e, base); + + // convert the event into a custom event to send + var prefix = e.target === sourceEditor ? 'scesrc' : 'scewys'; + var customEvent = $.Event(e); + customEvent.type = prefix + e.type; + + $editorContainer.trigger(customEvent, base); + }; + + /** + *

Binds a handler to the specified events

+ * + *

This function only binds to a limited list of + * supported events.
+ * The supported events are: + *

    + *
  • keyup
  • + *
  • keydown
  • + *
  • Keypress
  • + *
  • blur
  • + *
  • focus
  • + *
  • nodechanged
    + * When the current node containing the selection changes + * in WYSIWYG mode
  • + *
  • contextmenu
  • + *
  • selectionchanged
  • + *
  • valuechanged
  • + *
+ *

+ * + *

The events param should be a string containing the event(s) + * to bind this handler to. If multiple, they should be separated + * by spaces.

+ * + * @param {String} events + * @param {Function} handler + * @param {Boolean} excludeWysiwyg If to exclude adding this handler + * to the WYSIWYG editor + * @param {Boolean} excludeSource if to exclude adding this handler + * to the source editor + * @return {this} + * @function + * @name bind + * @memberOf jQuery.sceditor.prototype + * @since 1.4.1 + */ + base.bind = function (events, handler, excludeWysiwyg, excludeSource) { + events = events.split(' '); + + var i = events.length; + while (i--) { + if ($.isFunction(handler)) { + // Use custom events to allow passing the instance as the + // 2nd argument. + // Also allows unbinding without unbinding the editors own + // event handlers. + if (!excludeWysiwyg) { + $editorContainer.on('scewys' + events[i], handler); + } + + if (!excludeSource) { + $editorContainer.on('scesrc' + events[i], handler); + } + + // Start sending value changed events + if (events[i] === 'valuechanged') { + triggerValueChanged.hasHandler = true; + } + } + } + + return base; + }; + + /** + * Unbinds an event that was bound using bind(). + * + * @param {String} events + * @param {Function} handler + * @param {Boolean} excludeWysiwyg If to exclude unbinding this + * handler from the WYSIWYG editor + * @param {Boolean} excludeSource if to exclude unbinding this + * handler from the source editor + * @return {this} + * @function + * @name unbind + * @memberOf jQuery.sceditor.prototype + * @since 1.4.1 + * @see bind + */ + base.unbind = function ( + events, handler, excludeWysiwyg, excludeSource + ) { + events = events.split(' '); + + var i = events.length; + while (i--) { + if ($.isFunction(handler)) { + if (!excludeWysiwyg) { + $editorContainer.off('scewys' + events[i], handler); + } + + if (!excludeSource) { + $editorContainer.off('scesrc' + events[i], handler); + } + } + } + + return base; + }; + + /** + * Blurs the editors input area + * + * @return {this} + * @function + * @name blur + * @memberOf jQuery.sceditor.prototype + * @since 1.3.6 + */ + /** + * Adds a handler to the editors blur event + * + * @param {Function} handler + * @param {Boolean} excludeWysiwyg If to exclude adding this handler + * to the WYSIWYG editor + * @param {Boolean} excludeSource if to exclude adding this handler + * to the source editor + * @return {this} + * @function + * @name blur^2 + * @memberOf jQuery.sceditor.prototype + * @since 1.4.1 + */ + base.blur = function (handler, excludeWysiwyg, excludeSource) { + if ($.isFunction(handler)) { + base.bind('blur', handler, excludeWysiwyg, excludeSource); + } else if (!base.sourceMode()) { + $wysiwygBody.blur(); + } else { + $sourceEditor.blur(); + } + + return base; + }; + + /** + * Focuses the editors input area + * + * @return {this} + * @function + * @name focus + * @memberOf jQuery.sceditor.prototype + */ + /** + * Adds an event handler to the focus event + * + * @param {Function} handler + * @param {Boolean} excludeWysiwyg If to exclude adding this handler + * to the WYSIWYG editor + * @param {Boolean} excludeSource if to exclude adding this handler + * to the source editor + * @return {this} + * @function + * @name focus^2 + * @memberOf jQuery.sceditor.prototype + * @since 1.4.1 + */ + base.focus = function (handler, excludeWysiwyg, excludeSource) { + if ($.isFunction(handler)) { + base.bind('focus', handler, excludeWysiwyg, excludeSource); + } else if (!base.inSourceMode()) { + var container, + rng = rangeHelper.selectedRange(); + + // Fix FF bug where it shows the cursor in the wrong place + // if the editor hasn't had focus before. See issue #393 + if (!currentSelection && !rangeHelper.hasSelection()) { + autofocus(); + } + + // Check if cursor is set after a BR when the BR is the only + // child of the parent. In Firefox this causes a line break + // to occur when something is typed. See issue #321 + if (!IE_BR_FIX && rng && rng.endOffset === 1 && rng.collapsed) { + container = rng.endContainer; + + if (container && container.childNodes.length === 1 && + $(container.firstChild).is('br')) { + rng.setStartBefore(container.firstChild); + rng.collapse(true); + rangeHelper.selectRange(rng); + } + } + + wysiwygEditor.contentWindow.focus(); + $wysiwygBody[0].focus(); + + // Needed for IE < 9 + if (lastRange) { + rangeHelper.selectRange(lastRange); + + // remove the stored range after being set. + // If the editor loses focus it should be + // saved again. + lastRange = null; + } + } else { + sourceEditor.focus(); + } + + updateActiveButtons(); + + return base; + }; + + /** + * Adds a handler to the key down event + * + * @param {Function} handler + * @param {Boolean} excludeWysiwyg If to exclude adding this handler + * to the WYSIWYG editor + * @param {Boolean} excludeSource If to exclude adding this handler + * to the source editor + * @return {this} + * @function + * @name keyDown + * @memberOf jQuery.sceditor.prototype + * @since 1.4.1 + */ + base.keyDown = function (handler, excludeWysiwyg, excludeSource) { + return base.bind('keydown', handler, excludeWysiwyg, excludeSource); + }; + + /** + * Adds a handler to the key press event + * + * @param {Function} handler + * @param {Boolean} excludeWysiwyg If to exclude adding this handler + * to the WYSIWYG editor + * @param {Boolean} excludeSource If to exclude adding this handler + * to the source editor + * @return {this} + * @function + * @name keyPress + * @memberOf jQuery.sceditor.prototype + * @since 1.4.1 + */ + base.keyPress = function (handler, excludeWysiwyg, excludeSource) { + return base + .bind('keypress', handler, excludeWysiwyg, excludeSource); + }; + + /** + * Adds a handler to the key up event + * + * @param {Function} handler + * @param {Boolean} excludeWysiwyg If to exclude adding this handler + * to the WYSIWYG editor + * @param {Boolean} excludeSource If to exclude adding this handler + * to the source editor + * @return {this} + * @function + * @name keyUp + * @memberOf jQuery.sceditor.prototype + * @since 1.4.1 + */ + base.keyUp = function (handler, excludeWysiwyg, excludeSource) { + return base.bind('keyup', handler, excludeWysiwyg, excludeSource); + }; + + /** + *

Adds a handler to the node changed event.

+ * + *

Happens whenever the node containing the selection/caret + * changes in WYSIWYG mode.

+ * + * @param {Function} handler + * @return {this} + * @function + * @name nodeChanged + * @memberOf jQuery.sceditor.prototype + * @since 1.4.1 + */ + base.nodeChanged = function (handler) { + return base.bind('nodechanged', handler, false, true); + }; + + /** + *

Adds a handler to the selection changed event

+ * + *

Happens whenever the selection changes in WYSIWYG mode.

+ * + * @param {Function} handler + * @return {this} + * @function + * @name selectionChanged + * @memberOf jQuery.sceditor.prototype + * @since 1.4.1 + */ + base.selectionChanged = function (handler) { + return base.bind('selectionchanged', handler, false, true); + }; + + /** + *

Adds a handler to the value changed event

+ * + *

Happens whenever the current editor value changes.

+ * + *

Whenever anything is inserted, the value changed or + * 1.5 secs after text is typed. If a space is typed it will + * cause the event to be triggered immediately instead of + * after 1.5 seconds

+ * + * @param {Function} handler + * @param {Boolean} excludeWysiwyg If to exclude adding this handler + * to the WYSIWYG editor + * @param {Boolean} excludeSource If to exclude adding this handler + * to the source editor + * @return {this} + * @function + * @name valueChanged + * @memberOf jQuery.sceditor.prototype + * @since 1.4.5 + */ + base.valueChanged = function (handler, excludeWysiwyg, excludeSource) { + return base + .bind('valuechanged', handler, excludeWysiwyg, excludeSource); + }; + + /** + * Emoticons keypress handler + * @private + */ + emoticonsKeyPress = function (e) { + var replacedEmoticon, + cachePos = 0, + emoticonsCache = base.emoticonsCache, + curChar = String.fromCharCode(e.which); + // TODO: Make configurable + if ($(currentBlockNode).is('code') || + $(currentBlockNode).parents('code').length) { + return; + } + + if (!emoticonsCache) { + emoticonsCache = []; + + $.each($.extend( + {}, + options.emoticons.more, + options.emoticons.dropdown, + options.emoticons.hidden + ), function (key, url) { + emoticonsCache[cachePos++] = [ + key, + _tmpl('emoticon', { + key: key, + url: url.url || url, + tooltip: url.tooltip || key + }) + ]; + }); + + emoticonsCache.sort(function (a, b) { + return a[0].length - b[0].length; + }); + + base.emoticonsCache = emoticonsCache; + base.longestEmoticonCode = + emoticonsCache[emoticonsCache.length - 1][0].length; + } + + replacedEmoticon = rangeHelper.replaceKeyword( + base.emoticonsCache, + true, + true, + base.longestEmoticonCode, + options.emoticonsCompat, + curChar + ); + + if (replacedEmoticon && options.emoticonsCompat) { + currentEmoticons = $wysiwygBody + .find('img[data-sceditor-emoticon]'); + + return /^\s$/.test(curChar); + } + + return !replacedEmoticon; + }; + + /** + * Makes sure emoticons are surrounded by whitespace + * @private + */ + emoticonsCheckWhitespace = function () { + if (!currentEmoticons.length) { + return; + } + + var prev, next, parent, range, previousText, rangeStartContainer, + currentBlock = base.currentBlockNode(), + rangeStart = false, + noneWsRegex = /[^\s\xA0\u2002\u2003\u2009\u00a0]+/; + + currentEmoticons = $.map(currentEmoticons, function (emoticon) { + // Ignore emoticons that have been removed from DOM + if (!emoticon || !emoticon.parentNode) { + return null; + } + + if (!$.contains(currentBlock, emoticon)) { + return emoticon; + } + + prev = emoticon.previousSibling; + next = emoticon.nextSibling; + previousText = prev.nodeValue; + + // For IE's HTMLPhraseElement + if (previousText === null) { + previousText = prev.innerText || ''; + } + + if ((!prev || !noneWsRegex.test(prev.nodeValue.slice(-1))) && + (!next || !noneWsRegex.test((next.nodeValue || '')[0]))) { + return emoticon; + } + + parent = emoticon.parentNode; + range = rangeHelper.cloneSelected(); + rangeStartContainer = range.startContainer; + previousText = previousText + + $(emoticon).data('sceditor-emoticon'); + + // Store current caret position + if (rangeStartContainer === next) { + rangeStart = previousText.length + range.startOffset; + } else if (rangeStartContainer === currentBlock && + currentBlock.childNodes[range.startOffset] === next) { + rangeStart = previousText.length; + } else if (rangeStartContainer === prev) { + rangeStart = range.startOffset; + } + + if (!next || next.nodeType !== 3) { + next = parent.insertBefore( + parent.ownerDocument.createTextNode(''), next + ); + } + + next.insertData(0, previousText); + parent.removeChild(prev); + parent.removeChild(emoticon); + + // Need to update the range starting + // position if it has been modified + if (rangeStart !== false) { + range.setStart(next, rangeStart); + range.collapse(true); + rangeHelper.selectRange(range); + } + + return null; + }); + }; + + /** + * Gets if emoticons are currently enabled + * @return {boolean} + * @function + * @name emoticons + * @memberOf jQuery.sceditor.prototype + * @since 1.4.2 + */ + /** + * Enables/disables emoticons + * + * @param {boolean} enable + * @return {this} + * @function + * @name emoticons^2 + * @memberOf jQuery.sceditor.prototype + * @since 1.4.2 + */ + base.emoticons = function (enable) { + if (!enable && enable !== false) { + return options.emoticonsEnabled; + } + + options.emoticonsEnabled = enable; + + if (enable) { + $wysiwygBody.keypress(emoticonsKeyPress); + + if (!base.sourceMode()) { + rangeHelper.saveRange(); + + replaceEmoticons($wysiwygBody[0]); + currentEmoticons = $wysiwygBody + .find('img[data-sceditor-emoticon]'); + triggerValueChanged(false); + + rangeHelper.restoreRange(); + } + } else { + $wysiwygBody + .find('img[data-sceditor-emoticon]') + .replaceWith(function () { + return $(this).data('sceditor-emoticon'); + }); + + currentEmoticons = []; + $wysiwygBody.off('keypress', emoticonsKeyPress); + + triggerValueChanged(); + } + + return base; + }; + + /** + * Gets the current WYSIWYG editors inline CSS + * + * @return {string} + * @function + * @name css + * @memberOf jQuery.sceditor.prototype + * @since 1.4.3 + */ + /** + * Sets inline CSS for the WYSIWYG editor + * + * @param {string} css + * @return {this} + * @function + * @name css^2 + * @memberOf jQuery.sceditor.prototype + * @since 1.4.3 + */ + base.css = function (css) { + if (!inlineCss) { + inlineCss = $('' + + '' + + '' + + '' + + '

' + + '', + + toolbarButton: '' + + '
{dispName}
', + + emoticon: '', + + 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} name + * @param {Object} params + * @param {Boolean} createHtml + * @private + */ + return 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; + }; + }.call(exports, __webpack_require__, exports, module), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__)); + + +/***/ }, +/* 9 */ +/***/ function(module, exports, __webpack_require__) { + + var __WEBPACK_AMD_DEFINE_RESULT__;!(__WEBPACK_AMD_DEFINE_RESULT__ = function (require) { + 'use strict'; + + var $ = __webpack_require__(1); + var IE_VER = __webpack_require__(6).ie; + var _tmpl = __webpack_require__(8); + + // In IE < 11 a BR at the end of a block level element + // causes a line break. In all other browsers it's collapsed. + var IE_BR_FIX = IE_VER && IE_VER < 11; + + + /** + * Map of all the commands for SCEditor + * @type {Object} + * @name commands + * @memberOf jQuery.sceditor + */ + var defaultCommnds = { + // START_COMMAND: Bold + bold: { + exec: 'bold', + tooltip: 'Bold', + shortcut: 'Ctrl+B' + }, + // END_COMMAND + // START_COMMAND: Italic + italic: { + exec: 'italic', + tooltip: 'Italic', + shortcut: 'Ctrl+I' + }, + // END_COMMAND + // START_COMMAND: Underline + underline: { + exec: 'underline', + tooltip: 'Underline', + shortcut: 'Ctrl+U' + }, + // 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: { + _dropDown: function (editor, caller, callback) { + var fontIdx = 0, + fonts = editor.opts.fonts.split(','), + content = $('
'), + /** @private */ + clickFunc = function () { + callback($(this).data('font')); + editor.closeDropDown(true); + return false; + }; + + for (; fontIdx < fonts.length; fontIdx++) { + content.append( + _tmpl('fontOpt', { + font: fonts[fontIdx] + }, true).click(clickFunc) + ); + } + + editor.createDropDown(caller, 'font-picker', content); + }, + exec: function (caller) { + var editor = this; + + defaultCommnds.font._dropDown( + editor, + caller, + function (fontName) { + editor.execCommand('fontname', fontName); + } + ); + }, + tooltip: 'Font Name' + }, + // END_COMMAND + // START_COMMAND: Size + size: { + _dropDown: function (editor, caller, callback) { + var content = $('
'), + /** @private */ + clickFunc = function (e) { + callback($(this).data('size')); + editor.closeDropDown(true); + e.preventDefault(); + }; + + for (var i = 1; i <= 7; i++) { + content.append(_tmpl('sizeOpt', { + size: i + }, true).click(clickFunc)); + } + + editor.createDropDown(caller, 'fontsize-picker', content); + }, + exec: function (caller) { + var editor = this; + + defaultCommnds.size._dropDown( + editor, + caller, + function (fontSize) { + editor.execCommand('fontsize', fontSize); + } + ); + }, + tooltip: 'Font Size' + }, + // END_COMMAND + // START_COMMAND: Colour + color: { + _dropDown: function (editor, caller, callback) { + var i, x, color, colors, + genColor = {r: 255, g: 255, b: 255}, + content = $('
'), + colorColumns = editor.opts.colors ? + editor.opts.colors.split('|') : new Array(21), + // IE is slow at string concatenation so use an array + html = [], + cmd = defaultCommnds.color; + + if (!cmd._htmlCache) { + for (i = 0; i < colorColumns.length; ++i) { + colors = colorColumns[i] ? + colorColumns[i].split(',') : new Array(21); + + html.push('
'); + + for (x = 0; x < colors.length; ++x) { + // use pre defined colour if can otherwise use the + // generated color + color = colors[x] || '#' + + genColor.r.toString(16) + + genColor.g.toString(16) + + genColor.b.toString(16); + + html.push( + '' + ); + + if (x % 5 === 0) { + genColor.g -= 51; + genColor.b = 255; + } else { + genColor.b -= 51; + } + } + + html.push('
'); + + if (i % 5 === 0) { + genColor.r -= 51; + genColor.g = 255; + genColor.b = 255; + } else { + genColor.g = 255; + genColor.b = 255; + } + } + + cmd._htmlCache = html.join(''); + } + + content.append(cmd._htmlCache) + .find('a') + .click(function (e) { + callback($(this).attr('data-color')); + editor.closeDropDown(true); + e.preventDefault(); + }); + + editor.createDropDown(caller, 'color-picker', content); + }, + exec: function (caller) { + var editor = this; + + defaultCommnds.color._dropDown( + editor, + caller, + function (color) { + editor.execCommand('forecolor', color); + } + ); + }, + 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 val, content, + editor = this; + + content = _tmpl('pastetext', { + label: editor._( + 'Paste your text inside the following box:' + ), + insert: editor._('Insert') + }, true); + + content.find('.button').click(function (e) { + val = content.find('#txt').val(); + + if (val) { + editor.wysiwygEditorInsertText(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: Indent + indent: { + state: function (parents, firstBlock) { + // Only works with lists, for now + // This is a nested list, so it will always work + var range, startParent, endParent, + $firstBlock = $(firstBlock), + parentLists = $firstBlock.parents('ul,ol,menu'), + parentList = parentLists.first(); + + // in case it's a list with only a single
  • + if (parentLists.length > 1 || + parentList.children().length > 1) { + return 0; + } + + if ($firstBlock.is('ul,ol,menu')) { + // if the whole list is selected, then this must be + // invalidated because the browser will place a + //
    there + range = this.getRangeHelper().selectedRange(); + + if (window.Range && range instanceof Range) { + startParent = range.startContainer.parentNode; + endParent = range.endContainer.parentNode; + + // TODO: could use nodeType for this? + // Maybe just check the firstBlock contains both the start and end containers + // Select the tag, not the textNode + // (that's why the parentNode) + if (startParent !== + startParent.parentNode.firstElementChild || + // work around a bug in FF + ($(endParent).is('li') && endParent !== + endParent.parentNode.lastElementChild)) { + return 0; + } + // it's IE... As it is impossible to know well when to + // accept, better safe than sorry + } else { + return $firstBlock.is('li,ul,ol,menu') ? 0 : -1; + } + } + + return -1; + }, + exec: function () { + var editor = this, + $elm = $(editor.getRangeHelper().getFirstBlockParent()); + + editor.focus(); + + // An indent system is quite complicated as there are loads + // of complications and issues around how to indent text + // As default, let's just stay with indenting the lists, + // at least, for now. + if ($elm.parents('ul,ol,menu')) { + editor.execCommand('indent'); + } + }, + tooltip: 'Add indent' + }, + // END_COMMAND + // START_COMMAND: Outdent + outdent: { + state: function (parents, firstBlock) { + return $(firstBlock).is('ul,ol,menu') || + $(firstBlock).parents('ul,ol,menu').length > 0 ? 0 : -1; + }, + exec: function () { + var editor = this, + $elm = $(editor.getRangeHelper().getFirstBlockParent()); + + if ($elm.parents('ul,ol,menu')) { + editor.execCommand('outdent'); + } + }, + tooltip: 'Remove one indent' + }, + // END_COMMAND + + // START_COMMAND: Table + table: { + forceNewLineAfter: ['table'], + exec: function (caller) { + var editor = this, + content = _tmpl('table', { + rows: editor._('Rows:'), + cols: editor._('Cols:'), + insert: editor._('Insert') + }, true); + + content.find('.button').click(function (e) { + var row, col, + rows = content.find('#rows').val() - 0, + cols = content.find('#cols').val() - 0, + html = ''; + + if (rows < 1 || cols < 1) { + return; + } + + for (row = 0; row < rows; row++) { + html += ''; + + for (col = 0; col < cols; col++) { + html += ''; + } + + html += ''; + } + + html += '
    ' + + (IE_BR_FIX ? '' : '
    ') + + '
    '; + + 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: { + forceNewLineAfter: ['code'], + exec: function () { + this.wysiwygEditorInsertHtml( + '', + (IE_BR_FIX ? '' : '
    ') + '
    ' + ); + }, + 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(), + width = content.find('#width').val(), + height = content.find('#height').val(), + attrs = ''; + + if (width) { + attrs += ' width="' + width + '"'; + } + + if (height) { + attrs += ' height="' + height + '"'; + } + + if (val) { + 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:'), + desc: editor._('Description (optional):'), + insert: editor._('Insert') + }, true); + + content.find('.button').click(function (e) { + var val = content.find('#email').val(), + description = content.find('#des').val(); + + if (val) { + // needed for IE to reset the last range + editor.focus(); + + if (!editor.getRangeHelper().selectedHtml() || + description) { + description = description || val; + + editor.wysiwygEditorInsertHtml( + '' + + description + + '' + ); + } 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) { + // needed for IE to restore the last range + editor.focus(); + + // If there is no selected text then must set the URL as + // the text. Most browsers do this automatically, sadly + // IE doesn't. + if (!editor.getRangeHelper().selectedHtml() || + description) { + 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: { + state: function () { + var $current = $(this.currentNode()); + return $current.is('a') || + $current.parents('a').length > 0 ? 0 : -1; + }, + exec: function () { + var $current = $(this.currentNode()), + $anchor = $current.is('a') ? $current : + $current.parents('a').first(); + + if ($anchor.length) { + $anchor.replaceWith($anchor.contents()); + } + }, + tooltip: 'Unlink' + }, + // END_COMMAND + + + // START_COMMAND: Quote + quote: { + forceNewLineAfter: ['blockquote'], + 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; + end = null; + // if not add a newline to the end of the inserted quote + } else if (this.getRangeHelper().selectedHtml() === '') { + end = (IE_BR_FIX ? '' : '
    ') + end; + } + + this.wysiwygEditorInsertHtml(before, end); + }, + tooltip: 'Insert a Quote' + }, + // END_COMMAND + + // START_COMMAND: Emoticons + emoticon: { + exec: function (caller) { + var editor = this; + + var createContent = function (includeMore) { + var $moreLink, + emoticonsCompat = editor.opts.emoticonsCompat, + rangeHelper = editor.getRangeHelper(), + startSpace = emoticonsCompat && + rangeHelper.getOuterText(true, 1) !== ' ' ? + ' ' : '', + endSpace = emoticonsCompat && + rangeHelper.getOuterText(false, 1) !== ' ' ? + ' ' : '', + $content = $('
    '), + $line = $('
    ').appendTo($content), + perLine = 0, + emoticons = $.extend( + {}, + editor.opts.emoticons.dropdown, + includeMore ? editor.opts.emoticons.more : {} + ); + + $.each(emoticons, function () { + perLine++; + }); + perLine = Math.sqrt(perLine); + + $.each(emoticons, function (code, emoticon) { + $line.append( + $('').attr({ + src: emoticon.url || emoticon, + alt: code, + title: emoticon.tooltip || code + }).click(function () { + editor.insert(startSpace + $(this).attr('alt') + + endSpace, null, false).closeDropDown(true); + + return false; + }) + ); + + if ($line.children().length >= perLine) { + $line = $('
    ').appendTo($content); + } + }); + + if (!includeMore && editor.opts.emoticons.more) { + $moreLink = $( + '' + + editor._('More') + '' + ).click(function () { + editor.createDropDown( + caller, + 'more-emoticons', + createContent(true) + ); + + return false; + }); + + $content.append($moreLink); + } + + return $content; + }; + + editor.createDropDown( + caller, + 'emoticons', + createContent(false) + ); + }, + txtExec: function (caller) { + defaultCommnds.emoticon.exec.call(this, caller); + }, + tooltip: 'Insert an emoticon' + }, + // END_COMMAND + + // START_COMMAND: YouTube + youtube: { + _dropDown: function (editor, caller, handleIdFunc) { + var matches, + content = _tmpl('youtubeMenu', { + label: editor._('Video URL:'), + insert: editor._('Insert') + }, true); + + content.find('.button').click(function (e) { + var val = content + .find('#link') + .val(); + + if (val) { + matches = val.match( + /(?:v=|v\/|embed\/|youtu.be\/)(.{11})/ + ); + + if (matches) { + val = matches[1]; + } + + if (/^[a-zA-Z0-9_\-]{11}$/.test(val)) { + handleIdFunc(val); + } else { + /*global alert:false*/ + alert('Invalid YouTube video'); + } + } + + editor.closeDropDown(true); + e.preventDefault(); + }); + + editor.createDropDown(caller, 'insertlink', content); + }, + exec: function (caller) { + var editor = this; + + defaultCommnds.youtube._dropDown( + editor, + caller, + function (id) { + editor.wysiwygEditorInsertHtml(_tmpl('youtube', { + id: id + })); + } + ); + }, + tooltip: 'Insert a YouTube video' + }, + // END_COMMAND + + // START_COMMAND: Date + date: { + _date: function (editor) { + 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; + } + + return editor.opts.dateFormat + .replace(/year/i, year) + .replace(/month/i, month) + .replace(/day/i, day); + }, + exec: function () { + this.insertText(defaultCommnds.date._date(this)); + }, + txtExec: function () { + this.insertText(defaultCommnds.date._date(this)); + }, + tooltip: 'Insert current date' + }, + // END_COMMAND + + // START_COMMAND: Time + time: { + _time: 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; + } + + return hours + ':' + mins + ':' + secs; + }, + exec: function () { + this.insertText(defaultCommnds.time._time()); + }, + txtExec: function () { + this.insertText(defaultCommnds.time._time()); + }, + tooltip: 'Insert current time' + }, + // END_COMMAND + + + // START_COMMAND: Ltr + ltr: { + state: function (parents, firstBlock) { + return firstBlock && firstBlock.style.direction === 'ltr'; + }, + exec: function () { + var editor = this, + elm = editor.getRangeHelper().getFirstBlockParent(), + $elm = $(elm); + + editor.focus(); + + if (!elm || $elm.is('body')) { + editor.execCommand('formatBlock', 'p'); + + elm = editor.getRangeHelper().getFirstBlockParent(); + $elm = $(elm); + + if (!elm || $elm.is('body')) { + return; + } + } + + if ($elm.css('direction') === 'ltr') { + $elm.css('direction', ''); + } else { + $elm.css('direction', 'ltr'); + } + }, + tooltip: 'Left-to-Right' + }, + // END_COMMAND + + // START_COMMAND: Rtl + rtl: { + state: function (parents, firstBlock) { + return firstBlock && firstBlock.style.direction === 'rtl'; + }, + exec: function () { + var editor = this, + elm = editor.getRangeHelper().getFirstBlockParent(), + $elm = $(elm); + + editor.focus(); + + if (!elm || $elm.is('body')) { + editor.execCommand('formatBlock', 'p'); + + elm = editor.getRangeHelper().getFirstBlockParent(); + $elm = $(elm); + + if (!elm || $elm.is('body')) { + return; + } + } + + if ($elm.css('direction') === 'rtl') { + $elm.css('direction', ''); + } else { + $elm.css('direction', 'rtl'); + } + }, + tooltip: 'Right-to-Left' + }, + // END_COMMAND + + + // START_COMMAND: Print + print: { + exec: 'print', + tooltip: 'Print' + }, + // END_COMMAND + + // START_COMMAND: Maximize + maximize: { + state: function () { + return this.maximize(); + }, + exec: function () { + this.maximize(!this.maximize()); + }, + txtExec: function () { + this.maximize(!this.maximize()); + }, + tooltip: 'Maximize', + shortcut: 'Ctrl+Shift+M' + }, + // END_COMMAND + + // START_COMMAND: Source + source: { + state: function () { + return this.sourceMode(); + }, + exec: function () { + this.toggleSourceMode(); + }, + txtExec: function () { + this.toggleSourceMode(); + }, + tooltip: 'View source', + shortcut: 'Ctrl+Shift+S' + }, + // 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: {} + }; + + return defaultCommnds; + }.call(exports, __webpack_require__, exports, module), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__)); + + +/***/ }, +/* 10 */ +/***/ function(module, exports, __webpack_require__) { + + var __WEBPACK_AMD_DEFINE_RESULT__;!(__WEBPACK_AMD_DEFINE_RESULT__ = function (require) { + 'use strict'; + + var $ = __webpack_require__(1); + + + /** + * Default options for SCEditor + * @type {Object} + */ + return { + /** @lends jQuery.sceditor.defaultOptions */ + /** + * Toolbar buttons order and groups. Should be comma separated and + * have a bar | to separate groups + * + * @type {String} + */ + toolbar: 'bold,italic,underline,strike,subscript,superscript|' + + 'left,center,right,justify|font,size,color,removeformat|' + + 'cut,copy,paste,pastetext|bulletlist,orderedlist,indent,outdent|' + + 'table|code,quote|horizontalrule,image,email,link,unlink|' + + 'emoticon,youtube,date,time|ltr,rtl|print,maximize,source', + + /** + * Comma separated list of commands to excludes from the toolbar + * + * @type {String} + */ + toolbarExclude: null, + + /** + * Stylesheet to include in the WYSIWYG editor. This is what will style + * the WYSIWYG elements + * + * @type {String} + */ + style: 'jquery.sceditor.default.css', + + /** + * Comma separated list of fonts for the font selector + * + * @type {String} + */ + fonts: 'Arial,Arial Black,Comic Sans MS,Courier New,Georgia,Impact,' + + 'Sans-serif,Serif,Times New Roman,Trebuchet MS,Verdana', + + /** + * Colors should be comma separated and have a bar | to signal a new + * column. + * + * If null the colors will be auto generated. + * + * @type {string} + */ + colors: null, + + /** + * The locale to use. + * @type {String} + */ + locale: $('html').attr('lang') || 'en', + + /** + * The Charset to use + * @type {String} + */ + charset: 'utf-8', + + /** + * Compatibility mode for emoticons. + * + * Helps if you have emoticons such as :/ which would put an emoticon + * inside http:// + * + * This mode requires emoticons to be surrounded by whitespace or end of + * line chars. This mode has limited As You Type emoticon conversion + * support. It will not replace AYT for end of line chars, only + * emoticons surrounded by whitespace. They will still be replaced + * correctly when loaded just not AYT. + * + * @type {Boolean} + */ + emoticonsCompat: false, + + /** + * If to enable emoticons. Can be changes at runtime using the + * emoticons() method. + * + * @type {Boolean} + * @since 1.4.2 + */ + emoticonsEnabled: true, + + /** + * Emoticon root URL + * + * @type {String} + */ + 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 + * + * @type {int} + */ + width: null, + + /** + * Height of the editor including toolbar. Set to null for automatic + * height + * + * @type {int} + */ + height: null, + + /** + * If to allow the editor to be resized + * + * @type {Boolean} + */ + resizeEnabled: true, + + /** + * Min resize to width, set to null for half textarea width or -1 for + * unlimited + * + * @type {int} + */ + resizeMinWidth: null, + /** + * Min resize to height, set to null for half textarea height or -1 for + * unlimited + * + * @type {int} + */ + resizeMinHeight: null, + /** + * Max resize to height, set to null for double textarea height or -1 + * for unlimited + * + * @type {int} + */ + resizeMaxHeight: null, + /** + * Max resize to width, set to null for double textarea width or -1 for + * unlimited + * + * @type {int} + */ + resizeMaxWidth: null, + /** + * If resizing by height is enabled + * + * @type {Boolean} + */ + resizeHeight: true, + /** + * If resizing by width is enabled + * + * @type {Boolean} + */ + resizeWidth: true, + + /** + * Date format, will be overridden if locale specifies one. + * + * The words year, month and day will be replaced with the users current + * year, month and day. + * + * @type {String} + */ + dateFormat: 'year-month-day', + + /** + * Element to inset the toolbar into. + * + * @type {HTMLElement} + */ + toolbarContainer: null, + + /** + * If to enable paste filtering. This is currently experimental, please + * report any issues. + * + * @type {Boolean} + */ + enablePasteFiltering: false, + + /** + * If to completely disable pasting into the editor + * + * @type {Boolean} + */ + disablePasting: false, + + /** + * If the editor is read only. + * + * @type {Boolean} + */ + readOnly: false, + + /** + * If to set the editor to right-to-left mode. + * + * If set to null the direction will be automatically detected. + * + * @type {Boolean} + */ + rtl: false, + + /** + * If to auto focus the editor on page load + * + * @type {Boolean} + */ + autofocus: false, + + /** + * If to auto focus the editor to the end of the content + * + * @type {Boolean} + */ + autofocusEnd: true, + + /** + * If to auto expand the editor to fix the content + * + * @type {Boolean} + */ + autoExpand: false, + + /** + * If to auto update original textbox on blur + * + * @type {Boolean} + */ + autoUpdate: false, + + /** + * If to enable the browsers built in spell checker + * + * @type {Boolean} + */ + spellcheck: true, + + /** + * If to run the source editor when there is no WYSIWYG support. Only + * really applies to mobile OS's. + * + * @type {Boolean} + */ + runWithoutWysiwygSupport: false, + + /** + * If to load the editor in source mode and still allow switching + * between WYSIWYG and source mode + * + * @type {Boolean} + */ + startInSourceMode: false, + + /** + * Optional ID to give the editor. + * + * @type {String} + */ + id: null, + + /** + * Comma separated list of plugins + * + * @type {String} + */ + plugins: '', + + /** + * z-index to set the editor container to. Needed for jQuery UI dialog. + * + * @type {Int} + */ + zIndex: null, + + /** + * If to trim the BBCode. Removes any spaces at the start and end of the + * BBCode string. + * + * @type {Boolean} + */ + bbcodeTrim: false, + + /** + * If to disable removing block level elements by pressing backspace at + * the start of them + * + * @type {Boolean} + */ + disableBlockRemove: false, + + /** + * BBCode parser options, only applies if using the editor in BBCode + * mode. + * + * See SCEditor.BBCodeParser.defaults for list of valid options + * + * @type {Object} + */ + parserOptions: { }, + + /** + * CSS that will be added to the to dropdown menu (eg. z-index) + * + * @type {Object} + */ + dropDownCss: { } + }; + }.call(exports, __webpack_require__, exports, module), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__)); + + +/***/ } +/******/ ]);;/** + * SCEditor XHTML Plugin + * http://www.sceditor.com/ + * + * Copyright (C) 2011-2013, Sam Clarke (samclarke.com) + * + * SCEditor is licensed under the MIT license: + * http://www.opensource.org/licenses/mit-license.php + * + * @author Sam Clarke + * @requires jQuery + */ +/*global prompt: true*/ +(function ($) { + 'use strict'; + + var SCEditor = $.sceditor; + var sceditorPlugins = SCEditor.plugins; + var dom = SCEditor.dom; + + var defaultCommandsOverrides = { + bold: { + txtExec: [ + '', + '' + ] + }, + italic: { + txtExec: [ + '', + '' + ] + }, + underline: { + txtExec: [ + '', + '' + ] + }, + strike: { + txtExec: [ + '', + '' + ] + }, + subscript: { + txtExec: [ + '', + '' + ] + }, + superscript: { + txtExec: [ + '', + '' + ] + }, + left: { + txtExec: [ + '
    ', + '
    ' + ] + }, + center: { + txtExec: [ + '
    ', + '
    ' + ] + }, + right: { + txtExec: [ + '
    ', + '
    ' + ] + }, + justify: { + txtExec: [ + '
    ', + '
    ' + ] + }, + font: { + txtExec: function (caller) { + var editor = this; + + SCEditor.command.get('font')._dropDown( + editor, + caller, + function (fontName) { + editor.insertText('', ''); + } + ); + } + }, + size: { + txtExec: function (caller) { + var editor = this; + + SCEditor.command.get('size')._dropDown( + editor, + caller, + function (fontSize) { + editor.insertText('', ''); + } + ); + } + }, + color: { + txtExec: function (caller) { + var editor = this; + + SCEditor.command.get('color')._dropDown( + editor, + caller, + function (color) { + editor.insertText('', ''); + } + ); + } + }, + bulletlist: { + txtExec: [ + '
    • ', + '
    ' + ] + }, + orderedlist: { + txtExec: [ + '
    1. ', + '
    ' + ] + }, + table: { + txtExec: [ + '
    ', + '
    ' + ] + }, + horizontalrule: { + txtExec: [ + '
    ' + ] + }, + code: { + txtExec: [ + '', + '' + ] + }, + image: { + txtExec: function (caller, selected) { + var url = prompt(this._('Enter the image URL:'), selected); + + if (url) { + this.insertText(''); + } + } + }, + email: { + txtExec: function (caller, sel) { + var email, text, + display = sel && sel.indexOf('@') > -1 ? null : sel; + + email = prompt( + this._('Enter the e-mail address:'), + (display ? '' : sel) + ); + + text = prompt( + this._('Enter the displayed text:'), + display || email + ) || email; + + if (email) { + this.insertText( + '' + text + '' + ); + } + } + }, + link: { + txtExec: function (caller, sel) { + var display = sel && sel.indexOf('http://') > -1 ? null : sel, + url = prompt(this._('Enter URL:'), + (display ? 'http://' : sel)), + text = prompt(this._('Enter the displayed text:'), + display || url) || url; + + if (url) { + this.insertText( + '' + text + '' + ); + } + } + }, + quote: { + txtExec: [ + '
    ', + '
    ' + ] + }, + youtube: { + txtExec: function (caller) { + var editor = this; + + SCEditor.command.get('youtube')._dropDown( + editor, + caller, + function (id) { + editor.insertText( + '' + ); + } + ); + } + }, + rtl: { + txtExec: [ + '
    ', + '
    ' + ] + }, + ltr: { + txtExec: [ + '
    ', + '
    ' + ] + } + }; + + /** + * XHTMLSerializer part of the XHTML plugin. + * + * @class XHTMLSerializer + * @name jQuery.sceditor.XHTMLSerializer + * @since v1.4.1 + */ + SCEditor.XHTMLSerializer = function () { + var base = this; + + var opts = { + indentStr: '\t' + }; + + /** + * Array containing the output, used as it's faster + * than string concatenation in slow browsers. + * @type {Array} + * @private + */ + var outputStringBuilder = []; + + /** + * Current indention level + * @type {Number} + * @private + */ + var currentIndent = 0; + + /** + * @private + */ + var escapeEntites, + trim, + serializeNode, + handleDoc, + handleElement, + handleCdata, + handleComment, + handleText, + output, + canIndent; +// TODO: use escape.entities + /** + * Escapes XHTML entities + * + * @param {String} str + * @return {String} + * @private + */ + escapeEntites = function (str) { + var entites = { + '&': '&', + '<': '<', + '>': '>', + '"': '"' + }; + + return !str ? '' : str.replace(/[&<>"]/g, function (entity) { + return entites[entity] || entity; + }); + }; + + /** + * @param {string} str + * @return {string} + * @private + */ + trim = function (str) { + return str + // New lines will be shown as spaces so just convert to spaces. + .replace(/[\r\n]/, ' ') + .replace(/[^\S|\u00A0]+/g, ' '); + }; + + /** + * Serializes a node to XHTML + * + * @param {Node} node Node to serialize + * @param {Boolean} onlyChildren If to only serialize the nodes + * children and not the node + * itself + * @return {String} The serialized node + * @name serialize + * @memberOf jQuery.sceditor.XHTMLSerializer.prototype + * @since v1.4.1 + */ + base.serialize = function (node, onlyChildren) { + outputStringBuilder = []; + + if (onlyChildren) { + node = node.firstChild; + + while (node) { + serializeNode(node); + node = node.nextSibling; + } + } else { + serializeNode(node); + } + + return outputStringBuilder.join(''); + }; + + /** + * Serializes a node to the outputStringBuilder + * + * @param {Node} node + * @return {Void} + * @private + */ + serializeNode = function (node, parentIsPre) { + switch (node.nodeType) { + case 1: // element + var tagName = node.nodeName.toLowerCase(); + + // IE comment + if (tagName === '!') { + handleComment(node); + } else { + handleElement(node, parentIsPre); + } + break; + + case 3: // text + handleText(node, parentIsPre); + break; + + case 4: // cdata section + handleCdata(node); + break; + + case 8: // comment + handleComment(node); + break; + + case 9: // document + case 11: // document fragment + handleDoc(node); + break; + + // Ignored types + case 2: // attribute + case 5: // entity ref + case 6: // entity + case 7: // processing instruction + case 10: // document type + case 12: // notation + break; + } + }; + + /** + * Handles doc node + * @param {Node} node + * @return {void} + * @private + */ + handleDoc = function (node) { + var child = node.firstChild; + + while (child) { + serializeNode(child); + child = child.nextSibling; + } + }; + + /** + * Handles element nodes + * @param {Node} node + * @return {void} + * @private + */ + handleElement = function (node, parentIsPre) { + var child, attr, attrValue, + tagName = node.nodeName.toLowerCase(), + isIframe = tagName === 'iframe', + attrIdx = node.attributes.length, + firstChild = node.firstChild, + // pre || pre-wrap with any vendor prefix + isPre = parentIsPre || + /pre(?:\-wrap)?$/i.test($(node).css('whiteSpace')), + selfClosing = !node.firstChild && !dom.canHaveChildren(node) && + !isIframe; + + if ($(node).hasClass('sceditor-ignore')) { + return; + } + + output('<' + tagName, !parentIsPre && canIndent(node)); + while (attrIdx--) { + attr = node.attributes[attrIdx]; + + // IE < 8 returns all possible attributes not just specified + // ones. IE < 8 also doesn't say value on input is specified + // so just assume it is. + if (!SCEditor.ie || attr.specified || + (tagName === 'input' && attr.name === 'value')) { + // IE < 8 doesn't return the CSS for the style attribute + if (SCEditor.ie < 8 && /style/i.test(attr.name)) { + attrValue = node.style.cssText; + } else { + attrValue = attr.value; + } + + output(' ' + attr.name.toLowerCase() + '="' + + escapeEntites(attrValue) + '"', false); + } + } + output(selfClosing ? ' />' : '>', false); + + if (!isIframe) { + child = firstChild; + } + + while (child) { + currentIndent++; + + serializeNode(child, isPre); + child = child.nextSibling; + + currentIndent--; + } + + if (!selfClosing) { + output( + '', + !isPre && !isIframe && canIndent(node) && + firstChild && canIndent(firstChild) + ); + } + }; + + /** + * Handles CDATA nodes + * @param {Node} node + * @return {void} + * @private + */ + handleCdata = function (node) { + output(''); + }; + + /** + * Handles comment nodes + * @param {Node} node + * @return {void} + * @private + */ + handleComment = function (node) { + output(''); + }; + + /** + * Handles text nodes + * @param {Node} node + * @return {void} + * @private + */ + handleText = function (node, parentIsPre) { + var text = node.nodeValue; + + if (!parentIsPre) { + text = trim(text); + } + + if (text) { + output(escapeEntites(text), !parentIsPre && canIndent(node)); + } + }; + + /** + * Adds a string to the outputStringBuilder. + * + * The string will be indented unless indent is set to boolean false. + * @param {String} str + * @param {Boolean} indent + * @return {void} + * @private + */ + output = function (str, indent) { + var i = currentIndent; + + if (indent !== false) { + // Don't add a new line if it's the first element + if (outputStringBuilder.length) { + outputStringBuilder.push('\n'); + } + + while (i--) { + outputStringBuilder.push(opts.indentStr); + } + } + + outputStringBuilder.push(str); + }; + + /** + * Checks if should indent the node or not + * @param {Node} node + * @return {boolean} + * @private + */ + canIndent = function (node) { + var prev = node.previousSibling; + + if (node.nodeType !== 1 && prev) { + return !dom.isInline(prev); + } + + // first child of a block element + if (!prev && !dom.isInline(node.parentNode)) { + return true; + } + + return !dom.isInline(node); + }; + }; + + /** + * SCEditor XHTML plugin + * @class xhtml + * @name jQuery.sceditor.plugins.xhtml + * @since v1.4.1 + */ + sceditorPlugins.xhtml = function () { + var base = this; + + /** + * Tag converstions cache + * @type {Object} + * @private + */ + var tagConvertersCache = {}; + + /** + * Attributes filter cache + * @type {Object} + * @private + */ + var attrsCache = {}; + + /** + * Private methods + * @private + */ + var convertTags, + convertNode, + isEmpty, + removeTags, + mergeAttribsFilters, + removeAttribs, + wrapInlines; + + + /** + * Init + * @return {void} + */ + base.init = function () { + if (!$.isEmptyObject(sceditorPlugins.xhtml.converters || {})) { + $.each( + sceditorPlugins.xhtml.converters, + function (idx, converter) { + $.each(converter.tags, function (tagname) { + if (!tagConvertersCache[tagname]) { + tagConvertersCache[tagname] = []; + } + + tagConvertersCache[tagname].push(converter); + }); + } + ); + } + + this.commands = $.extend(true, + {}, defaultCommandsOverrides, this.commands); + }; + + /** + * Converts the WYSIWYG content to XHTML + * @param {String} html + * @param {Node} domBody + * @return {String} + * @memberOf jQuery.sceditor.plugins.xhtml.prototype + */ + base.signalToSource = function (html, domBody) { + domBody = domBody.jquery ? domBody[0] : domBody; + + convertTags(domBody); + removeTags(domBody); + removeAttribs(domBody); + wrapInlines(domBody); + + return (new SCEditor.XHTMLSerializer()).serialize(domBody, true); + }; + + /** + * Converts the XHTML to WYSIWYG content. + * + * This doesn't currently do anything as XHTML + * is valid WYSIWYG content. + * @param {String} text + * @return {String} + * @memberOf jQuery.sceditor.plugins.xhtml.prototype + */ + base.signalToWysiwyg = function (text) { + return text; + }; + + /** + * Deprecated, use dom.convertElement() instead. + * @deprecated + */ + base.convertTagTo = dom.convertElement; + + /** + * Runs all converters for the specified tagName + * against the DOM node. + * @param {String} tagName + * @param {jQuery} $node + * @return {Node} node + * @private + */ + convertNode = function (tagName, $node, node) { + if (!tagConvertersCache[tagName]) { + return; + } + + $.each(tagConvertersCache[tagName], function (idx, converter) { + if (converter.tags[tagName]) { + $.each(converter.tags[tagName], function (attr, values) { + if (!node.getAttributeNode) { + return; + } + + attr = node.getAttributeNode(attr); + + // IE < 8 always returns an attribute regardless of if + // it has been specified so must check it. + if (!attr || (SCEditor.ie < 8 && !attr.specified)) { + return; + } + + if (values && $.inArray(attr.value, values) < 0) { + return; + } + + converter.conv.call(base, node, $node); + }); + } else if (converter.conv) { + converter.conv.call(base, node, $node); + } + }); + }; + + /** + * Converts any tags/attributes to their XHTML equivalents + * @param {Node} node + * @return {Void} + * @private + */ + convertTags = function (node) { + dom.traverse(node, function (node) { + var $node = $(node), + tagName = node.nodeName.toLowerCase(); + + convertNode('*', $node, node); + convertNode(tagName, $node, node); + }, true); + }; + + /** + * Tests if a node is empty and can be removed. + * + * @param {Node} node + * @return {Boolean} + * @private + */ + isEmpty = function (node, excludeBr) { + var childNodes = node.childNodes, + tagName = node.nodeName.toLowerCase(), + nodeValue = node.nodeValue, + childrenLength = childNodes.length; + + if (excludeBr && tagName === 'br') { + return true; + } + + if ($(node).hasClass('sceditor-ignore')) { + return true; + } + + if (!dom.canHaveChildren(node)) { + return false; + } + + // \S|\u00A0 = any non space char + if (nodeValue && /\S|\u00A0/.test(nodeValue)) { + return false; + } + + while (childrenLength--) { + if (!isEmpty(childNodes[childrenLength], + excludeBr && !node.previousSibling && !node.nextSibling)) { + return false; + } + } + + return true; + }; + + /** + * Removes any tags that are not white listed or if no + * tags are white listed it will remove any tags that + * are black listed. + * + * @param {Node} rootNode + * @return {Void} + * @private + */ + removeTags = function (rootNode) { + dom.traverse(rootNode, function (node) { + var remove, + tagName = node.nodeName.toLowerCase(), + parentNode = node.parentNode, + nodeType = node.nodeType, + isBlock = !dom.isInline(node), + previousSibling = node.previousSibling, + nextSibling = node.nextSibling, + isTopLevel = parentNode === rootNode, + noSiblings = !previousSibling && !nextSibling, + empty = tagName !== 'iframe' && isEmpty(node, + isTopLevel && noSiblings && tagName !== 'br'), + document = node.ownerDocument, + allowedtags = sceditorPlugins.xhtml.allowedTags, + disallowedTags = sceditorPlugins.xhtml.disallowedTags; + + // 3 = text node + if (nodeType === 3) { + return; + } + + if (nodeType === 4) { + tagName = '!cdata'; + } else if (tagName === '!' || nodeType === 8) { + tagName = '!comment'; + } + + if (empty) { + remove = true; + // 3 is text node which do not get filtered + } else if (allowedtags && allowedtags.length) { + remove = ($.inArray(tagName, allowedtags) < 0); + } else if (disallowedTags && disallowedTags.length) { + remove = ($.inArray(tagName, disallowedTags) > -1); + } + + if (remove) { + if (!empty) { + if (isBlock && previousSibling && + dom.isInline(previousSibling)) { + parentNode.insertBefore( + document.createTextNode(' '), node); + } + + // Insert all the childen after node + while (node.firstChild) { + parentNode.insertBefore(node.firstChild, + nextSibling); + } + + if (isBlock && nextSibling && + dom.isInline(nextSibling)) { + parentNode.insertBefore( + document.createTextNode(' '), nextSibling); + } + } + + parentNode.removeChild(node); + } + }, true); + }; + + /** + * Merges two sets of attribute filters into one + * + * @param {Object} filtersA + * @param {Object} filtersB + * @return {Object} + * @private + */ + mergeAttribsFilters = function (filtersA, filtersB) { + var ret = {}; + + if (filtersA) { + $.extend(ret, filtersA); + } + + if (!filtersB) { + return ret; + } + + $.each(filtersB, function (attrName, values) { + if ($.isArray(values)) { + ret[attrName] = $.merge(ret[attrName] || [], values); + } else if (!ret[attrName]) { + ret[attrName] = null; + } + }); + + return ret; + }; + + /** + * Wraps adjacent inline child nodes of root + * in paragraphs. + * + * @param {Node} root + * @private + */ + wrapInlines = function (root) { + var adjacentInlines = []; + var wrapAdjacents = function () { + if (adjacentInlines.length) { + $('

    ', root.ownerDocument) + .insertBefore(adjacentInlines[0]) + .append(adjacentInlines); + + adjacentInlines = []; + } + }; + + // Strip empty text nodes so they don't get wrapped. + dom.removeWhiteSpace(root); + + var node = root.firstChild; + while (node) { + if (dom.isInline(node) && !$(node).is('.sceditor-ignore')) { + adjacentInlines.push(node); + } else { + wrapAdjacents(); + } + + node = node.nextSibling; + } + + wrapAdjacents(); + }; + + /** + * Removes any attributes that are not white listed or + * if no attributes are white listed it will remove + * any attributes that are black listed. + * @param {Node} node + * @return {Void} + * @private + */ + removeAttribs = function (node) { + var tagName, attr, attrName, attrsLength, validValues, remove, + allowedAttribs = sceditorPlugins.xhtml.allowedAttribs, + isAllowed = allowedAttribs && + !$.isEmptyObject(allowedAttribs), + disallowedAttribs = sceditorPlugins.xhtml.disallowedAttribs, + isDisallowed = disallowedAttribs && + !$.isEmptyObject(disallowedAttribs); + + attrsCache = {}; + + dom.traverse(node, function (node) { + if (!node.attributes) { + return; + } + + tagName = node.nodeName.toLowerCase(); + attrsLength = node.attributes.length; + + if (attrsLength) { + if (!attrsCache[tagName]) { + if (isAllowed) { + attrsCache[tagName] = mergeAttribsFilters( + allowedAttribs['*'], + allowedAttribs[tagName] + ); + } else { + attrsCache[tagName] = mergeAttribsFilters( + disallowedAttribs['*'], + disallowedAttribs[tagName] + ); + } + } + + while (attrsLength--) { + attr = node.attributes[attrsLength]; + attrName = attr.name; + validValues = attrsCache[tagName][attrName]; + remove = false; + + if (isAllowed) { + remove = validValues !== null && + (!$.isArray(validValues) || + $.inArray(attr.value, validValues) < 0); + } else if (isDisallowed) { + remove = validValues === null || + ($.isArray(validValues) && + $.inArray(attr.value, validValues) > -1); + } + + if (remove) { + node.removeAttribute(attrName); + } + } + } + }); + }; + }; + + /** + * Tag conveters, a converter is applied to all + * tags that match the criteria. + * @type {Array} + * @name jQuery.sceditor.plugins.xhtml.converters + * @since v1.4.1 + */ + sceditorPlugins.xhtml.converters = [ + { + tags: { + '*': { + width: null + } + }, + conv: function (node, $node) { + $node.css('width', $node.attr('width')).removeAttr('width'); + } + }, + { + tags: { + '*': { + height: null + } + }, + conv: function (node, $node) { + $node.css('height', $node.attr('height')).removeAttr('height'); + } + }, + { + tags: { + 'li': { + value: null + } + }, + conv: function (node, $node) { + if (SCEditor.ie < 8) { + node.removeAttribute('value'); + } else { + $node.removeAttr('value'); + } + } + }, + { + tags: { + '*': { + text: null + } + }, + conv: function (node, $node) { + $node.css('color', $node.attr('text')).removeAttr('text'); + } + }, + { + tags: { + '*': { + color: null + } + }, + conv: function (node, $node) { + $node.css('color', $node.attr('color')).removeAttr('color'); + } + }, + { + tags: { + '*': { + face: null + } + }, + conv: function (node, $node) { + $node.css('fontFamily', $node.attr('face')).removeAttr('face'); + } + }, + { + tags: { + '*': { + align: null + } + }, + conv: function (node, $node) { + $node.css('textAlign', $node.attr('align')).removeAttr('align'); + } + }, + { + tags: { + '*': { + border: null + } + }, + conv: function (node, $node) { + $node + .css('borderWidth',$node.attr('border')) + .removeAttr('border'); + } + }, + { + tags: { + applet: { + name: null + }, + img: { + name: null + }, + layer: { + name: null + }, + map: { + name: null + }, + object: { + name: null + }, + param: { + name: null + } + }, + conv: function (node, $node) { + if (!$node.attr('id')) { + $node.attr('id', $node.attr('name')); + } + + $node.removeAttr('name'); + } + }, + { + tags: { + '*': { + vspace: null + } + }, + conv: function (node, $node) { + $node + .css('marginTop', $node.attr('vspace') - 0) + .css('marginBottom', $node.attr('vspace') - 0) + .removeAttr('vspace'); + } + }, + { + tags: { + '*': { + hspace: null + } + }, + conv: function (node, $node) { + $node + .css('marginLeft', $node.attr('hspace') - 0) + .css('marginRight', $node.attr('hspace') - 0) + .removeAttr('hspace'); + } + }, + { + tags: { + 'hr': { + noshade: null + } + }, + conv: function (node, $node) { + $node.css('borderStyle', 'solid').removeAttr('noshade'); + } + }, + { + tags: { + '*': { + nowrap: null + } + }, + conv: function (node, $node) { + $node.css('white-space', 'nowrap').removeAttr('nowrap'); + } + }, + { + tags: { + big: null + }, + conv: function (node) { + $(this.convertTagTo(node, 'span')).css('fontSize', 'larger'); + } + }, + { + tags: { + small: null + }, + conv: function (node) { + $(this.convertTagTo(node, 'span')).css('fontSize', 'smaller'); + } + }, + { + tags: { + b: null + }, + conv: function (node) { + $(this.convertTagTo(node, 'strong')); + } + }, + { + tags: { + u: null + }, + conv: function (node) { + $(this.convertTagTo(node, 'span')) + .css('textDecoration', 'underline'); + } + }, + { + tags: { + i: null + }, + conv: function (node) { + $(this.convertTagTo(node, 'em')); + } + }, + { + tags: { + s: null, + strike: null + }, + conv: function (node) { + $(this.convertTagTo(node, 'span')) + .css('textDecoration', 'line-through'); + } + }, + { + tags: { + dir: null + }, + conv: function (node) { + this.convertTagTo(node, 'ul'); + } + }, + { + tags: { + center: null + }, + conv: function (node) { + $(this.convertTagTo(node, 'div')) + .css('textAlign', 'center'); + } + }, + { + tags: { + font: { + size: null + } + }, + conv: function (node, $node) { + var size = $node.css('fontSize'), + fontSize = size; + + // IE < 8 sets a font tag with no size to +0 so + // should just skip it. + if (fontSize !== '+0') { + // IE 8 and below incorrectly returns the value of the size + // attribute instead of the px value so must convert it + if (SCEditor.ie < 9) { + fontSize = 10; + + if (size > 1) { + fontSize = 13; + } + if (size > 2) { + fontSize = 16; + } + if (size > 3) { + fontSize = 18; + } + if (size > 4) { + fontSize = 24; + } + if (size > 5) { + fontSize = 32; + } + if (size > 6) { + fontSize = 48; + } + } + + $node.css('fontSize', fontSize); + } + + $node.removeAttr('size'); + } + }, + { + tags: { + font: null + }, + conv: function (node) { + // All it's attributes will be converted + // by the attribute converters + this.convertTagTo(node, 'span'); + } + }, + { + tags: { + '*': { + type: ['_moz'] + } + }, + conv: function (node, $node) { + $node.removeAttr('type'); + } + }, + { + tags: { + '*': { + '_moz_dirty': null + } + }, + conv: function (node, $node) { + $node.removeAttr('_moz_dirty'); + } + }, + { + tags: { + '*': { + '_moz_editor_bogus_node': null + } + }, + conv: function (node, $node) { + $node.remove(); + } + } + ]; + + /** + * Allowed attributes map. + * + * To allow an attribute for all tags use * as the tag name. + * + * Leave empty or null to allow all attributes. (the disallow + * list will be used to filter them instead) + * @type {Object} + * @name jQuery.sceditor.plugins.xhtml.allowedAttribs + * @since v1.4.1 + */ + sceditorPlugins.xhtml.allowedAttribs = {}; + + /** + * Attributes that are not allowed. + * + * Only used if allowed attributes is null or empty. + * @type {Object} + * @name jQuery.sceditor.plugins.xhtml.disallowedAttribs + * @since v1.4.1 + */ + sceditorPlugins.xhtml.disallowedAttribs = {}; + + /** + * Array containing all the allowed tags. + * + * If null or empty all tags will be allowed. + * @type {Array} + * @name jQuery.sceditor.plugins.xhtml.allowedTags + * @since v1.4.1 + */ + sceditorPlugins.xhtml.allowedTags = []; + + /** + * Array containing all the disallowed tags. + * + * Only used if allowed tags is null or empty. + * @type {Array} + * @name jQuery.sceditor.plugins.xhtml.disallowedTags + * @since v1.4.1 + */ + sceditorPlugins.xhtml.disallowedTags = []; +}(jQuery)); -- cgit v1.3