diff options
Diffstat (limited to 'js/sceditor/development/jquery.sceditor.js')
| -rw-r--r-- | js/sceditor/development/jquery.sceditor.js | 7230 |
1 files changed, 7230 insertions, 0 deletions
diff --git a/js/sceditor/development/jquery.sceditor.js b/js/sceditor/development/jquery.sceditor.js new file mode 100644 index 00000000..551afcd4 --- /dev/null +++ b/js/sceditor/development/jquery.sceditor.js @@ -0,0 +1,7230 @@ +/******/ (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 = $('<div class="sceditor-container" />') + .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 = $('<textarea></textarea>'); + $wysiwygEditor = $( + '<iframe frameborder="0" allowfullscreen="true"></iframe>' + ); + + /* 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 = $('<div class="sceditor-toolbar" unselectable="on" />'); + + $.each(groups, function (idx, group) { + $group = $('<div class="sceditor-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 = $('<div class="sceditor-grip" />'), + // Cover is used to cover the editor iframe so document + // still gets mouse move events + $cover = $('<div class="sceditor-resize-cover" />'), + 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 <br> 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} + */ + /** + * <p>Sets the width and/or height of the editor.</p> + * + * <p>If width or height is not numeric it is ignored.</p> + * + * @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} + */ + /** + * <p>Sets the width and/or height of the editor.</p> + * + * <p>If width or height is not numeric it is ignored.</p> + * + * <p>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.</p> + * + * @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 = $('<div class="sceditor-dropdown ' + cssClass + '" />') + .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; + }; + + + /** + * <p>Inserts HTML into WYSIWYG editor.</p> + * + * <p>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.</p> + * + * @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) + ); + }; + + /** + * <p>Inserts text into the WYSIWYG or source editor depending on which + * mode the editor is in.</p> + * + * <p>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.</p> + * + * @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; + }; + + /** + * <p>Like wysiwygEditorInsertHtml but inserts text into the + * source mode editor instead.</p> + * + * <p>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.</p> + * + * <p>The cursor will be placed after the text param. If endText is + * specified the cursor will be placed before endText, so passing:<br /> + * + * '[b]', '[/b]'</p> + * + * <p>Would cause the cursor to be placed:<br /> + * + * [b]Selected text|[/b]</p> + * + * @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; + }; + + /** + * <p>Gets the value of the editor.</p> + * + * <p>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).</p> + * + * @since 1.3.5 + * @return {string} + * @function + * @name val + * @memberOf jQuery.sceditor.prototype + */ + /** + * <p>Sets the value of the editor.</p> + * + * <p>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.</p> + * + * @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; + }; + + /** + * <p>Inserts HTML/BBCode into the editor</p> + * + * <p>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.</p> + * + * <p>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.</p> + * + * @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 + */ + /** + * <p>Inserts HTML/BBCode into the editor</p> + * + * <p>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.</p> + * + * <p>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.</p> + * + * <p>If the allowMixed param is set to true, HTML any will not be + * escaped</p> + * + * @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 = $('<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 = $('<div>') + .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 = '<p>' + (IE_VER ? '' : '<br />') + '</p>'; + } + + $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 + })); + } + }; + + /** + * <p>Gets the current node that contains the selection/caret in + * WYSIWYG mode.</p> + * + * <p>Will be null in sourceMode or if there is no selection.</p> + * @return {Node} + * @function + * @name currentNode + * @memberOf jQuery.sceditor.prototype + */ + base.currentNode = function () { + return currentNode; + }; + + /** + * <p>Gets the first block level node that contains the + * selection/caret in WYSIWYG mode.</p> + * + * <p>Will be null in sourceMode or if there is no selection.</p> + * @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 <br> of a block will be collapsed unless it is + // IE < 11 so need to make sure the <br> 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 <br> + 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('<br>'); + } + } + + 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 <p> or <div> + 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 ? '<br />' : ''; + 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); + }; + + /** + * <p>Binds a handler to the specified events</p> + * + * <p>This function only binds to a limited list of + * supported events.<br /> + * The supported events are: + * <ul> + * <li>keyup</li> + * <li>keydown</li> + * <li>Keypress</li> + * <li>blur</li> + * <li>focus</li> + * <li>nodechanged<br /> + * When the current node containing the selection changes + * in WYSIWYG mode</li> + * <li>contextmenu</li> + * <li>selectionchanged</li> + * <li>valuechanged</li> + * </ul> + * </p> + * + * <p>The events param should be a string containing the event(s) + * to bind this handler to. If multiple, they should be separated + * by spaces.</p> + * + * @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); + }; + + /** + * <p>Adds a handler to the node changed event.</p> + * + * <p>Happens whenever the node containing the selection/caret + * changes in WYSIWYG mode.</p> + * + * @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); + }; + + /** + * <p>Adds a handler to the selection changed event</p> + * + * <p>Happens whenever the selection changes in WYSIWYG mode.</p> + * + * @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); + }; + + /** + * <p>Adds a handler to the value changed event</p> + * + * <p>Happens whenever the current editor value changes.</p> + * + * <p>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</p> + * + * @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 = $('<style id="#inline" />', $wysiwygDoc[0]) + .appendTo($wysiwygDoc.find('head'))[0]; + } + + if (typeof css !== 'string') { + return inlineCss.styleSheet ? + inlineCss.styleSheet.cssText : inlineCss.innerHTML; + } + + if (inlineCss.styleSheet) { + inlineCss.styleSheet.cssText = css; + } else { + inlineCss.innerHTML = css; + } + + return base; + }; + + /** + * Handles the keydown event, used for shortcuts + * @private + */ + handleKeyDown = function (e) { + var shortcut = [], + SHIFT_KEYS = { + '`': '~', + '1': '!', + '2': '@', + '3': '#', + '4': '$', + '5': '%', + '6': '^', + '7': '&', + '8': '*', + '9': '(', + '0': ')', + '-': '_', + '=': '+', + ';': ': ', + '\'': '"', + ',': '<', + '.': '>', + '/': '?', + '\\': '|', + '[': '{', + ']': '}' + }, + SPECIAL_KEYS = { + 8: 'backspace', + 9: 'tab', + 13: 'enter', + 19: 'pause', + 20: 'capslock', + 27: 'esc', + 32: 'space', + 33: 'pageup', + 34: 'pagedown', + 35: 'end', + 36: 'home', + 37: 'left', + 38: 'up', + 39: 'right', + 40: 'down', + 45: 'insert', + 46: 'del', + 91: 'win', + 92: 'win', + 93: 'select', + 96: '0', + 97: '1', + 98: '2', + 99: '3', + 100: '4', + 101: '5', + 102: '6', + 103: '7', + 104: '8', + 105: '9', + 106: '*', + 107: '+', + 109: '-', + 110: '.', + 111: '/', + 112: 'f1', + 113: 'f2', + 114: 'f3', + 115: 'f4', + 116: 'f5', + 117: 'f6', + 118: 'f7', + 119: 'f8', + 120: 'f9', + 121: 'f10', + 122: 'f11', + 123: 'f12', + 144: 'numlock', + 145: 'scrolllock', + 186: ';', + 187: '=', + 188: ',', + 189: '-', + 190: '.', + 191: '/', + 192: '`', + 219: '[', + 220: '\\', + 221: ']', + 222: '\'' + }, + NUMPAD_SHIFT_KEYS = { + 109: '-', + 110: 'del', + 111: '/', + 96: '0', + 97: '1', + 98: '2', + 99: '3', + 100: '4', + 101: '5', + 102: '6', + 103: '7', + 104: '8', + 105: '9' + }, + which = e.which, + character = SPECIAL_KEYS[which] || + String.fromCharCode(which).toLowerCase(); + + if (e.ctrlKey || e.metaKey) { + shortcut.push('ctrl'); + } + + if (e.altKey) { + shortcut.push('alt'); + } + + if (e.shiftKey) { + shortcut.push('shift'); + + if (NUMPAD_SHIFT_KEYS[which]) { + character = NUMPAD_SHIFT_KEYS[which]; + } else if (SHIFT_KEYS[character]) { + character = SHIFT_KEYS[character]; + } + } + + // Shift is 16, ctrl is 17 and alt is 18 + if (character && (which < 16 || which > 18)) { + shortcut.push(character); + } + + shortcut = shortcut.join('+'); + if (shortcutHandlers[shortcut]) { + return shortcutHandlers[shortcut].call(base); + } + }; + + /** + * Adds a shortcut handler to the editor + * @param {String} shortcut + * @param {String|Function} cmd + * @return {jQuery.sceditor} + */ + base.addShortcut = function (shortcut, cmd) { + shortcut = shortcut.toLowerCase(); + + if (typeof cmd === 'string') { + shortcutHandlers[shortcut] = function () { + handleCommand( + toolbarButtons[cmd], + base.commands[cmd] + ); + + return false; + }; + } else { + shortcutHandlers[shortcut] = cmd; + } + + return base; + }; + + /** + * Removes a shortcut handler + * @param {String} shortcut + * @return {jQuery.sceditor} + */ + base.removeShortcut = function (shortcut) { + delete shortcutHandlers[shortcut.toLowerCase()]; + + return base; + }; + + /** + * Handles the backspace key press + * + * Will remove block styling like quotes/code ect if at the start. + * @private + */ + handleBackSpace = function (e) { + var node, offset, tmpRange, range, parent; + + // 8 is the backspace key + if (options.disableBlockRemove || e.which !== 8 || + !(range = rangeHelper.selectedRange())) { + return; + } + + if (!globalWin.getSelection) { + node = range.parentElement(); + tmpRange = $wysiwygDoc[0].selection.createRange(); + + // Select the entire parent and set the end + // as start of the current range + tmpRange.moveToElementText(node); + tmpRange.setEndPoint('EndToStart', range); + + // Number of characters selected is the start offset + // relative to the parent node + offset = tmpRange.text.length; + } else { + node = range.startContainer; + offset = range.startOffset; + } + + if (offset !== 0 || !(parent = currentStyledBlockNode())) { + return; + } + + while (node !== parent) { + while (node.previousSibling) { + node = node.previousSibling; + + // Everything but empty text nodes before the cursor + // should prevent the style from being removed + if (node.nodeType !== 3 || node.nodeValue) { + return; + } + } + + if (!(node = node.parentNode)) { + return; + } + } + + if (!parent || $(parent).is('body')) { + return; + } + + // The backspace was pressed at the start of + // the container so clear the style + base.clearBlockFormatting(parent); + return false; + }; + + /** + * Gets the first styled block node that contains the cursor + * @return {HTMLElement} + */ + currentStyledBlockNode = function () { + var block = currentBlockNode; + + while (!dom.hasStyling(block) || dom.isInline(block, true)) { + if (!(block = block.parentNode) || $(block).is('body')) { + return; + } + } + + return block; + }; + + /** + * Clears the formatting of the passed block element. + * + * If block is false, if will clear the styling of the first + * block level element that contains the cursor. + * @param {HTMLElement} block + * @since 1.4.4 + */ + base.clearBlockFormatting = function (block) { + block = block || currentStyledBlockNode(); + + if (!block || $(block).is('body')) { + return base; + } + + rangeHelper.saveRange(); + + block.className = ''; + lastRange = null; + + $(block).attr('style', ''); + + if (!$(block).is('p,div,td')) { + dom.convertElement(block, 'p'); + } + + rangeHelper.restoreRange(); + return base; + }; + + /** + * Triggers the valueChanged signal if there is + * a plugin that handles it. + * + * If rangeHelper.saveRange() has already been + * called, then saveRange should be set to false + * to prevent the range being saved twice. + * + * @since 1.4.5 + * @param {Boolean} saveRange If to call rangeHelper.saveRange(). + * @private + */ + triggerValueChanged = function (saveRange) { + if (!pluginManager || + (!pluginManager.hasHandler('valuechangedEvent') && + !triggerValueChanged.hasHandler)) { + return; + } + + var currentHtml, + sourceMode = base.sourceMode(), + hasSelection = !sourceMode && rangeHelper.hasSelection(); + + // Don't need to save the range if sceditor-start-marker + // is present as the range is already saved + saveRange = saveRange !== false && + !$wysiwygDoc[0].getElementById('sceditor-start-marker'); + + // Clear any current timeout as it's now been triggered + if (valueChangedKeyUp.timer) { + clearTimeout(valueChangedKeyUp.timer); + valueChangedKeyUp.timer = false; + } + + if (hasSelection && saveRange) { + rangeHelper.saveRange(); + } + + currentHtml = sourceMode ? + $sourceEditor.val() : + $wysiwygBody.html(); + + // Only trigger if something has actually changed. + if (currentHtml !== triggerValueChanged.lastHtmlValue) { + triggerValueChanged.lastHtmlValue = currentHtml; + + $editorContainer.trigger($.Event('valuechanged', { + rawValue: sourceMode ? base.val() : currentHtml + })); + } + + if (hasSelection && saveRange) { + rangeHelper.removeMarkers(); + } + }; + + /** + * Should be called whenever there is a blur event + * @private + */ + valueChangedBlur = function () { + if (valueChangedKeyUp.timer) { + triggerValueChanged(); + } + }; + + /** + * Should be called whenever there is a keypress event + * @param {Event} e The keypress event + * @private + */ + valueChangedKeyUp = function (e) { + var which = e.which, + lastChar = valueChangedKeyUp.lastChar, + lastWasSpace = (lastChar === 13 || lastChar === 32), + lastWasDelete = (lastChar === 8 || lastChar === 46); + + valueChangedKeyUp.lastChar = which; + + // 13 = return & 32 = space + if (which === 13 || which === 32) { + if (!lastWasSpace) { + triggerValueChanged(); + } else { + valueChangedKeyUp.triggerNextChar = true; + } + // 8 = backspace & 46 = del + } else if (which === 8 || which === 46) { + if (!lastWasDelete) { + triggerValueChanged(); + } else { + valueChangedKeyUp.triggerNextChar = true; + } + } else if (valueChangedKeyUp.triggerNextChar) { + triggerValueChanged(); + valueChangedKeyUp.triggerNextChar = false; + } + + // Clear the previous timeout and set a new one. + if (valueChangedKeyUp.timer) { + clearTimeout(valueChangedKeyUp.timer); + } + + // Trigger the event 1.5s after the last keypress if space + // isn't pressed. This might need to be lowered, will need + // to look into what the slowest average Chars Per Min is. + valueChangedKeyUp.timer = setTimeout(function () { + triggerValueChanged(); + }, 1500); + }; + + autoUpdate = function () { + if (!autoUpdateCanceled) { + base.updateOriginal(); + } + + autoUpdateCanceled = false; + }; + + // run the initializer + init(); + }; + + + /** + * Map containing the loaded SCEditor locales + * @type {Object} + * @name locale + * @memberOf jQuery.sceditor + */ + SCEditor.locale = {}; + + + /** + * Static command helper class + * @class command + * @name jQuery.sceditor.command + */ + SCEditor.command = + /** @lends jQuery.sceditor.command */ + { + /** + * Gets a command + * + * @param {String} name + * @return {Object|null} + * @since v1.3.5 + */ + get: function (name) { + return SCEditor.commands[name] || null; + }, + + /** + * <p>Adds a command to the editor or updates an existing + * command if a command with the specified name already exists.</p> + * + * <p>Once a command is add it can be included in the toolbar by + * adding it's name to the toolbar option in the constructor. It + * can also be executed manually by calling + * {@link jQuery.sceditor.execCommand}</p> + * + * @example + * SCEditor.command.set("hello", + * { + * exec: function () { + * alert("Hello World!"); + * } + * }); + * + * @param {String} name + * @param {Object} cmd + * @return {this|false} Returns false if name or cmd is false + * @since v1.3.5 + */ + set: function (name, cmd) { + if (!name || !cmd) { + return false; + } + + // merge any existing command properties + cmd = $.extend(SCEditor.commands[name] || {}, cmd); + + cmd.remove = function () { + SCEditor.command.remove(name); + }; + + SCEditor.commands[name] = cmd; + return this; + }, + + /** + * Removes a command + * + * @param {String} name + * @return {this} + * @since v1.3.5 + */ + remove: function (name) { + if (SCEditor.commands[name]) { + delete SCEditor.commands[name]; + } + + return this; + } + }; + + return SCEditor; + }.call(exports, __webpack_require__, exports, module), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__)); + + +/***/ }, +/* 3 */ +/***/ function(module, exports, __webpack_require__) { + + var __WEBPACK_AMD_DEFINE_RESULT__;!(__WEBPACK_AMD_DEFINE_RESULT__ = function () { + 'use strict'; + + var plugins = {}; + + /** + * Plugin Manager class + * @class PluginManager + * @name PluginManager + */ + var PluginManager = function (thisObj) { + /** + * Alias of this + * + * @private + * @type {Object} + */ + var base = this; + + /** + * Array of all currently registered plugins + * + * @type {Array} + * @private + */ + var registeredPlugins = []; + + + /** + * Changes a signals name from "name" into "signalName". + * + * @param {String} signal + * @return {String} + * @private + */ + var formatSignalName = function (signal) { + return 'signal' + signal.charAt(0).toUpperCase() + signal.slice(1); + }; + + /** + * Calls handlers for a signal + * + * @see call() + * @see callOnlyFirst() + * @param {Array} args + * @param {Boolean} returnAtFirst + * @return {Mixed} + * @private + */ + var callHandlers = function (args, returnAtFirst) { + args = [].slice.call(args); + + var idx, ret, + signal = formatSignalName(args.shift()); + + for (idx = 0; idx < registeredPlugins.length; idx++) { + if (signal in registeredPlugins[idx]) { + ret = registeredPlugins[idx][signal].apply(thisObj, args); + + if (returnAtFirst) { + return ret; + } + } + } + }; + + /** + * Calls all handlers for the passed signal + * + * @param {String} signal + * @param {...String} args + * @return {Void} + * @function + * @name call + * @memberOf PluginManager.prototype + */ + base.call = function () { + callHandlers(arguments, false); + }; + + /** + * Calls the first handler for a signal, and returns the + * + * @param {String} signal + * @param {...String} args + * @return {Mixed} The result of calling the handler + * @function + * @name callOnlyFirst + * @memberOf PluginManager.prototype + */ + base.callOnlyFirst = function () { + return callHandlers(arguments, true); + }; + + /** + * Checks if a signal has a handler + * + * @param {String} signal + * @return {Boolean} + * @function + * @name hasHandler + * @memberOf PluginManager.prototype + */ + base.hasHandler = function (signal) { + var i = registeredPlugins.length; + signal = formatSignalName(signal); + + while (i--) { + if (signal in registeredPlugins[i]) { + return true; + } + } + + return false; + }; + + /** + * Checks if the plugin exists in plugins + * + * @param {String} plugin + * @return {Boolean} + * @function + * @name exists + * @memberOf PluginManager.prototype + */ + base.exists = function (plugin) { + if (plugin in plugins) { + plugin = plugins[plugin]; + + return typeof plugin === 'function' && + typeof plugin.prototype === 'object'; + } + + return false; + }; + + /** + * Checks if the passed plugin is currently registered. + * + * @param {String} plugin + * @return {Boolean} + * @function + * @name isRegistered + * @memberOf PluginManager.prototype + */ + base.isRegistered = function (plugin) { + if (base.exists(plugin)) { + var idx = registeredPlugins.length; + + while (idx--) { + if (registeredPlugins[idx] instanceof plugins[plugin]) { + return true; + } + } + } + + return false; + }; + + /** + * Registers a plugin to receive signals + * + * @param {String} plugin + * @return {Boolean} + * @function + * @name register + * @memberOf PluginManager.prototype + */ + base.register = function (plugin) { + if (!base.exists(plugin) || base.isRegistered(plugin)) { + return false; + } + + plugin = new plugins[plugin](); + registeredPlugins.push(plugin); + + if ('init' in plugin) { + plugin.init.call(thisObj); + } + + return true; + }; + + /** + * Deregisters a plugin. + * + * @param {String} plugin + * @return {Boolean} + * @function + * @name deregister + * @memberOf PluginManager.prototype + */ + base.deregister = function (plugin) { + var removedPlugin, + pluginIdx = registeredPlugins.length, + removed = false; + + if (!base.isRegistered(plugin)) { + return removed; + } + + while (pluginIdx--) { + if (registeredPlugins[pluginIdx] instanceof plugins[plugin]) { + removedPlugin = registeredPlugins.splice(pluginIdx, 1)[0]; + removed = true; + + if ('destroy' in removedPlugin) { + removedPlugin.destroy.call(thisObj); + } + } + } + + return removed; + }; + + /** + * Clears all plugins and removes the owner reference. + * + * Calling any functions on this object after calling + * destroy will cause a JS error. + * + * @name destroy + * @memberOf PluginManager.prototype + */ + base.destroy = function () { + var i = registeredPlugins.length; + + while (i--) { + if ('destroy' in registeredPlugins[i]) { + registeredPlugins[i].destroy.call(thisObj); + } + } + + registeredPlugins = []; + thisObj = null; + }; + }; + + PluginManager.plugins = plugins; + + return PluginManager; + }.call(exports, __webpack_require__, exports, module), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__)); + + +/***/ }, +/* 4 */ +/***/ function(module, exports, __webpack_require__) { + + var __WEBPACK_AMD_DEFINE_RESULT__;!(__WEBPACK_AMD_DEFINE_RESULT__ = function (require) { + 'use strict'; + + var $ = __webpack_require__(1); + var dom = __webpack_require__(5); + var escape = __webpack_require__(7); + var browser = __webpack_require__(6); + + 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; + + + var _nodeToHtml = function (node) { + return $('<p>', node.ownerDocument).append(node).html(); + }; + + /** + * Gets the text, start/end node and offset for + * length chars left or right of the passed node + * at the specified offset. + * + * @param {Node} node + * @param {Number} offset + * @param {Boolean} isLeft + * @param {Number} length + * @return {Object} + * @private + */ + var outerText = function (range, isLeft, length) { + var nodeValue, remaining, start, end, node, + text = '', + next = range.startContainer, + offset = range.startOffset; + + // Handle cases where node is a paragraph and offset + // refers to the index of a text node. + // 3 = text node + if (next && next.nodeType !== 3) { + next = next.childNodes[offset]; + offset = 0; + } + + start = end = offset; + + while (length > text.length && next && next.nodeType === 3) { + nodeValue = next.nodeValue; + remaining = length - text.length; + + // If not the first node, start and end should be at their + // max values as will be updated when getting the text + if (node) { + end = nodeValue.length; + start = 0; + } + + node = next; + + if (isLeft) { + start = Math.max(end - remaining, 0); + offset = start; + + text = nodeValue.substr(start, end - start) + text; + next = node.previousSibling; + } else { + end = Math.min(remaining, nodeValue.length); + offset = start + end; + + text += nodeValue.substr(start, end); + next = node.nextSibling; + } + } + + return { + node: node || next, + offset: offset, + text: text + }; + }; + + var RangeHelper = function (w, d) { + var _createMarker, _isOwner, _prepareInput, + doc = d || w.contentDocument || w.document, + win = w, + isW3C = !!w.getSelection, + startMarker = 'sceditor-start-marker', + endMarker = 'sceditor-end-marker', + CHARACTER = 'character', // Used to improve minification + base = this; + + /** + * <p>Inserts HTML into the current range replacing any selected + * text.</p> + * + * <p>If endHTML is specified the selected contents will be put between + * html and endHTML. If there is nothing selected html and endHTML are + * just concatenate together.</p> + * + * @param {string} html + * @param {string} endHTML + * @return False on fail + * @function + * @name insertHTML + * @memberOf RangeHelper.prototype + */ + base.insertHTML = function (html, endHTML) { + var node, div, + range = base.selectedRange(); + + if (!range) { + return false; + } + + if (isW3C) { + if (endHTML) { + html += base.selectedHtml() + endHTML; + } + + div = doc.createElement('p'); + node = doc.createDocumentFragment(); + div.innerHTML = html; + + while (div.firstChild) { + node.appendChild(div.firstChild); + } + + base.insertNode(node); + } else { + range.pasteHTML(_prepareInput(html, endHTML, true)); + base.restoreRange(); + } + }; + + /** + * Prepares HTML to be inserted by adding a zero width space + * if the last child is empty and adding the range start/end + * markers to the last child. + * + * @param {Node|string} node + * @param {Node|string} endNode + * @param {boolean} returnHtml + * @return {Node|string} + * @private + */ + _prepareInput = function (node, endNode, returnHtml) { + var lastChild, $lastChild, + div = doc.createElement('div'), + $div = $(div); + + if (typeof node === 'string') { + if (endNode) { + node += base.selectedHtml() + endNode; + } + + $div.html(node); + } else { + $div.append(node); + + if (endNode) { + $div + .append(base.selectedRange().extractContents()) + .append(endNode); + } + } + + if (!(lastChild = div.lastChild)) { + return; + } + + while (!dom.isInline(lastChild.lastChild, true)) { + lastChild = lastChild.lastChild; + } + + if (dom.canHaveChildren(lastChild)) { + $lastChild = $(lastChild); + + // IE <= 8 and Webkit won't allow the cursor to be placed + // inside an empty tag, so add a zero width space to it. + if (!lastChild.lastChild) { + $lastChild.append('\u200B'); + } + } + + // Needed so IE <= 8 can place the cursor after emoticons and images + if (IE_VER && IE_VER < 9 && $(lastChild).is('img')) { + $div.append('\u200B'); + } + + base.removeMarkers(); + + // Append marks to last child so when restored cursor will be in + // the right place + ($lastChild || $div) + .append(_createMarker(startMarker)) + .append(_createMarker(endMarker)); + + if (returnHtml) { + return $div.html(); + } + + return $(doc.createDocumentFragment()).append($div.contents())[0]; + }; + + /** + * <p>The same as insertHTML except with DOM nodes instead</p> + * + * <p><strong>Warning:</strong> the nodes must belong to the + * document they are being inserted into. Some browsers + * will throw exceptions if they don't.</p> + * + * @param {Node} node + * @param {Node} endNode + * @return False on fail + * @function + * @name insertNode + * @memberOf RangeHelper.prototype + */ + base.insertNode = function (node, endNode) { + if (isW3C) { + var input = _prepareInput(node, endNode), + range = base.selectedRange(), + parent = range.commonAncestorContainer; + + if (!input) { + return false; + } + + range.deleteContents(); + + // FF allows <br /> to be selected but inserting a node + // into <br /> will cause it not to be displayed so must + // insert before the <br /> in FF. + // 3 = TextNode + if (parent && parent.nodeType !== 3 && + !dom.canHaveChildren(parent)) { + parent.parentNode.insertBefore(input, parent); + } else { + range.insertNode(input); + } + + base.restoreRange(); + } else { + base.insertHTML( + _nodeToHtml(node), + endNode ? _nodeToHtml(endNode) : null + ); + } + }; + + /** + * <p>Clones the selected Range</p> + * + * <p>IE <= 8 will return a TextRange, all other browsers + * will return a Range object.</p> + * + * @return {Range|TextRange} + * @function + * @name cloneSelected + * @memberOf RangeHelper.prototype + */ + base.cloneSelected = function () { + var range = base.selectedRange(); + + if (range) { + return isW3C ? range.cloneRange() : range.duplicate(); + } + }; + + /** + * <p>Gets the selected Range</p> + * + * <p>IE <= 8 will return a TextRange, all other browsers + * will return a Range object.</p> + * + * @return {Range|TextRange} + * @function + * @name selectedRange + * @memberOf RangeHelper.prototype + */ + base.selectedRange = function () { + var range, firstChild, + sel = isW3C ? win.getSelection() : doc.selection; + + if (!sel) { + return; + } + + // When creating a new range, set the start to the first child + // element of the body element to avoid errors in FF. + if (sel.getRangeAt && sel.rangeCount <= 0) { + firstChild = doc.body; + while (firstChild.firstChild) { + firstChild = firstChild.firstChild; + } + + range = doc.createRange(); + // Must be setStartBefore otherwise it can cause infinite + // loops with lists in WebKit. See issue 442 + range.setStartBefore(firstChild); + + sel.addRange(range); + } + + if (isW3C && sel.rangeCount > 0) { + range = sel.getRangeAt(0); + } + + if (!isW3C && sel.type !== 'Control') { + range = sel.createRange(); + } + + // IE fix to make sure only return selections that + // are part of the WYSIWYG iframe + return _isOwner(range) ? range : null; + }; + + /** + * Checks if an IE TextRange range belongs to + * this document or not. + * + * Returns true if the range isn't an IE range or + * if the range is null. + * + * @private + */ + _isOwner = function (range) { + var parent; + + if (range && !isW3C) { + parent = range.parentElement(); + } + + // IE fix to make sure only return selections + // that are part of the WYSIWYG iframe + return parent ? + parent.ownerDocument === doc : + true; + }; + + /** + * Gets if there is currently a selection + * + * @return {boolean} + * @function + * @name hasSelection + * @since 1.4.4 + * @memberOf RangeHelper.prototype + */ + base.hasSelection = function () { + var sel = isW3C ? win.getSelection() : doc.selection; + + if (isW3C || !sel) { + return sel && sel.rangeCount > 0; + } + + return sel.type !== 'None' && _isOwner(sel.createRange()); + }; + + /** + * Gets the currently selected HTML + * + * @return {string} + * @function + * @name selectedHtml + * @memberOf RangeHelper.prototype + */ + base.selectedHtml = function () { + var div, + range = base.selectedRange(); + + if (range) { + + // IE9+ and all other browsers + if (isW3C) { + div = doc.createElement('p'); + div.appendChild(range.cloneContents()); + + return div.innerHTML; + // IE < 9 + } else if (range.text !== '' && range.htmlText) { + return range.htmlText; + } + } + + return ''; + }; + + /** + * Gets the parent node of the selected contents in the range + * + * @return {HTMLElement} + * @function + * @name parentNode + * @memberOf RangeHelper.prototype + */ + base.parentNode = function () { + var range = base.selectedRange(); + + if (range) { + return range.parentElement ? + range.parentElement() : + range.commonAncestorContainer; + } + }; + + /** + * Gets the first block level parent of the selected + * contents of the range. + * + * @return {HTMLElement} + * @function + * @name getFirstBlockParent + * @memberOf RangeHelper.prototype + */ + /** + * Gets the first block level parent of the selected + * contents of the range. + * + * @param {Node} n The element to get the first block level parent from + * @return {HTMLElement} + * @function + * @name getFirstBlockParent^2 + * @since 1.4.1 + * @memberOf RangeHelper.prototype + */ + base.getFirstBlockParent = function (n) { + var func = function (node) { + if (!dom.isInline(node, true)) { + return node; + } + + node = node ? node.parentNode : null; + + return node ? func(node) : node; + }; + + return func(n || base.parentNode()); + }; + + /** + * Inserts a node at either the start or end of the current selection + * + * @param {Bool} start + * @param {Node} node + * @function + * @name insertNodeAt + * @memberOf RangeHelper.prototype + */ + base.insertNodeAt = function (start, node) { + var currentRange = base.selectedRange(), + range = base.cloneSelected(); + + if (!range) { + return false; + } + + range.collapse(start); + + if (isW3C) { + range.insertNode(node); + } else { + range.pasteHTML(_nodeToHtml(node)); + } + + // Reselect the current range. + // Fixes issue with Chrome losing the selection. Issue#82 + base.selectRange(currentRange); + }; + + /** + * Creates a marker node + * + * @param {string} id + * @return {Node} + * @private + */ + _createMarker = function (id) { + base.removeMarker(id); + + var marker = doc.createElement('span'); + marker.id = id; + marker.style.lineHeight = '0'; + marker.style.display = 'none'; + marker.className = 'sceditor-selection sceditor-ignore'; + marker.innerHTML = ' '; + + return marker; + }; + + /** + * Inserts start/end markers for the current selection + * which can be used by restoreRange to re-select the + * range. + * + * @memberOf RangeHelper.prototype + * @function + * @name insertMarkers + */ + base.insertMarkers = function () { + base.insertNodeAt(true, _createMarker(startMarker)); + base.insertNodeAt(false, _createMarker(endMarker)); + }; + + /** + * Gets the marker with the specified ID + * + * @param {string} id + * @return {Node} + * @function + * @name getMarker + * @memberOf RangeHelper.prototype + */ + base.getMarker = function (id) { + return doc.getElementById(id); + }; + + /** + * Removes the marker with the specified ID + * + * @param {string} id + * @function + * @name removeMarker + * @memberOf RangeHelper.prototype + */ + base.removeMarker = function (id) { + var marker = base.getMarker(id); + + if (marker) { + marker.parentNode.removeChild(marker); + } + }; + + /** + * Removes the start/end markers + * + * @function + * @name removeMarkers + * @memberOf RangeHelper.prototype + */ + base.removeMarkers = function () { + base.removeMarker(startMarker); + base.removeMarker(endMarker); + }; + + /** + * Saves the current range location. Alias of insertMarkers() + * + * @function + * @name saveRage + * @memberOf RangeHelper.prototype + */ + base.saveRange = function () { + base.insertMarkers(); + }; + + /** + * Select the specified range + * + * @param {Range|TextRange} range + * @function + * @name selectRange + * @memberOf RangeHelper.prototype + */ + base.selectRange = function (range) { + if (isW3C) { + var lastChild; + var sel = win.getSelection(); + var container = range.endContainer; + + // 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 && range.collapsed && container && + !dom.isInline(container, true)) { + + lastChild = container.lastChild; + while (lastChild && $(lastChild).is('.sceditor-ignore')) { + lastChild = lastChild.previousSibling; + } + + if ($(lastChild).is('br')) { + var rng = doc.createRange(); + rng.setEndAfter(lastChild); + rng.collapse(false); + + if (base.compare(range, rng)) { + range.setStartBefore(lastChild); + range.collapse(true); + } + } + } + + if (sel) { + base.clear(); + sel.addRange(range); + } + } else { + range.select(); + } + }; + + /** + * Restores the last range saved by saveRange() or insertMarkers() + * + * @function + * @name restoreRange + * @memberOf RangeHelper.prototype + */ + base.restoreRange = function () { + var marker, isCollapsed, previousSibling, + range = base.selectedRange(), + start = base.getMarker(startMarker), + end = base.getMarker(endMarker); + + if (!start || !end || !range) { + return false; + } + + isCollapsed = start.nextSibling === end; + + if (!isW3C) { + range = doc.body.createTextRange(); + marker = doc.body.createTextRange(); + + // IE < 9 cannot set focus after a BR so need to insert + // a dummy char after it to allow the cursor to be placed + previousSibling = start.previousSibling; + if (start.nextSibling === end && (!previousSibling || + !dom.isInline(previousSibling, true) || + $(previousSibling).is('br'))) { + $(start).before('\u200B'); + } + + marker.moveToElementText(start); + range.setEndPoint('StartToStart', marker); + range.moveStart(CHARACTER, 0); + + marker.moveToElementText(end); + range.setEndPoint('EndToStart', marker); + range.moveEnd(CHARACTER, 0); + } else { + range = doc.createRange(); + + range.setStartBefore(start); + range.setEndAfter(end); + } + + if (isCollapsed) { + range.collapse(true); + } + + base.selectRange(range); + base.removeMarkers(); + }; + + /** + * Selects the text left and right of the current selection + * + * @param {int} left + * @param {int} right + * @since 1.4.3 + * @function + * @name selectOuterText + * @memberOf RangeHelper.prototype + */ + base.selectOuterText = function (left, right) { + var start, end, + range = base.cloneSelected(); + + if (!range) { + return false; + } + + range.collapse(false); + + if (!isW3C) { + range.moveStart(CHARACTER, 0 - left); + range.moveEnd(CHARACTER, right); + } else { + start = outerText(range, true, left); + end = outerText(range, false, right); + + range.setStart(start.node, start.offset); + range.setEnd(end.node, end.offset); + } + + base.selectRange(range); + }; + + /** + * Gets the text left or right of the current selection + * + * @param {boolean} before + * @param {number} length + * @since 1.4.3 + * @function + * @name selectOuterText + * @memberOf RangeHelper.prototype + */ + base.getOuterText = function (before, length) { + var range = base.cloneSelected(); + + if (!range) { + return ''; + } + + range.collapse(!before); + + if (!isW3C) { + if (before) { + range.moveStart(CHARACTER, 0 - length); + } else { + range.moveEnd(CHARACTER, length); + } + + return range.text; + } + + return outerText(range, before, length).text; + }; + + /** + * Replaces keywords with values based on the current caret position + * + * @param {Array} keywords + * @param {boolean} includeAfter If to include the text after the + * current caret position or just + * text before + * @param {boolean} keywordsSorted If the keywords array is pre + * sorted shortest to longest + * @param {number} longestKeyword Length of the longest keyword + * @param {boolean} requireWhitespace If the key must be surrounded + * by whitespace + * @param {string} keypressChar If this is being called from + * a keypress event, this should be + * set to the pressed character + * @return {boolean} + * @function + * @name replaceKeyword + * @memberOf RangeHelper.prototype + */ + /*jshint maxparams: false*/ + base.replaceKeyword = function ( + keywords, + includeAfter, + keywordsSorted, + longestKeyword, + requireWhitespace, + keypressChar + ) { + if (!keywordsSorted) { + keywords.sort(function (a, b) { + return a[0].length - b[0].length; + }); + } + + var outerText, matchPos, startIndex, leftLen, + charsLeft, keyword, keywordLen, + whitespaceRegex = '[\\s\xA0\u2002\u2003\u2009]', + keywordIdx = keywords.length, + whitespaceLen = requireWhitespace ? 1 : 0, + maxKeyLen = longestKeyword || + keywords[keywordIdx - 1][0].length; + + if (requireWhitespace) { + // requireWhitespace doesn't work with textRanges as they + // select text on the other side of elements causing + // space-img-key to match when it shouldn't. + if (!isW3C) { + return false; + } + + maxKeyLen++; + } + + keypressChar = keypressChar || ''; + outerText = base.getOuterText(true, maxKeyLen); + leftLen = outerText.length; + outerText += keypressChar; + + if (includeAfter) { + outerText += base.getOuterText(false, maxKeyLen); + } + + while (keywordIdx--) { + keyword = keywords[keywordIdx][0]; + keywordLen = keyword.length; + startIndex = Math.max(0, leftLen - keywordLen - whitespaceLen); + + if (requireWhitespace) { + matchPos = outerText + .substr(startIndex) + .search(new RegExp( + '(?:' + whitespaceRegex + ')' + + escape.regex(keyword) + + '(?=' + whitespaceRegex + ')' + )); + } else { + matchPos = outerText.indexOf(keyword, startIndex); + } + + if (matchPos > -1) { + // Add the length of the text that was removed by substr() + // when matching and also add 1 for the whitespace + if (requireWhitespace) { + matchPos += startIndex + 1; + } + + // Make sure the match is between before and + // after, not just entirely in one side or the other + if (matchPos <= leftLen && + matchPos + keywordLen + whitespaceLen >= leftLen) { + charsLeft = leftLen - matchPos; + + // If the keypress char is white space then it should + // not be replaced, only chars that are part of the + // key should be replaced. + base.selectOuterText( + charsLeft, + keywordLen - charsLeft - + (/^\S/.test(keypressChar) ? 1 : 0) + ); + + base.insertHTML(keywords[keywordIdx][1]); + return true; + } + } + } + + return false; + }; + + /** + * Compares two ranges. + * + * If rangeB is undefined it will be set to + * the current selected range + * + * @param {Range|TextRange} rangeA + * @param {Range|TextRange} rangeB + * @return {boolean} + */ + base.compare = function (rangeA, rangeB) { + var END_TO_END = isW3C ? Range.END_TO_END : 'EndToEnd', + START_TO_START = isW3C ? Range.START_TO_START : 'StartToStart', + comparePoints = isW3C ? + 'compareBoundaryPoints' : + 'compareEndPoints'; + + if (!rangeB) { + rangeB = base.selectedRange(); + } + + if (!rangeA || !rangeB) { + return !rangeA && !rangeB; + } + + return _isOwner(rangeA) && _isOwner(rangeB) && + rangeA[comparePoints](END_TO_END, rangeB) === 0 && + rangeA[comparePoints](START_TO_START, rangeB) === 0; + }; + + /** + * Removes any current selection + * + * @since 1.4.6 + */ + base.clear = function () { + var sel = isW3C ? win.getSelection() : doc.selection; + + if (sel) { + if (sel.removeAllRanges) { + sel.removeAllRanges(); + } else if (sel.empty) { + sel.empty(); + } + } + }; + }; + + return RangeHelper; + }.call(exports, __webpack_require__, exports, module), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__)); + + +/***/ }, +/* 5 */ +/***/ function(module, exports, __webpack_require__) { + + var __WEBPACK_AMD_DEFINE_RESULT__;!(__WEBPACK_AMD_DEFINE_RESULT__ = function (require) { + 'use strict'; + + var $ = __webpack_require__(1); + var browser = __webpack_require__(6); + + var _propertyNameCache = {}; + + var dom = { + /** + * Loop all child nodes of the passed node + * + * The function should accept 1 parameter being the node. + * If the function returns false the loop will be exited. + * + * @param {HTMLElement} node + * @param {Function} func Callback which is called with every + * child node as the first argument. + * @param {bool} innermostFirst If the innermost node should be passed + * to the function before it's parents. + * @param {bool} siblingsOnly If to only traverse the nodes siblings + * @param {bool} reverse If to traverse the nodes in reverse + */ + /*jshint maxparams: false*/ + traverse: function (node, func, innermostFirst, siblingsOnly, reverse) { + if (node) { + node = reverse ? node.lastChild : node.firstChild; + + while (node) { + var next = reverse ? + node.previousSibling : + node.nextSibling; + + if ( + (!innermostFirst && func(node) === false) || + (!siblingsOnly && dom.traverse( + node, func, innermostFirst, siblingsOnly, reverse + ) === false) || + (innermostFirst && func(node) === false) + ) { + return false; + } + + // move to next child + node = next; + } + } + }, + + /** + * Like traverse but loops in reverse + * @see traverse + */ + rTraverse: function (node, func, innermostFirst, siblingsOnly) { + this.traverse(node, func, innermostFirst, siblingsOnly, true); + }, + + /** + * Parses HTML + * + * @param {String} html + * @param {Document} context + * @since 1.4.4 + * @return {Array} + */ + parseHTML: function (html, context) { + var ret = [], + tmp = (context || document).createElement('div'); + + tmp.innerHTML = html; + + $.merge(ret, tmp.childNodes); + + return ret; + }, + + /** + * Checks if an element has any styling. + * + * It has styling if it is not a plain <div> or <p> or + * if it has a class, style attribute or data. + * + * @param {HTMLElement} elm + * @return {Boolean} + * @since 1.4.4 + */ + hasStyling: function (elm) { + var $elm = $(elm); + + return elm && (!$elm.is('p,div') || elm.className || + $elm.attr('style') || !$.isEmptyObject($elm.data())); + }, + + /** + * Converts an element from one type to another. + * + * For example it can convert the element <b> to <strong> + * + * @param {HTMLElement} oldElm + * @param {String} toTagName + * @return {HTMLElement} + * @since 1.4.4 + */ + convertElement: function (oldElm, toTagName) { + var child, attr, + oldAttrs = oldElm.attributes, + attrsIdx = oldAttrs.length, + newElm = oldElm.ownerDocument.createElement(toTagName); + + while (attrsIdx--) { + attr = oldAttrs[attrsIdx]; + + // IE < 8 returns all possible attributes instead of just + // the specified ones so have to check it is specified. + if (!browser.ie || attr.specified) { + // IE < 8 doesn't return the CSS for the style attribute + // so must copy it manually + if (browser.ie < 8 && /style/i.test(attr.name)) { + dom.copyCSS(oldElm, newElm); + } else { + // Some browsers parse invalid attributes names like + // 'size"2' which throw an exception when set, just + // ignore these. + try { + newElm.setAttribute(attr.name, attr.value); + } catch (ex) {} + } + } + } + + while ((child = oldElm.firstChild)) { + newElm.appendChild(child); + } + + oldElm.parentNode.replaceChild(newElm, oldElm); + + return newElm; + }, + + /** + * List of block level elements separated by bars (|) + * @type {string} + */ + blockLevelList: '|body|hr|p|div|h1|h2|h3|h4|h5|h6|address|pre|form|' + + 'table|tbody|thead|tfoot|th|tr|td|li|ol|ul|blockquote|center|', + + /** + * List of elements that do not allow children separated by bars (|) + * + * @param {Node} node + * @return {bool} + * @since 1.4.5 + */ + canHaveChildren: function (node) { + // 1 = Element + // 9 = Document + // 11 = Document Fragment + if (!/11?|9/.test(node.nodeType)) { + return false; + } + + // List of empty HTML tags separated by bar (|) character. + // Source: http://www.w3.org/TR/html4/index/elements.html + // Source: http://www.w3.org/TR/html5/syntax.html#void-elements + return ('|iframe|area|base|basefont|br|col|frame|hr|img|input|wbr' + + '|isindex|link|meta|param|command|embed|keygen|source|track|' + + 'object|').indexOf('|' + node.nodeName.toLowerCase() + '|') < 0; + }, + + /** + * Checks if an element is inline + * + * @return {bool} + */ + isInline: function (elm, includeCodeAsBlock) { + var tagName, + nodeType = (elm || {}).nodeType || 3; + + if (nodeType !== 1) { + return nodeType === 3; + } + + tagName = elm.tagName.toLowerCase(); + + if (tagName === 'code') { + return !includeCodeAsBlock; + } + + return dom.blockLevelList.indexOf('|' + tagName + '|') < 0; + }, + + /** + * <p>Copys the CSS from 1 node to another.</p> + * + * <p>Only copies CSS defined on the element e.g. style attr.</p> + * + * @param {HTMLElement} from + * @param {HTMLElement} to + */ + copyCSS: function (from, to) { + to.style.cssText = from.style.cssText + to.style.cssText; + }, + + /** + * Fixes block level elements inside in inline elements. + * + * @param {HTMLElement} node + */ + fixNesting: function (node) { + var getLastInlineParent = function (node) { + while (dom.isInline(node.parentNode, true)) { + node = node.parentNode; + } + + return node; + }; + + dom.traverse(node, function (node) { + // Any blocklevel element inside an inline element needs fixing. + if (node.nodeType === 1 && !dom.isInline(node, true) && + dom.isInline(node.parentNode, true)) { + var parent = getLastInlineParent(node), + rParent = parent.parentNode, + before = dom.extractContents(parent, node), + middle = node; + + // copy current styling so when moved out of the parent + // it still has the same styling + dom.copyCSS(parent, middle); + + rParent.insertBefore(before, parent); + rParent.insertBefore(middle, parent); + } + }); + }, + + /** + * Finds the common parent of two nodes + * + * @param {HTMLElement} node1 + * @param {HTMLElement} node2 + * @return {HTMLElement} + */ + findCommonAncestor: function (node1, node2) { + // Not as fast as making two arrays of parents and comparing + // but is a lot smaller and as it's currently only used with + // fixing invalid nesting it doesn't need to be very fast + return $(node1).parents().has($(node2)).first(); + }, + + getSibling: function (node, previous) { + if (!node) { + return null; + } + + return (previous ? node.previousSibling : node.nextSibling) || + dom.getSibling(node.parentNode, previous); + }, + + /** + * Removes unused whitespace from the root and all it's children + * + * @name removeWhiteSpace^1 + * @param {HTMLElement} root + */ + /** + * Removes unused whitespace from the root and all it's children. + * + * If preserveNewLines is true, new line characters will not be removed + * + * @name removeWhiteSpace^2 + * @param {HTMLElement} root + * @param {boolean} preserveNewLines + * @since 1.4.3 + */ + removeWhiteSpace: function (root, preserveNewLines) { + var nodeValue, nodeType, next, previous, previousSibling, + cssWhiteSpace, nextNode, trimStart, + getSibling = dom.getSibling, + isInline = dom.isInline, + node = root.firstChild; + + while (node) { + nextNode = node.nextSibling; + nodeValue = node.nodeValue; + nodeType = node.nodeType; + + // 1 = element + if (nodeType === 1 && node.firstChild) { + cssWhiteSpace = $(node).css('whiteSpace'); + + // Skip all pre & pre-wrap with any vendor prefix + if (!/pre(\-wrap)?$/i.test(cssWhiteSpace)) { + dom.removeWhiteSpace( + node, + /line$/i.test(cssWhiteSpace) + ); + } + } + + // 3 = textnode + if (nodeType === 3 && nodeValue) { + next = getSibling(node); + previous = getSibling(node, true); + trimStart = false; + + while ($(previous).hasClass('sceditor-ignore')) { + previous = getSibling(previous, true); + } + // If previous sibling isn't inline or is a textnode that + // ends in whitespace, time the start whitespace + if (isInline(node) && previous) { + previousSibling = previous; + + while (previousSibling.lastChild) { + previousSibling = previousSibling.lastChild; + } + + trimStart = previousSibling.nodeType === 3 ? + /[\t\n\r ]$/.test(previousSibling.nodeValue) : + !isInline(previousSibling); + } + + // Clear zero width spaces + nodeValue = nodeValue.replace(/\u200B/g, ''); + + // Strip leading whitespace + if (!previous || !isInline(previous) || trimStart) { + nodeValue = nodeValue.replace( + preserveNewLines ? /^[\t ]+/ : /^[\t\n\r ]+/, + '' + ); + } + + // Strip trailing whitespace + if (!next || !isInline(next)) { + nodeValue = nodeValue.replace( + preserveNewLines ? /[\t ]+$/ : /[\t\n\r ]+$/, + '' + ); + } + + // Remove empty text nodes + if (!nodeValue.length) { + root.removeChild(node); + } else { + node.nodeValue = nodeValue.replace( + preserveNewLines ? /[\t ]+/g : /[\t\n\r ]+/g, + ' ' + ); + } + } + + node = nextNode; + } + }, + + /** + * Extracts all the nodes between the start and end nodes + * + * @param {HTMLElement} startNode The node to start extracting at + * @param {HTMLElement} endNode The node to stop extracting at + * @return {DocumentFragment} + */ + extractContents: function (startNode, endNode) { + var extract, + commonAncestor = dom + .findCommonAncestor(startNode, endNode) + .get(0), + startReached = false, + endReached = false; + + extract = function (root) { + var clone, + docFrag = startNode.ownerDocument.createDocumentFragment(); + + dom.traverse(root, function (node) { + // if end has been reached exit loop + if (endReached || node === endNode) { + endReached = true; + + return false; + } + + if (node === startNode) { + startReached = true; + } + + // if the start has been reached and this elm contains + // the end node then clone it + // if this node contains the start node then add it + if ($.contains(node, startNode) || + (startReached && $.contains(node, endNode))) { + clone = node.cloneNode(false); + + clone.appendChild(extract(node)); + docFrag.appendChild(clone); + + // otherwise move it if its parent isn't already part of it + } else if (startReached && !$.contains(docFrag, node)) { + docFrag.appendChild(node); + } + }, false); + + return docFrag; + }; + + return extract(commonAncestor); + }, + + /** + * Gets the offset position of an element + * + * @param {HTMLElement} obj + * @return {Object} An object with left and top properties + */ + getOffset: function (obj) { + var pLeft = 0, + pTop = 0; + + while (obj) { + pLeft += obj.offsetLeft; + pTop += obj.offsetTop; + obj = obj.offsetParent; + } + + return { + left: pLeft, + top: pTop + }; + }, + + /** + * Gets the value of a CSS property from the elements style attribute + * + * @param {HTMLElement} elm + * @param {String} property + * @return {String} + */ + getStyle: function (elm, property) { + var $elm, direction, styleValue, + elmStyle = elm.style; + + if (!elmStyle) { + return ''; + } + + if (!_propertyNameCache[property]) { + _propertyNameCache[property] = $.camelCase(property); + } + + property = _propertyNameCache[property]; + styleValue = elmStyle[property]; + + // Add an exception for text-align + if ('textAlign' === property) { + $elm = $(elm); + direction = elmStyle.direction; + styleValue = styleValue || $elm.css(property); + + if ($elm.parent().css(property) === styleValue || + $elm.css('display') !== 'block' || + $elm.is('hr') || $elm.is('th')) { + return ''; + } + // check all works with changes and merge with prev? + // IE changes text-align to the same as the current direction + // so skip unless its not the same + if ((/right/i.test(styleValue) && direction === 'rtl') || + (/left/i.test(styleValue) && direction === 'ltr')) { + return ''; + } + } + + return styleValue; + }, + + /** + * Tests if an element has a style. + * + * If values are specified it will check that the styles value + * matches one of the values + * + * @param {HTMLElement} elm + * @param {String} property + * @param {String|Array} values + * @return {Boolean} + */ + hasStyle: function (elm, property, values) { + var styleValue = dom.getStyle(elm, property); + + if (!styleValue) { + return false; + } + + return !values || styleValue === values || + ($.isArray(values) && $.inArray(styleValue, values) > -1); + } + }; + + return dom; + }.call(exports, __webpack_require__, exports, module), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__)); + + +/***/ }, +/* 6 */ +/***/ function(module, exports, __webpack_require__) { + + var __WEBPACK_AMD_DEFINE_RESULT__;!(__WEBPACK_AMD_DEFINE_RESULT__ = function (require, exports) { + 'use strict'; + + var $ = __webpack_require__(1); + + var USER_AGENT = navigator.userAgent; + + /** + * Detects the version of IE is being used if any. + * + * Will be the IE version number or undefined if the + * browser is not IE. + * + * Source: https://gist.github.com/527683 with extra code + * for IE 10 & 11 detection. + * + * @function + * @name ie + * @type {int} + */ + exports.ie = (function () { + var undef, + v = 3, + doc = document, + div = doc.createElement('div'), + all = div.getElementsByTagName('i'); + + do { + div.innerHTML = '<!--[if gt IE ' + (++v) + ']><i></i><![endif]-->'; + } while (all[0]); + + // Detect IE 10 as it doesn't support conditional comments. + if ((doc.documentMode && doc.all && window.atob)) { + v = 10; + } + + // Detect IE 11 + if (v === 4 && doc.documentMode) { + v = 11; + } + + return v > 4 ? v : undef; + }()); + + /** + * <p>Detects if the browser is iOS</p> + * + * <p>Needed to fix iOS specific bugs/</p> + * + * @function + * @name ios + * @memberOf jQuery.sceditor + * @type {Boolean} + */ + exports.ios = /iPhone|iPod|iPad| wosbrowser\//i.test(USER_AGENT); + + /** + * If the browser supports WYSIWYG editing (e.g. older mobile browsers). + * + * @function + * @name isWysiwygSupported + * @return {Boolean} + */ + exports.isWysiwygSupported = (function () { + var match, isUnsupported, undef, + editableAttr = $('<p contenteditable="true">')[0].contentEditable; + + // Check if the contenteditable attribute is supported + if (editableAttr === undef || editableAttr === 'inherit') { + return false; + } + + // I think blackberry supports contentEditable or will at least + // give a valid value for the contentEditable detection above + // so it isn't included in the below tests. + + // I hate having to do UA sniffing but some mobile browsers say they + // support contentediable when it isn't usable, i.e. you can't enter + // text. + // This is the only way I can think of to detect them which is also how + // every other editor I've seen deals with this issue. + + // Exclude Opera mobile and mini + isUnsupported = /Opera Mobi|Opera Mini/i.test(USER_AGENT); + + if (/Android/i.test(USER_AGENT)) { + isUnsupported = true; + + if (/Safari/.test(USER_AGENT)) { + // Android browser 534+ supports content editable + // This also matches Chrome which supports content editable too + match = /Safari\/(\d+)/.exec(USER_AGENT); + isUnsupported = (!match || !match[1] ? true : match[1] < 534); + } + } + + // The current version of Amazon Silk supports it, older versions didn't + // As it uses webkit like Android, assume it's the same and started + // working at versions >= 534 + if (/ Silk\//i.test(USER_AGENT)) { + match = /AppleWebKit\/(\d+)/.exec(USER_AGENT); + isUnsupported = (!match || !match[1] ? true : match[1] < 534); + } + + // iOS 5+ supports content editable + if (exports.ios) { + // Block any version <= 4_x(_x) + isUnsupported = /OS [0-4](_\d)+ like Mac/i.test(USER_AGENT); + } + + // FireFox does support WYSIWYG on mobiles so override + // any previous value if using FF + if (/Firefox/i.test(USER_AGENT)) { + isUnsupported = false; + } + + if (/OneBrowser/i.test(USER_AGENT)) { + isUnsupported = false; + } + + // UCBrowser works but doesn't give a unique user agent + if (navigator.vendor === 'UCWEB') { + isUnsupported = false; + } + + return !isUnsupported; + }()); + }.call(exports, __webpack_require__, exports, module), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__)); + + +/***/ }, +/* 7 */ +/***/ function(module, exports, __webpack_require__) { + + var __WEBPACK_AMD_DEFINE_RESULT__;!(__WEBPACK_AMD_DEFINE_RESULT__ = function (require, exports) { + 'use strict'; + + var VALID_SCHEME_REGEX = + /^(?:https?|s?ftp|mailto|spotify|skype|ssh|teamspeak|tel):|(?:\/\/)/i; + + /** + * Escapes a string so it's safe to use in regex + * + * @param {String} str + * @return {String} + * @name regex + */ + exports.regex = function (str) { + return str.replace(/([\-.*+?^=!:${}()|\[\]\/\\])/g, '\\$1'); + }; + + /** + * Escapes all HTML entities in a string + * + * If noQuotes is set to false, all single and double + * quotes will also be escaped + * + * @param {String} str + * @param {Boolean} [noQuotes=false] + * @return {String} + * @name entities + * @since 1.4.1 + */ + exports.entities = function (str, noQuotes) { + if (!str) { + return str; + } + + var replacements = { + '&': '&', + '<': '<', + '>': '>', + ' ': ' ', + '\r\n': '\n', + '\r': '\n', + '\n': '<br />' + }; + + if (noQuotes !== false) { + replacements['"'] = '"'; + replacements['\''] = '''; + replacements['`'] = '`'; + } + + str = str.replace(/ {2}|\r\n|[&<>\r\n'"`]/g, function (match) { + return replacements[match] || match; + }); + + return str; + }; + + /** + * Escape URI scheme. + * + * Appends the current URL to a url if it has a scheme that is not: + * + * http + * https + * sftp + * ftp + * mailto + * spotify + * skype + * ssh + * teamspeak + * tel + * // + * + * **IMPORTANT**: This does not escape any HTML in a url, for + * that use the escape.entities() method. + * + * @param {String} url + * @return {String} + * @name escapeUriScheme + * @memberOf jQuery.sceditor + * @since 1.4.5 + */ + exports.uriScheme = function (url) { + /*jshint maxlen:false*/ + var path, + // If there is a : before a / then it has a scheme + hasScheme = /^[^\/]*:/i, + location = window.location; + + // Has no scheme or a valid scheme + if ((!url || !hasScheme.test(url)) || VALID_SCHEME_REGEX.test(url)) { + return url; + } + + path = location.pathname.split('/'); + path.pop(); + + return location.protocol + '//' + + location.host + + path.join('/') + '/' + + url; + }; + }.call(exports, __webpack_require__, exports, module), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__)); + + +/***/ }, +/* 8 */ +/***/ function(module, exports, __webpack_require__) { + + var __WEBPACK_AMD_DEFINE_RESULT__;!(__WEBPACK_AMD_DEFINE_RESULT__ = function () { + 'use strict'; + + /** + * HTML templates used by the editor and default commands + * @type {Object} + * @private + */ + var _templates = { + html: + '<!DOCTYPE html>' + + '<html{attrs}>' + + '<head>' + + // TODO: move these styles into the CSS file + '<style>.ie * {min-height: auto !important} ' + + '.ie table td {height:15px} ' + + // Target Edge (fixes edge issues) + '@supports (-ms-ime-align:auto) { ' + + '* { min-height: auto !important; } ' + + '}' + + '</style>' + + '<meta http-equiv="Content-Type" ' + + 'content="text/html;charset={charset}" />' + + '<link rel="stylesheet" type="text/css" href="{style}" />' + + '</head>' + + '<body contenteditable="true" {spellcheck}><p></p></body>' + + '</html>', + + toolbarButton: '<a class="sceditor-button sceditor-button-{name}" ' + + 'data-sceditor-command="{name}" unselectable="on">' + + '<div unselectable="on">{dispName}</div></a>', + + emoticon: '<img src="{url}" data-sceditor-emoticon="{key}" ' + + 'alt="{key}" title="{tooltip}" />', + + fontOpt: '<a class="sceditor-font-option" href="#" ' + + 'data-font="{font}"><font face="{font}">{font}</font></a>', + + sizeOpt: '<a class="sceditor-fontsize-option" data-size="{size}" ' + + 'href="#"><font size="{size}">{size}</font></a>', + + pastetext: + '<div><label for="txt">{label}</label> ' + + '<textarea cols="20" rows="7" id="txt"></textarea></div>' + + '<div><input type="button" class="button" value="{insert}" />' + + '</div>', + + table: + '<div><label for="rows">{rows}</label><input type="text" ' + + 'id="rows" value="2" /></div>' + + '<div><label for="cols">{cols}</label><input type="text" ' + + 'id="cols" value="2" /></div>' + + '<div><input type="button" class="button" value="{insert}"' + + ' /></div>', + + image: + '<div><label for="link">{url}</label> ' + + '<input type="text" id="image" placeholder="http://" /></div>' + + '<div><label for="width">{width}</label> ' + + '<input type="text" id="width" size="2" /></div>' + + '<div><label for="height">{height}</label> ' + + '<input type="text" id="height" size="2" /></div>' + + '<div><input type="button" class="button" value="{insert}" />' + + '</div>', + + email: + '<div><label for="email">{label}</label> ' + + '<input type="text" id="email" /></div>' + + '<div><label for="des">{desc}</label> ' + + '<input type="text" id="des" /></div>' + + '<div><input type="button" class="button" value="{insert}" />' + + '</div>', + + link: + '<div><label for="link">{url}</label> ' + + '<input type="text" id="link" placeholder="http://" /></div>' + + '<div><label for="des">{desc}</label> ' + + '<input type="text" id="des" /></div>' + + '<div><input type="button" class="button" value="{ins}" /></div>', + + youtubeMenu: + '<div><label for="link">{label}</label> ' + + '<input type="text" id="link" placeholder="https://" /></div>' + + '<div><input type="button" class="button" value="{insert}" />' + + '</div>', + + youtube: + '<iframe width="560" height="315" ' + + 'src="https://www.youtube.com/embed/{id}?wmode=opaque" ' + + 'data-youtube-id="{id}" frameborder="0" allowfullscreen></iframe>' + }; + + /** + * <p>Replaces any params in a template with the passed params.</p> + * + * <p>If createHtml is passed it will use jQuery to create the HTML. The + * same as doing: $(editor.tmpl("html", {params...}));</p> + * + * @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 = $('<div />'), + /** @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 = $('<div />'), + /** @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 = $('<div />'), + 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('<div class="sceditor-color-column">'); + + 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( + '<a href="#" class="sceditor-color-option"' + + ' style="background-color: ' + color + '"' + + ' data-color="' + color + '"></a>' + ); + + if (x % 5 === 0) { + genColor.g -= 51; + genColor.b = 255; + } else { + genColor.b -= 51; + } + } + + html.push('</div>'); + + 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 <li> + 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 + // <blockquote> 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 = '<table>'; + + if (rows < 1 || cols < 1) { + return; + } + + for (row = 0; row < rows; row++) { + html += '<tr>'; + + for (col = 0; col < cols; col++) { + html += '<td>' + + (IE_BR_FIX ? '' : '<br />') + + '</td>'; + } + + html += '</tr>'; + } + + html += '</table>'; + + editor.wysiwygEditorInsertHtml(html); + editor.closeDropDown(true); + e.preventDefault(); + }); + + editor.createDropDown(caller, 'inserttable', content); + }, + tooltip: 'Insert a table' + }, + // END_COMMAND + + // START_COMMAND: Horizontal Rule + horizontalrule: { + exec: 'inserthorizontalrule', + tooltip: 'Insert a horizontal rule' + }, + // END_COMMAND + + // START_COMMAND: Code + code: { + forceNewLineAfter: ['code'], + exec: function () { + this.wysiwygEditorInsertHtml( + '<code>', + (IE_BR_FIX ? '' : '<br />') + '</code>' + ); + }, + 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( + '<img' + attrs + ' src="' + val + '" />' + ); + } + + editor.closeDropDown(true); + e.preventDefault(); + }); + + editor.createDropDown(caller, 'insertimage', content); + }, + tooltip: 'Insert an image' + }, + // END_COMMAND + + // START_COMMAND: E-mail + email: { + exec: function (caller) { + var editor = this, + content = _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( + '<a href="' + 'mailto:' + val + '">' + + description + + '</a>' + ); + } else { + editor.execCommand('createlink', 'mailto:' + val); + } + } + + editor.closeDropDown(true); + e.preventDefault(); + }); + + editor.createDropDown(caller, 'insertemail', content); + }, + tooltip: 'Insert an email' + }, + // END_COMMAND + + // START_COMMAND: Link + link: { + exec: function (caller) { + var editor = this, + content = _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( + '<a href="' + val + '">' + description + '</a>' + ); + } else { + editor.execCommand('createlink', val); + } + } + + editor.closeDropDown(true); + e.preventDefault(); + }); + + editor.createDropDown(caller, 'insertlink', content); + }, + tooltip: 'Insert a link' + }, + // END_COMMAND + + // START_COMMAND: Unlink + unlink: { + 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 = '<blockquote>', + end = '</blockquote>'; + + // if there is HTML passed set end to null so any selected + // text is replaced + if (html) { + author = (author ? '<cite>' + author + '</cite>' : ''); + before = before + author + html + end; + end = null; + // if not add a newline to the end of the inserted quote + } else if (this.getRangeHelper().selectedHtml() === '') { + end = (IE_BR_FIX ? '' : '<br />') + 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 = $('<div />'), + $line = $('<div />').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( + $('<img />').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 = $('<div />').appendTo($content); + } + }); + + if (!includeMore && editor.opts.emoticons.more) { + $moreLink = $( + '<a class="sceditor-more">' + + editor._('More') + '</a>' + ).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__)); + + +/***/ } +/******/ ]);
\ No newline at end of file |
