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

Sets the width and/or height of the editor.

+ * + *

If width or height is not numeric it is ignored.

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

Sets the width and/or height of the editor.

+ * + *

If width or height is not numeric it is ignored.

+ * + *

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

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

Inserts HTML into WYSIWYG editor.

+ * + *

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

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

Inserts text into the WYSIWYG or source editor depending on which + * mode the editor is in.

+ * + *

If endText is specified any selected text will be placed between + * text and endText. If no text is selected text and endText will + * just be concatenate together.

+ * + * @param {String} text + * @param {String} [endText=null] + * @since 1.3.5 + * @function + * @name insertText + * @memberOf jQuery.sceditor.prototype + */ + base.insertText = function (text, endText) { + if (base.inSourceMode()) { + base.sourceEditorInsertText(text, endText); + } else { + base.wysiwygEditorInsertText(text, endText); + } + + return base; + }; + + /** + *

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

+ * + *

If endText is specified any selected text will be placed between + * text and endText. If no text is selected text and endText will + * just be concatenate together.

+ * + *

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

+ * + *

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

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

Gets the value of the editor.

+ * + *

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

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

Sets the value of the editor.

+ * + *

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

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

Inserts HTML/BBCode into the editor

+ * + *

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

+ * + *

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

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

Inserts HTML/BBCode into the editor

+ * + *

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

+ * + *

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

+ * + *

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

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

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

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

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

+ * + *

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

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

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

+ * + *

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

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

or

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

Binds a handler to the specified events

+ * + *

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

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

+ * + *

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

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

Adds a handler to the node changed event.

+ * + *

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

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

Adds a handler to the selection changed event

+ * + *

Happens whenever the selection changes in WYSIWYG mode.

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

Adds a handler to the value changed event

+ * + *

Happens whenever the current editor value changes.

+ * + *

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

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

' + + '', + + toolbarButton: '' + + '
{dispName}
', + + emoticon: '', + + fontOpt: '{font}', + + sizeOpt: '{size}', + + pastetext: + '
' + + '
' + + '
' + + '
', + + table: + '
' + + '
' + + '
', + + image: + '
' + + '
' + + '
' + + '
' + + '
' + + '
' + + '
' + + '
', + + email: + '
' + + '
' + + '
' + + '
' + + '
' + + '
', + + link: + '
' + + '
' + + '
' + + '
' + + '
', + + youtubeMenu: + '
' + + '
' + + '
' + + '
', + + youtube: + '' + }; + + /** + *

Replaces any params in a template with the passed params.

+ * + *

If createHtml is passed it will use jQuery to create the HTML. The + * same as doing: $(editor.tmpl("html", {params...}));

+ * + * @param {string} name + * @param {Object} params + * @param {Boolean} createHtml + * @private + */ + return function (name, params, createHtml) { + var template = _templates[name]; + + $.each(params, function (name, val) { + template = template.replace( + new RegExp('\\{' + name + '\\}', 'g'), val + ); + }); + + if (createHtml) { + template = $(template); + } + + return template; + }; + }.call(exports, __webpack_require__, exports, module), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__)); + + +/***/ }, +/* 9 */ +/***/ function(module, exports, __webpack_require__) { + + var __WEBPACK_AMD_DEFINE_RESULT__;!(__WEBPACK_AMD_DEFINE_RESULT__ = function (require) { + 'use strict'; + + var $ = __webpack_require__(1); + var IE_VER = __webpack_require__(6).ie; + var _tmpl = __webpack_require__(8); + + // In IE < 11 a BR at the end of a block level element + // causes a line break. In all other browsers it's collapsed. + var IE_BR_FIX = IE_VER && IE_VER < 11; + + + /** + * Map of all the commands for SCEditor + * @type {Object} + * @name commands + * @memberOf jQuery.sceditor + */ + var defaultCommnds = { + // START_COMMAND: Bold + bold: { + exec: 'bold', + tooltip: 'Bold', + shortcut: 'Ctrl+B' + }, + // END_COMMAND + // START_COMMAND: Italic + italic: { + exec: 'italic', + tooltip: 'Italic', + shortcut: 'Ctrl+I' + }, + // END_COMMAND + // START_COMMAND: Underline + underline: { + exec: 'underline', + tooltip: 'Underline', + shortcut: 'Ctrl+U' + }, + // END_COMMAND + // START_COMMAND: Strikethrough + strike: { + exec: 'strikethrough', + tooltip: 'Strikethrough' + }, + // END_COMMAND + // START_COMMAND: Subscript + subscript: { + exec: 'subscript', + tooltip: 'Subscript' + }, + // END_COMMAND + // START_COMMAND: Superscript + superscript: { + exec: 'superscript', + tooltip: 'Superscript' + }, + // END_COMMAND + + // START_COMMAND: Left + left: { + exec: 'justifyleft', + tooltip: 'Align left' + }, + // END_COMMAND + // START_COMMAND: Centre + center: { + exec: 'justifycenter', + tooltip: 'Center' + }, + // END_COMMAND + // START_COMMAND: Right + right: { + exec: 'justifyright', + tooltip: 'Align right' + }, + // END_COMMAND + // START_COMMAND: Justify + justify: { + exec: 'justifyfull', + tooltip: 'Justify' + }, + // END_COMMAND + + // START_COMMAND: Font + font: { + _dropDown: function (editor, caller, callback) { + var fontIdx = 0, + fonts = editor.opts.fonts.split(','), + content = $('
'), + /** @private */ + clickFunc = function () { + callback($(this).data('font')); + editor.closeDropDown(true); + return false; + }; + + for (; fontIdx < fonts.length; fontIdx++) { + content.append( + _tmpl('fontOpt', { + font: fonts[fontIdx] + }, true).click(clickFunc) + ); + } + + editor.createDropDown(caller, 'font-picker', content); + }, + exec: function (caller) { + var editor = this; + + defaultCommnds.font._dropDown( + editor, + caller, + function (fontName) { + editor.execCommand('fontname', fontName); + } + ); + }, + tooltip: 'Font Name' + }, + // END_COMMAND + // START_COMMAND: Size + size: { + _dropDown: function (editor, caller, callback) { + var content = $('
'), + /** @private */ + clickFunc = function (e) { + callback($(this).data('size')); + editor.closeDropDown(true); + e.preventDefault(); + }; + + for (var i = 1; i <= 7; i++) { + content.append(_tmpl('sizeOpt', { + size: i + }, true).click(clickFunc)); + } + + editor.createDropDown(caller, 'fontsize-picker', content); + }, + exec: function (caller) { + var editor = this; + + defaultCommnds.size._dropDown( + editor, + caller, + function (fontSize) { + editor.execCommand('fontsize', fontSize); + } + ); + }, + tooltip: 'Font Size' + }, + // END_COMMAND + // START_COMMAND: Colour + color: { + _dropDown: function (editor, caller, callback) { + var i, x, color, colors, + genColor = {r: 255, g: 255, b: 255}, + content = $('
'), + colorColumns = editor.opts.colors ? + editor.opts.colors.split('|') : new Array(21), + // IE is slow at string concatenation so use an array + html = [], + cmd = defaultCommnds.color; + + if (!cmd._htmlCache) { + for (i = 0; i < colorColumns.length; ++i) { + colors = colorColumns[i] ? + colorColumns[i].split(',') : new Array(21); + + html.push('
'); + + for (x = 0; x < colors.length; ++x) { + // use pre defined colour if can otherwise use the + // generated color + color = colors[x] || '#' + + genColor.r.toString(16) + + genColor.g.toString(16) + + genColor.b.toString(16); + + html.push( + '' + ); + + if (x % 5 === 0) { + genColor.g -= 51; + genColor.b = 255; + } else { + genColor.b -= 51; + } + } + + html.push('
'); + + if (i % 5 === 0) { + genColor.r -= 51; + genColor.g = 255; + genColor.b = 255; + } else { + genColor.g = 255; + genColor.b = 255; + } + } + + cmd._htmlCache = html.join(''); + } + + content.append(cmd._htmlCache) + .find('a') + .click(function (e) { + callback($(this).attr('data-color')); + editor.closeDropDown(true); + e.preventDefault(); + }); + + editor.createDropDown(caller, 'color-picker', content); + }, + exec: function (caller) { + var editor = this; + + defaultCommnds.color._dropDown( + editor, + caller, + function (color) { + editor.execCommand('forecolor', color); + } + ); + }, + tooltip: 'Font Color' + }, + // END_COMMAND + // START_COMMAND: Remove Format + removeformat: { + exec: 'removeformat', + tooltip: 'Remove Formatting' + }, + // END_COMMAND + + // START_COMMAND: Cut + cut: { + exec: 'cut', + tooltip: 'Cut', + errorMessage: 'Your browser does not allow the cut command. ' + + 'Please use the keyboard shortcut Ctrl/Cmd-X' + }, + // END_COMMAND + // START_COMMAND: Copy + copy: { + exec: 'copy', + tooltip: 'Copy', + errorMessage: 'Your browser does not allow the copy command. ' + + 'Please use the keyboard shortcut Ctrl/Cmd-C' + }, + // END_COMMAND + // START_COMMAND: Paste + paste: { + exec: 'paste', + tooltip: 'Paste', + errorMessage: 'Your browser does not allow the paste command. ' + + 'Please use the keyboard shortcut Ctrl/Cmd-V' + }, + // END_COMMAND + // START_COMMAND: Paste Text + pastetext: { + exec: function (caller) { + var val, content, + editor = this; + + content = _tmpl('pastetext', { + label: editor._( + 'Paste your text inside the following box:' + ), + insert: editor._('Insert') + }, true); + + content.find('.button').click(function (e) { + val = content.find('#txt').val(); + + if (val) { + editor.wysiwygEditorInsertText(val); + } + + editor.closeDropDown(true); + e.preventDefault(); + }); + + editor.createDropDown(caller, 'pastetext', content); + }, + tooltip: 'Paste Text' + }, + // END_COMMAND + // START_COMMAND: Bullet List + bulletlist: { + exec: 'insertunorderedlist', + tooltip: 'Bullet list' + }, + // END_COMMAND + // START_COMMAND: Ordered List + orderedlist: { + exec: 'insertorderedlist', + tooltip: 'Numbered list' + }, + // END_COMMAND + // START_COMMAND: Indent + indent: { + state: function (parents, firstBlock) { + // Only works with lists, for now + // This is a nested list, so it will always work + var range, startParent, endParent, + $firstBlock = $(firstBlock), + parentLists = $firstBlock.parents('ul,ol,menu'), + parentList = parentLists.first(); + + // in case it's a list with only a single
  • + if (parentLists.length > 1 || + parentList.children().length > 1) { + return 0; + } + + if ($firstBlock.is('ul,ol,menu')) { + // if the whole list is selected, then this must be + // invalidated because the browser will place a + //
    there + range = this.getRangeHelper().selectedRange(); + + if (window.Range && range instanceof Range) { + startParent = range.startContainer.parentNode; + endParent = range.endContainer.parentNode; + + // TODO: could use nodeType for this? + // Maybe just check the firstBlock contains both the start and end containers + // Select the tag, not the textNode + // (that's why the parentNode) + if (startParent !== + startParent.parentNode.firstElementChild || + // work around a bug in FF + ($(endParent).is('li') && endParent !== + endParent.parentNode.lastElementChild)) { + return 0; + } + // it's IE... As it is impossible to know well when to + // accept, better safe than sorry + } else { + return $firstBlock.is('li,ul,ol,menu') ? 0 : -1; + } + } + + return -1; + }, + exec: function () { + var editor = this, + $elm = $(editor.getRangeHelper().getFirstBlockParent()); + + editor.focus(); + + // An indent system is quite complicated as there are loads + // of complications and issues around how to indent text + // As default, let's just stay with indenting the lists, + // at least, for now. + if ($elm.parents('ul,ol,menu')) { + editor.execCommand('indent'); + } + }, + tooltip: 'Add indent' + }, + // END_COMMAND + // START_COMMAND: Outdent + outdent: { + state: function (parents, firstBlock) { + return $(firstBlock).is('ul,ol,menu') || + $(firstBlock).parents('ul,ol,menu').length > 0 ? 0 : -1; + }, + exec: function () { + var editor = this, + $elm = $(editor.getRangeHelper().getFirstBlockParent()); + + if ($elm.parents('ul,ol,menu')) { + editor.execCommand('outdent'); + } + }, + tooltip: 'Remove one indent' + }, + // END_COMMAND + + // START_COMMAND: Table + table: { + forceNewLineAfter: ['table'], + exec: function (caller) { + var editor = this, + content = _tmpl('table', { + rows: editor._('Rows:'), + cols: editor._('Cols:'), + insert: editor._('Insert') + }, true); + + content.find('.button').click(function (e) { + var row, col, + rows = content.find('#rows').val() - 0, + cols = content.find('#cols').val() - 0, + html = ''; + + if (rows < 1 || cols < 1) { + return; + } + + for (row = 0; row < rows; row++) { + html += ''; + + for (col = 0; col < cols; col++) { + html += ''; + } + + html += ''; + } + + html += '
    ' + + (IE_BR_FIX ? '' : '
    ') + + '
    '; + + editor.wysiwygEditorInsertHtml(html); + editor.closeDropDown(true); + e.preventDefault(); + }); + + editor.createDropDown(caller, 'inserttable', content); + }, + tooltip: 'Insert a table' + }, + // END_COMMAND + + // START_COMMAND: Horizontal Rule + horizontalrule: { + exec: 'inserthorizontalrule', + tooltip: 'Insert a horizontal rule' + }, + // END_COMMAND + + // START_COMMAND: Code + code: { + forceNewLineAfter: ['code'], + exec: function () { + this.wysiwygEditorInsertHtml( + '', + (IE_BR_FIX ? '' : '
    ') + '
    ' + ); + }, + tooltip: 'Code' + }, + // END_COMMAND + + // START_COMMAND: Image + image: { + exec: function (caller) { + var editor = this, + content = _tmpl('image', { + url: editor._('URL:'), + width: editor._('Width (optional):'), + height: editor._('Height (optional):'), + insert: editor._('Insert') + }, true); + + content.find('.button').click(function (e) { + var val = content.find('#image').val(), + width = content.find('#width').val(), + height = content.find('#height').val(), + attrs = ''; + + if (width) { + attrs += ' width="' + width + '"'; + } + + if (height) { + attrs += ' height="' + height + '"'; + } + + if (val) { + editor.wysiwygEditorInsertHtml( + '' + ); + } + + editor.closeDropDown(true); + e.preventDefault(); + }); + + editor.createDropDown(caller, 'insertimage', content); + }, + tooltip: 'Insert an image' + }, + // END_COMMAND + + // START_COMMAND: E-mail + email: { + exec: function (caller) { + var editor = this, + content = _tmpl('email', { + label: editor._('E-mail:'), + desc: editor._('Description (optional):'), + insert: editor._('Insert') + }, true); + + content.find('.button').click(function (e) { + var val = content.find('#email').val(), + description = content.find('#des').val(); + + if (val) { + // needed for IE to reset the last range + editor.focus(); + + if (!editor.getRangeHelper().selectedHtml() || + description) { + description = description || val; + + editor.wysiwygEditorInsertHtml( + '' + + description + + '' + ); + } else { + editor.execCommand('createlink', 'mailto:' + val); + } + } + + editor.closeDropDown(true); + e.preventDefault(); + }); + + editor.createDropDown(caller, 'insertemail', content); + }, + tooltip: 'Insert an email' + }, + // END_COMMAND + + // START_COMMAND: Link + link: { + exec: function (caller) { + var editor = this, + content = _tmpl('link', { + url: editor._('URL:'), + desc: editor._('Description (optional):'), + ins: editor._('Insert') + }, true); + + content.find('.button').click(function (e) { + var val = content.find('#link').val(), + description = content.find('#des').val(); + + if (val) { + // needed for IE to restore the last range + editor.focus(); + + // If there is no selected text then must set the URL as + // the text. Most browsers do this automatically, sadly + // IE doesn't. + if (!editor.getRangeHelper().selectedHtml() || + description) { + description = description || val; + + editor.wysiwygEditorInsertHtml( + '' + description + '' + ); + } else { + editor.execCommand('createlink', val); + } + } + + editor.closeDropDown(true); + e.preventDefault(); + }); + + editor.createDropDown(caller, 'insertlink', content); + }, + tooltip: 'Insert a link' + }, + // END_COMMAND + + // START_COMMAND: Unlink + unlink: { + state: function () { + var $current = $(this.currentNode()); + return $current.is('a') || + $current.parents('a').length > 0 ? 0 : -1; + }, + exec: function () { + var $current = $(this.currentNode()), + $anchor = $current.is('a') ? $current : + $current.parents('a').first(); + + if ($anchor.length) { + $anchor.replaceWith($anchor.contents()); + } + }, + tooltip: 'Unlink' + }, + // END_COMMAND + + + // START_COMMAND: Quote + quote: { + forceNewLineAfter: ['blockquote'], + exec: function (caller, html, author) { + var before = '
    ', + end = '
    '; + + // if there is HTML passed set end to null so any selected + // text is replaced + if (html) { + author = (author ? '' + author + '' : ''); + before = before + author + html + end; + end = null; + // if not add a newline to the end of the inserted quote + } else if (this.getRangeHelper().selectedHtml() === '') { + end = (IE_BR_FIX ? '' : '
    ') + end; + } + + this.wysiwygEditorInsertHtml(before, end); + }, + tooltip: 'Insert a Quote' + }, + // END_COMMAND + + // START_COMMAND: Emoticons + emoticon: { + exec: function (caller) { + var editor = this; + + var createContent = function (includeMore) { + var $moreLink, + emoticonsCompat = editor.opts.emoticonsCompat, + rangeHelper = editor.getRangeHelper(), + startSpace = emoticonsCompat && + rangeHelper.getOuterText(true, 1) !== ' ' ? + ' ' : '', + endSpace = emoticonsCompat && + rangeHelper.getOuterText(false, 1) !== ' ' ? + ' ' : '', + $content = $('
    '), + $line = $('
    ').appendTo($content), + perLine = 0, + emoticons = $.extend( + {}, + editor.opts.emoticons.dropdown, + includeMore ? editor.opts.emoticons.more : {} + ); + + $.each(emoticons, function () { + perLine++; + }); + perLine = Math.sqrt(perLine); + + $.each(emoticons, function (code, emoticon) { + $line.append( + $('').attr({ + src: emoticon.url || emoticon, + alt: code, + title: emoticon.tooltip || code + }).click(function () { + editor.insert(startSpace + $(this).attr('alt') + + endSpace, null, false).closeDropDown(true); + + return false; + }) + ); + + if ($line.children().length >= perLine) { + $line = $('
    ').appendTo($content); + } + }); + + if (!includeMore && editor.opts.emoticons.more) { + $moreLink = $( + '' + + editor._('More') + '' + ).click(function () { + editor.createDropDown( + caller, + 'more-emoticons', + createContent(true) + ); + + return false; + }); + + $content.append($moreLink); + } + + return $content; + }; + + editor.createDropDown( + caller, + 'emoticons', + createContent(false) + ); + }, + txtExec: function (caller) { + defaultCommnds.emoticon.exec.call(this, caller); + }, + tooltip: 'Insert an emoticon' + }, + // END_COMMAND + + // START_COMMAND: YouTube + youtube: { + _dropDown: function (editor, caller, handleIdFunc) { + var matches, + content = _tmpl('youtubeMenu', { + label: editor._('Video URL:'), + insert: editor._('Insert') + }, true); + + content.find('.button').click(function (e) { + var val = content + .find('#link') + .val(); + + if (val) { + matches = val.match( + /(?:v=|v\/|embed\/|youtu.be\/)(.{11})/ + ); + + if (matches) { + val = matches[1]; + } + + if (/^[a-zA-Z0-9_\-]{11}$/.test(val)) { + handleIdFunc(val); + } else { + /*global alert:false*/ + alert('Invalid YouTube video'); + } + } + + editor.closeDropDown(true); + e.preventDefault(); + }); + + editor.createDropDown(caller, 'insertlink', content); + }, + exec: function (caller) { + var editor = this; + + defaultCommnds.youtube._dropDown( + editor, + caller, + function (id) { + editor.wysiwygEditorInsertHtml(_tmpl('youtube', { + id: id + })); + } + ); + }, + tooltip: 'Insert a YouTube video' + }, + // END_COMMAND + + // START_COMMAND: Date + date: { + _date: function (editor) { + var now = new Date(), + year = now.getYear(), + month = now.getMonth() + 1, + day = now.getDate(); + + if (year < 2000) { + year = 1900 + year; + } + + if (month < 10) { + month = '0' + month; + } + + if (day < 10) { + day = '0' + day; + } + + return editor.opts.dateFormat + .replace(/year/i, year) + .replace(/month/i, month) + .replace(/day/i, day); + }, + exec: function () { + this.insertText(defaultCommnds.date._date(this)); + }, + txtExec: function () { + this.insertText(defaultCommnds.date._date(this)); + }, + tooltip: 'Insert current date' + }, + // END_COMMAND + + // START_COMMAND: Time + time: { + _time: function () { + var now = new Date(), + hours = now.getHours(), + mins = now.getMinutes(), + secs = now.getSeconds(); + + if (hours < 10) { + hours = '0' + hours; + } + + if (mins < 10) { + mins = '0' + mins; + } + + if (secs < 10) { + secs = '0' + secs; + } + + return hours + ':' + mins + ':' + secs; + }, + exec: function () { + this.insertText(defaultCommnds.time._time()); + }, + txtExec: function () { + this.insertText(defaultCommnds.time._time()); + }, + tooltip: 'Insert current time' + }, + // END_COMMAND + + + // START_COMMAND: Ltr + ltr: { + state: function (parents, firstBlock) { + return firstBlock && firstBlock.style.direction === 'ltr'; + }, + exec: function () { + var editor = this, + elm = editor.getRangeHelper().getFirstBlockParent(), + $elm = $(elm); + + editor.focus(); + + if (!elm || $elm.is('body')) { + editor.execCommand('formatBlock', 'p'); + + elm = editor.getRangeHelper().getFirstBlockParent(); + $elm = $(elm); + + if (!elm || $elm.is('body')) { + return; + } + } + + if ($elm.css('direction') === 'ltr') { + $elm.css('direction', ''); + } else { + $elm.css('direction', 'ltr'); + } + }, + tooltip: 'Left-to-Right' + }, + // END_COMMAND + + // START_COMMAND: Rtl + rtl: { + state: function (parents, firstBlock) { + return firstBlock && firstBlock.style.direction === 'rtl'; + }, + exec: function () { + var editor = this, + elm = editor.getRangeHelper().getFirstBlockParent(), + $elm = $(elm); + + editor.focus(); + + if (!elm || $elm.is('body')) { + editor.execCommand('formatBlock', 'p'); + + elm = editor.getRangeHelper().getFirstBlockParent(); + $elm = $(elm); + + if (!elm || $elm.is('body')) { + return; + } + } + + if ($elm.css('direction') === 'rtl') { + $elm.css('direction', ''); + } else { + $elm.css('direction', 'rtl'); + } + }, + tooltip: 'Right-to-Left' + }, + // END_COMMAND + + + // START_COMMAND: Print + print: { + exec: 'print', + tooltip: 'Print' + }, + // END_COMMAND + + // START_COMMAND: Maximize + maximize: { + state: function () { + return this.maximize(); + }, + exec: function () { + this.maximize(!this.maximize()); + }, + txtExec: function () { + this.maximize(!this.maximize()); + }, + tooltip: 'Maximize', + shortcut: 'Ctrl+Shift+M' + }, + // END_COMMAND + + // START_COMMAND: Source + source: { + state: function () { + return this.sourceMode(); + }, + exec: function () { + this.toggleSourceMode(); + }, + txtExec: function () { + this.toggleSourceMode(); + }, + tooltip: 'View source', + shortcut: 'Ctrl+Shift+S' + }, + // END_COMMAND + + // this is here so that commands above can be removed + // without having to remove the , after the last one. + // Needed for IE. + ignore: {} + }; + + return defaultCommnds; + }.call(exports, __webpack_require__, exports, module), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__)); + + +/***/ }, +/* 10 */ +/***/ function(module, exports, __webpack_require__) { + + var __WEBPACK_AMD_DEFINE_RESULT__;!(__WEBPACK_AMD_DEFINE_RESULT__ = function (require) { + 'use strict'; + + var $ = __webpack_require__(1); + + + /** + * Default options for SCEditor + * @type {Object} + */ + return { + /** @lends jQuery.sceditor.defaultOptions */ + /** + * Toolbar buttons order and groups. Should be comma separated and + * have a bar | to separate groups + * + * @type {String} + */ + toolbar: 'bold,italic,underline,strike,subscript,superscript|' + + 'left,center,right,justify|font,size,color,removeformat|' + + 'cut,copy,paste,pastetext|bulletlist,orderedlist,indent,outdent|' + + 'table|code,quote|horizontalrule,image,email,link,unlink|' + + 'emoticon,youtube,date,time|ltr,rtl|print,maximize,source', + + /** + * Comma separated list of commands to excludes from the toolbar + * + * @type {String} + */ + toolbarExclude: null, + + /** + * Stylesheet to include in the WYSIWYG editor. This is what will style + * the WYSIWYG elements + * + * @type {String} + */ + style: 'jquery.sceditor.default.css', + + /** + * Comma separated list of fonts for the font selector + * + * @type {String} + */ + fonts: 'Arial,Arial Black,Comic Sans MS,Courier New,Georgia,Impact,' + + 'Sans-serif,Serif,Times New Roman,Trebuchet MS,Verdana', + + /** + * Colors should be comma separated and have a bar | to signal a new + * column. + * + * If null the colors will be auto generated. + * + * @type {string} + */ + colors: null, + + /** + * The locale to use. + * @type {String} + */ + locale: $('html').attr('lang') || 'en', + + /** + * The Charset to use + * @type {String} + */ + charset: 'utf-8', + + /** + * Compatibility mode for emoticons. + * + * Helps if you have emoticons such as :/ which would put an emoticon + * inside http:// + * + * This mode requires emoticons to be surrounded by whitespace or end of + * line chars. This mode has limited As You Type emoticon conversion + * support. It will not replace AYT for end of line chars, only + * emoticons surrounded by whitespace. They will still be replaced + * correctly when loaded just not AYT. + * + * @type {Boolean} + */ + emoticonsCompat: false, + + /** + * If to enable emoticons. Can be changes at runtime using the + * emoticons() method. + * + * @type {Boolean} + * @since 1.4.2 + */ + emoticonsEnabled: true, + + /** + * Emoticon root URL + * + * @type {String} + */ + emoticonsRoot: '', + emoticons: { + dropdown: { + ':)': 'emoticons/smile.png', + ':angel:': 'emoticons/angel.png', + ':angry:': 'emoticons/angry.png', + '8-)': 'emoticons/cool.png', + ':\'(': 'emoticons/cwy.png', + ':ermm:': 'emoticons/ermm.png', + ':D': 'emoticons/grin.png', + '<3': 'emoticons/heart.png', + ':(': 'emoticons/sad.png', + ':O': 'emoticons/shocked.png', + ':P': 'emoticons/tongue.png', + ';)': 'emoticons/wink.png' + }, + more: { + ':alien:': 'emoticons/alien.png', + ':blink:': 'emoticons/blink.png', + ':blush:': 'emoticons/blush.png', + ':cheerful:': 'emoticons/cheerful.png', + ':devil:': 'emoticons/devil.png', + ':dizzy:': 'emoticons/dizzy.png', + ':getlost:': 'emoticons/getlost.png', + ':happy:': 'emoticons/happy.png', + ':kissing:': 'emoticons/kissing.png', + ':ninja:': 'emoticons/ninja.png', + ':pinch:': 'emoticons/pinch.png', + ':pouty:': 'emoticons/pouty.png', + ':sick:': 'emoticons/sick.png', + ':sideways:': 'emoticons/sideways.png', + ':silly:': 'emoticons/silly.png', + ':sleeping:': 'emoticons/sleeping.png', + ':unsure:': 'emoticons/unsure.png', + ':woot:': 'emoticons/w00t.png', + ':wassat:': 'emoticons/wassat.png' + }, + hidden: { + ':whistling:': 'emoticons/whistling.png', + ':love:': 'emoticons/wub.png' + } + }, + + /** + * Width of the editor. Set to null for automatic with + * + * @type {int} + */ + width: null, + + /** + * Height of the editor including toolbar. Set to null for automatic + * height + * + * @type {int} + */ + height: null, + + /** + * If to allow the editor to be resized + * + * @type {Boolean} + */ + resizeEnabled: true, + + /** + * Min resize to width, set to null for half textarea width or -1 for + * unlimited + * + * @type {int} + */ + resizeMinWidth: null, + /** + * Min resize to height, set to null for half textarea height or -1 for + * unlimited + * + * @type {int} + */ + resizeMinHeight: null, + /** + * Max resize to height, set to null for double textarea height or -1 + * for unlimited + * + * @type {int} + */ + resizeMaxHeight: null, + /** + * Max resize to width, set to null for double textarea width or -1 for + * unlimited + * + * @type {int} + */ + resizeMaxWidth: null, + /** + * If resizing by height is enabled + * + * @type {Boolean} + */ + resizeHeight: true, + /** + * If resizing by width is enabled + * + * @type {Boolean} + */ + resizeWidth: true, + + /** + * Date format, will be overridden if locale specifies one. + * + * The words year, month and day will be replaced with the users current + * year, month and day. + * + * @type {String} + */ + dateFormat: 'year-month-day', + + /** + * Element to inset the toolbar into. + * + * @type {HTMLElement} + */ + toolbarContainer: null, + + /** + * If to enable paste filtering. This is currently experimental, please + * report any issues. + * + * @type {Boolean} + */ + enablePasteFiltering: false, + + /** + * If to completely disable pasting into the editor + * + * @type {Boolean} + */ + disablePasting: false, + + /** + * If the editor is read only. + * + * @type {Boolean} + */ + readOnly: false, + + /** + * If to set the editor to right-to-left mode. + * + * If set to null the direction will be automatically detected. + * + * @type {Boolean} + */ + rtl: false, + + /** + * If to auto focus the editor on page load + * + * @type {Boolean} + */ + autofocus: false, + + /** + * If to auto focus the editor to the end of the content + * + * @type {Boolean} + */ + autofocusEnd: true, + + /** + * If to auto expand the editor to fix the content + * + * @type {Boolean} + */ + autoExpand: false, + + /** + * If to auto update original textbox on blur + * + * @type {Boolean} + */ + autoUpdate: false, + + /** + * If to enable the browsers built in spell checker + * + * @type {Boolean} + */ + spellcheck: true, + + /** + * If to run the source editor when there is no WYSIWYG support. Only + * really applies to mobile OS's. + * + * @type {Boolean} + */ + runWithoutWysiwygSupport: false, + + /** + * If to load the editor in source mode and still allow switching + * between WYSIWYG and source mode + * + * @type {Boolean} + */ + startInSourceMode: false, + + /** + * Optional ID to give the editor. + * + * @type {String} + */ + id: null, + + /** + * Comma separated list of plugins + * + * @type {String} + */ + plugins: '', + + /** + * z-index to set the editor container to. Needed for jQuery UI dialog. + * + * @type {Int} + */ + zIndex: null, + + /** + * If to trim the BBCode. Removes any spaces at the start and end of the + * BBCode string. + * + * @type {Boolean} + */ + bbcodeTrim: false, + + /** + * If to disable removing block level elements by pressing backspace at + * the start of them + * + * @type {Boolean} + */ + disableBlockRemove: false, + + /** + * BBCode parser options, only applies if using the editor in BBCode + * mode. + * + * See SCEditor.BBCodeParser.defaults for list of valid options + * + * @type {Object} + */ + parserOptions: { }, + + /** + * CSS that will be added to the to dropdown menu (eg. z-index) + * + * @type {Object} + */ + dropDownCss: { } + }; + }.call(exports, __webpack_require__, exports, module), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__)); + + +/***/ } +/******/ ]);;/** + * SCEditor BBCode Plugin + * http://www.sceditor.com/ + * + * Copyright (C) 2011-2014, Sam Clarke (samclarke.com) + * + * SCEditor is licensed under the MIT license: + * http://www.opensource.org/licenses/mit-license.php + * + * @fileoverview SCEditor BBCode Plugin + * @author Sam Clarke + * @requires jQuery + */ +/*global prompt: true*/ +/*jshint maxdepth: false*/ +// TODO: Tidy this code up and consider seperating the BBCode parser into a +// standalone module that can be used with other JS/NodeJS +(function ($, window, document) { + 'use strict'; + + var SCEditor = $.sceditor; + var sceditorPlugins = SCEditor.plugins; + var escapeEntities = SCEditor.escapeEntities; + var escapeUriScheme = SCEditor.escapeUriScheme; + + var IE_VER = SCEditor.ie; + + // In IE < 11 a BR at the end of a block level element + // causes a double line break. + var IE_BR_FIX = IE_VER && IE_VER < 11; + + + + var getEditorCommand = SCEditor.command.get; + + var defaultCommandsOverrides = { + bold: { + txtExec: ['[b]', '[/b]'] + }, + italic: { + txtExec: ['[i]', '[/i]'] + }, + underline: { + txtExec: ['[u]', '[/u]'] + }, + strike: { + txtExec: ['[s]', '[/s]'] + }, + subscript: { + txtExec: ['[sub]', '[/sub]'] + }, + superscript: { + txtExec: ['[sup]', '[/sup]'] + }, + left: { + txtExec: ['[left]', '[/left]'] + }, + center: { + txtExec: ['[center]', '[/center]'] + }, + right: { + txtExec: ['[right]', '[/right]'] + }, + justify: { + txtExec: ['[justify]', '[/justify]'] + }, + font: { + txtExec: function (caller) { + var editor = this; + + getEditorCommand('font')._dropDown( + editor, + caller, + function (fontName) { + editor.insertText( + '[font=' + fontName + ']', + '[/font]' + ); + } + ); + } + }, + size: { + txtExec: function (caller) { + var editor = this; + + getEditorCommand('size')._dropDown( + editor, + caller, + function (fontSize) { + editor.insertText( + '[size=' + fontSize + ']', + '[/size]' + ); + } + ); + } + }, + color: { + txtExec: function (caller) { + var editor = this; + + getEditorCommand('color')._dropDown( + editor, + caller, + function (color) { + editor.insertText( + '[color=' + color + ']', + '[/color]' + ); + } + ); + } + }, + bulletlist: { + txtExec: function (caller, selected) { + var content = ''; + + $.each(selected.split(/\r?\n/), function () { + content += (content ? '\n' : '') + + '[li]' + this + '[/li]'; + }); + + this.insertText('[ul]\n' + content + '\n[/ul]'); + } + }, + orderedlist: { + txtExec: function (caller, selected) { + var content = ''; + + $.each(selected.split(/\r?\n/), function () { + content += (content ? '\n' : '') + + '[li]' + this + '[/li]'; + }); + + sceditorPlugins.bbcode.bbcode.get(''); + + this.insertText('[ol]\n' + content + '\n[/ol]'); + } + }, + table: { + txtExec: ['[table][tr][td]', '[/td][/tr][/table]'] + }, + horizontalrule: { + txtExec: ['[hr]'] + }, + code: { + txtExec: ['[code]', '[/code]'] + }, + image: { + txtExec: function (caller, selected) { + var editor = this, + url = prompt(editor._('Enter the image URL:'), selected); + + if (url) { + editor.insertText('[img]' + url + '[/img]'); + } + } + }, + email: { + txtExec: function (caller, selected) { + var editor = this, + display = selected && selected.indexOf('@') > -1 ? + null : selected, + email = prompt(editor._('Enter the e-mail address:'), + (display ? '' : selected)), + text = prompt(editor._('Enter the displayed text:'), + display || email) || email; + + if (email) { + editor.insertText('[email=' + email + ']' + + text + '[/email]'); + } + } + }, + link: { + txtExec: function (caller, selected) { + var editor = this, + display = /^[a-z]+:\/\//i.test($.trim(selected)) ? + null : selected, + url = prompt(editor._('Enter URL:'), + (display ? 'http://' : $.trim(selected))), + text = prompt(editor._('Enter the displayed text:'), + display || url) || url; + + if (url) { + editor.insertText('[url=' + url + ']' + text + '[/url]'); + } + } + }, + quote: { + txtExec: ['[quote]', '[/quote]'] + }, + youtube: { + txtExec: function (caller) { + var editor = this; + + getEditorCommand('youtube')._dropDown( + editor, + caller, + function (id) { + editor.insertText('[youtube]' + id + '[/youtube]'); + } + ); + } + }, + rtl: { + txtExec: ['[rtl]', '[/rtl]'] + }, + ltr: { + txtExec: ['[ltr]', '[/ltr]'] + } + }; + + /** + * Removes any leading or trailing quotes ('") + * + * @return string + * @since v1.4.0 + */ + var _stripQuotes = function (str) { + return str ? + str.replace(/\\(.)/g, '$1').replace(/^(["'])(.*?)\1$/, '$2') : str; + }; + + /** + * Formats a string replacing {0}, {1}, {2}, ect. with + * the params provided + * + * @param {String} str The string to format + * @param {string} args... The strings to replace + * @return {String} + * @since v1.4.0 + */ + var _formatString = function () { + var undef; + var args = arguments; + + return args[0].replace(/\{(\d+)\}/g, function (str, p1) { + return args[p1 - 0 + 1] !== undef ? + args[p1 - 0 + 1] : + '{' + p1 + '}'; + }); + }; + + /** + * Enum of valid token types + * @type {Object} + * @private + */ + var TokenType = { + OPEN: 'open', + CONTENT: 'content', + NEWLINE: 'newline', + CLOSE: 'close' + }; + + + /** + * Tokenize token object + * + * @param {String} type The type of token this is, + * should be one of tokenType + * @param {String} name The name of this token + * @param {String} val The originally matched string + * @param {Array} attrs Any attributes. Only set on + * TokenType.OPEN tokens + * @param {Array} children Any children of this token + * @param {TokenizeToken} closing This tokens closing tag. + * Only set on TokenType.OPEN tokens + * @class TokenizeToken + * @name TokenizeToken + * @memberOf jQuery.sceditor.BBCodeParser.prototype + */ + var TokenizeToken = function ( + /*jshint maxparams: false*/ + type, name, val, attrs, children, closing + ) { + var base = this; + + base.type = type; + base.name = name; + base.val = val; + base.attrs = attrs || {}; + base.children = children || []; + base.closing = closing || null; + }; + + TokenizeToken.prototype = { + /** @lends jQuery.sceditor.BBCodeParser.prototype.TokenizeToken */ + /** + * Clones this token + * + * @param {Bool} includeChildren If to include the children in + * the clone. Defaults to false. + * @return {TokenizeToken} + */ + clone: function (includeChildren) { + var base = this; + + return new TokenizeToken( + base.type, + base.name, + base.val, + base.attrs, + includeChildren ? base.children : [], + base.closing ? base.closing.clone() : null + ); + }, + /** + * Splits this token at the specified child + * + * @param {TokenizeToken|Int} splitAt The child to split at or the + * index of the child + * @return {TokenizeToken} The right half of the split token or + * null if failed + */ + splitAt: function (splitAt) { + var clone; + var base = this; + var splitAtLength = 0; + var childrenLen = base.children.length; + + if (typeof splitAt !== 'number') { + splitAt = $.inArray(splitAt, base.children); + } + + if (splitAt < 0 || splitAt > childrenLen) { + return null; + } + + // Work out how many items are on the right side of the split + // to pass to splice() + while (childrenLen--) { + if (childrenLen >= splitAt) { + splitAtLength++; + } else { + childrenLen = 0; + } + } + + clone = base.clone(); + clone.children = base.children.splice(splitAt, splitAtLength); + return clone; + } + }; + + + /** + * SCEditor BBCode parser class + * + * @param {Object} options + * @class BBCodeParser + * @name jQuery.sceditor.BBCodeParser + * @since v1.4.0 + */ + var BBCodeParser = function (options) { + // make sure this is not being called as a function + if (!(this instanceof BBCodeParser)) { + return new BBCodeParser(options); + } + + var base = this; + + // Private methods + var init, + tokenizeTag, + tokenizeAttrs, + parseTokens, + normaliseNewLines, + fixNesting, + isChildAllowed, + removeEmpty, + fixChildren, + convertToHTML, + convertToBBCode, + hasTag, + quote, + lower, + last; + + + init = function () { + base.bbcodes = sceditorPlugins.bbcode.bbcodes; + base.opts = $.extend( + {}, + BBCodeParser.defaults, + options + ); + }; + + /** + * Takes a BBCode string and splits it into open, + * content and close tags. + * + * It does no checking to verify a tag has a matching open + * or closing tag or if the tag is valid child of any tag + * before it. For that the tokens should be passed to the + * parse function. + * + * @param {String} str + * @return {Array} + * @memberOf jQuery.sceditor.BBCodeParser.prototype + */ + base.tokenize = function (str) { + var matches, type, i; + var toks = []; + var tokens = [ + // Close must come before open as they are + // the same except close has a / at the start. + { + type: TokenType.CLOSE, + regex: /^\[\/[^\[\]]+\]/ + }, + { + type: TokenType.OPEN, + regex: /^\[[^\[\]]+\]/ + }, + { + type: TokenType.NEWLINE, + regex: /^(\r\n|\r|\n)/ + }, + { + type: TokenType.CONTENT, + regex: /^([^\[\r\n]+|\[)/ + } + ]; + + tokens.reverse(); + + strloop: + while (str.length) { + i = tokens.length; + while (i--) { + type = tokens[i].type; + + // Check if the string matches any of the tokens + if (!(matches = str.match(tokens[i].regex)) || + !matches[0]) { + continue; + } + + // Add the match to the tokens list + toks.push(tokenizeTag(type, matches[0])); + + // Remove the match from the string + str = str.substr(matches[0].length); + + // The token has been added so start again + continue strloop; + } + + // If there is anything left in the string which doesn't match + // any of the tokens then just assume it's content and add it. + if (str.length) { + toks.push(tokenizeTag(TokenType.CONTENT, str)); + } + + str = ''; + } + + return toks; + }; + + /** + * Extracts the name an params from a tag + * + * @param {tokenType} type + * @param {string} val + * @return {Object} + * @private + */ + tokenizeTag = function (type, val) { + var matches, attrs, name, + openRegex = /\[([^\]\s=]+)(?:([^\]]+))?\]/, + closeRegex = /\[\/([^\[\]]+)\]/; + + // Extract the name and attributes from opening tags and + // just the name from closing tags. + if (type === TokenType.OPEN && (matches = val.match(openRegex))) { + name = lower(matches[1]); + + if (matches[2] && (matches[2] = $.trim(matches[2]))) { + attrs = tokenizeAttrs(matches[2]); + } + } + + if (type === TokenType.CLOSE && (matches = val.match(closeRegex))) { + name = lower(matches[1]); + } + + if (type === TokenType.NEWLINE) { + name = '#newline'; + } + + // Treat all tokens without a name and + // all unknown BBCodes as content + if (!name || + ((type === TokenType.OPEN || type === TokenType.CLOSE) && + !sceditorPlugins.bbcode.bbcodes[name])) { + type = TokenType.CONTENT; + name = '#'; + } + + return new TokenizeToken(type, name, val, attrs); + }; + + /** + * Extracts the individual attributes from a string containing + * all the attributes. + * + * @param {String} attrs + * @return {Array} Assoc array of attributes + * @private + */ + tokenizeAttrs = function (attrs) { + var matches, + /* + ([^\s=]+) Anything that's not a space or equals + = Equals sign = + (?: + (?: + (["']) The opening quote + ( + (?:\\\2|[^\2])*? Anything that isn't the + unescaped opening quote + ) + \2 The opening quote again which + will close the string + ) + | If not a quoted string then match + ( + (?:.(?!\s\S+=))*.? Anything that isn't part of + [space][non-space][=] which + would be a new attribute + ) + ) + */ + attrRegex = + /([^\s=]+)=(?:(?:(["'])((?:\\\2|[^\2])*?)\2)|((?:.(?!\s\S+=))*.))/g, + ret = {}; + + // if only one attribute then remove the = from the start and + // strip any quotes + if (attrs.charAt(0) === '=' && attrs.indexOf('=', 1) < 0) { + ret.defaultattr = _stripQuotes(attrs.substr(1)); + } else { + if (attrs.charAt(0) === '=') { + attrs = 'defaultattr' + attrs; + } + + // No need to strip quotes here, the regex will do that. + while ((matches = attrRegex.exec(attrs))) { + ret[lower(matches[1])] = + _stripQuotes(matches[3]) || matches[4]; + } + } + + return ret; + }; + + /** + * Parses a string into an array of BBCodes + * + * @param {string} str + * @param {boolean} preserveNewLines If to preserve all new lines, not + * strip any based on the passed + * formatting options + * @return {Array} Array of BBCode objects + * @memberOf jQuery.sceditor.BBCodeParser.prototype + */ + base.parse = function (str, preserveNewLines) { + var ret = parseTokens(base.tokenize(str)); + var opts = base.opts; + + if (opts.fixInvalidChildren) { + fixChildren(ret); + } + + if (opts.removeEmptyTags) { + removeEmpty(ret); + } + + if (opts.fixInvalidNesting) { + fixNesting(ret); + } + + normaliseNewLines(ret, null, preserveNewLines); + + if (opts.removeEmptyTags) { + removeEmpty(ret); + } + + return ret; + }; + + /** + * Checks if an array of TokenizeToken's contains the + * specified token. + * + * Checks the tokens name and type match another tokens + * name and type in the array. + * + * @param {string} name + * @param {tokenType} type + * @param {Array} arr + * @return {Boolean} + * @private + */ + hasTag = function (name, type, arr) { + var i = arr.length; + + while (i--) { + if (arr[i].type === type && arr[i].name === name) { + return true; + } + } + + return false; + }; + + /** + * Checks if the child tag is allowed as one + * of the parent tags children. + * + * @param {TokenizeToken} parent + * @param {TokenizeToken} child + * @return {Boolean} + * @private + */ + isChildAllowed = function (parent, child) { + var parentBBCode = parent ? base.bbcodes[parent.name] : {}, + allowedChildren = parentBBCode.allowedChildren; + + if (base.opts.fixInvalidChildren && allowedChildren) { + return $.inArray(child.name || '#', allowedChildren) > -1; + } + + return true; + }; + +// TODO: Tidy this parseTokens() function up a bit. + /** + * Parses an array of tokens created by tokenize() + * + * @param {Array} toks + * @return {Array} Parsed tokens + * @see tokenize() + * @private + */ + parseTokens = function (toks) { + var token, bbcode, curTok, clone, i, previous, next, + cloned = [], + output = [], + openTags = [], + /** + * Returns the currently open tag or undefined + * @return {TokenizeToken} + */ + currentOpenTag = function () { + return last(openTags); + }, + /** + * Adds a tag to either the current tags children + * or to the output array. + * @param {TokenizeToken} token + * @private + */ + addTag = function (token) { + if (currentOpenTag()) { + currentOpenTag().children.push(token); + } else { + output.push(token); + } + }, + /** + * Checks if this tag closes the current tag + * @param {String} name + * @return {Void} + */ + closesCurrentTag = function (name) { + return currentOpenTag() && + (bbcode = base.bbcodes[currentOpenTag().name]) && + bbcode.closedBy && + $.inArray(name, bbcode.closedBy) > -1; + }; + + while ((token = toks.shift())) { + next = toks[0]; + + /* jshint indent:false */ + switch (token.type) { + case TokenType.OPEN: + // Check it this closes a parent, + // e.g. for lists [*]one [*]two + if (closesCurrentTag(token.name)) { + openTags.pop(); + } + + addTag(token); + bbcode = base.bbcodes[token.name]; + + // If this tag is not self closing and it has a closing + // tag then it is open and has children so add it to the + // list of open tags. If has the closedBy property then + // it is closed by other tags so include everything as + // it's children until one of those tags is reached. + if ((!bbcode || !bbcode.isSelfClosing) && + (bbcode.closedBy || + hasTag(token.name, TokenType.CLOSE, toks))) { + openTags.push(token); + } else if (!bbcode || !bbcode.isSelfClosing) { + token.type = TokenType.CONTENT; + } + break; + + case TokenType.CLOSE: + // check if this closes the current tag, + // e.g. [/list] would close an open [*] + if (currentOpenTag() && + token.name !== currentOpenTag().name && + closesCurrentTag('/' + token.name)) { + openTags.pop(); + } + + // If this is closing the currently open tag just pop + // the close tag off the open tags array + if (currentOpenTag() && + token.name === currentOpenTag().name) { + currentOpenTag().closing = token; + openTags.pop(); + + // If this is closing an open tag that is the parent of + // the current tag then clone all the tags including the + // current one until reaching the parent that is being + // closed. Close the parent and then add the clones back + // in. + } else if (hasTag(token.name, TokenType.OPEN, + openTags)) { + + // Remove the tag from the open tags + while ((curTok = openTags.pop())) { + + // If it's the tag that is being closed then + // discard it and break the loop. + if (curTok.name === token.name) { + curTok.closing = token; + break; + } + + // Otherwise clone this tag and then add any + // previously cloned tags as it's children + clone = curTok.clone(); + + if (cloned.length) { + clone.children.push(last(cloned)); + } + + cloned.push(clone); + } + + // Add the last cloned child to the now current tag + // (the parent of the tag which was being closed) + addTag(last(cloned)); + + // Add all the cloned tags to the open tags list + i = cloned.length; + while (i--) { + openTags.push(cloned[i]); + } + + cloned.length = 0; + + // This tag is closing nothing so treat it as content + } else { + token.type = TokenType.CONTENT; + addTag(token); + } + break; + + case TokenType.NEWLINE: + // handle things like + // [*]list\nitem\n[*]list1 + // where it should come out as + // [*]list\nitem[/*]\n[*]list1[/*] + // instead of + // [*]list\nitem\n[/*][*]list1[/*] + if (currentOpenTag() && next && + closesCurrentTag( + (next.type === TokenType.CLOSE ? '/' : '') + + next.name + )) { + // skip if the next tag is the closing tag for + // the option tag, i.e. [/*] + if (!(next.type === TokenType.CLOSE && + next.name === currentOpenTag().name)) { + bbcode = base.bbcodes[currentOpenTag().name]; + + if (bbcode && bbcode.breakAfter) { + openTags.pop(); + } else if (bbcode && + bbcode.isInline === false && + base.opts.breakAfterBlock && + bbcode.breakAfter !== false) { + openTags.pop(); + } + } + } + + addTag(token); + break; + + default: // content + addTag(token); + break; + } + + previous = token; + } + + return output; + }; + + /** + * Normalise all new lines + * + * Removes any formatting new lines from the BBCode + * leaving only content ones. I.e. for a list: + * + * [list] + * [*] list item one + * with a line break + * [*] list item two + * [/list] + * + * would become + * + * [list] [*] list item one + * with a line break [*] list item two [/list] + * + * Which makes it easier to convert to HTML or add + * the formatting new lines back in when converting + * back to BBCode + * + * @param {Array} children + * @param {TokenizeToken} parent + * @param {Bool} onlyRemoveBreakAfter + * @return {void} + */ + normaliseNewLines = function (children, parent, onlyRemoveBreakAfter) { + var token, left, right, parentBBCode, bbcode, + removedBreakEnd, removedBreakBefore, remove; + var childrenLength = children.length; +// TODO: this function really needs tidying up + if (parent) { + parentBBCode = base.bbcodes[parent.name]; + } + + var i = childrenLength; + while (i--) { + if (!(token = children[i])) { + continue; + } + + if (token.type === TokenType.NEWLINE) { + left = i > 0 ? children[i - 1] : null; + right = i < childrenLength - 1 ? children[i + 1] : null; + remove = false; + + // Handle the start and end new lines + // e.g. [tag]\n and \n[/tag] + if (!onlyRemoveBreakAfter && parentBBCode && + parentBBCode.isSelfClosing !== true) { + // First child of parent so must be opening line break + // (breakStartBlock, breakStart) e.g. [tag]\n + if (!left) { + if (parentBBCode.isInline === false && + base.opts.breakStartBlock && + parentBBCode.breakStart !== false) { + remove = true; + } + + if (parentBBCode.breakStart) { + remove = true; + } + // Last child of parent so must be end line break + // (breakEndBlock, breakEnd) + // e.g. \n[/tag] + // remove last line break (breakEndBlock, breakEnd) + } else if (!removedBreakEnd && !right) { + if (parentBBCode.isInline === false && + base.opts.breakEndBlock && + parentBBCode.breakEnd !== false) { + remove = true; + } + + if (parentBBCode.breakEnd) { + remove = true; + } + + removedBreakEnd = remove; + } + } + + if (left && left.type === TokenType.OPEN) { + if ((bbcode = base.bbcodes[left.name])) { + if (!onlyRemoveBreakAfter) { + if (bbcode.isInline === false && + base.opts.breakAfterBlock && + bbcode.breakAfter !== false) { + remove = true; + } + + if (bbcode.breakAfter) { + remove = true; + } + } else if (bbcode.isInline === false) { + remove = true; + } + } + } + + if (!onlyRemoveBreakAfter && !removedBreakBefore && + right && right.type === TokenType.OPEN) { + + if ((bbcode = base.bbcodes[right.name])) { + if (bbcode.isInline === false && + base.opts.breakBeforeBlock && + bbcode.breakBefore !== false) { + remove = true; + } + + if (bbcode.breakBefore) { + remove = true; + } + + removedBreakBefore = remove; + + if (remove) { + children.splice(i, 1); + continue; + } + } + } + + if (remove) { + children.splice(i, 1); + } + + // reset double removedBreakBefore removal protection. + // This is needed for cases like \n\n[\tag] where + // only 1 \n should be removed but without this they both + // would be. + removedBreakBefore = false; + } else if (token.type === TokenType.OPEN) { + normaliseNewLines(token.children, token, + onlyRemoveBreakAfter); + } + } + }; + + /** + * Fixes any invalid nesting. + * + * If it is a block level element inside 1 or more inline elements + * then those inline elements will be split at the point where the + * block level is and the block level element placed between the split + * parts. i.e. + * [inline]A[blocklevel]B[/blocklevel]C[/inline] + * Will become: + * [inline]A[/inline][blocklevel]B[/blocklevel][inline]C[/inline] + * + * @param {Array} children + * @param {Array} [parents] Null if there is no parents + * @param {Array} [insideInline] Boolean, if inside an inline element + * @param {Array} [rootArr] Root array if there is one + * @return {Array} + * @private + */ + fixNesting = function (children, parents, insideInline, rootArr) { + var token, i, parent, parentIndex, parentParentChildren, right; + + var isInline = function (token) { + var bbcode = base.bbcodes[token.name]; + + return !bbcode || bbcode.isInline !== false; + }; + + parents = parents || []; + rootArr = rootArr || children; + + // This must check the length each time as it can change when + // tokens are moved to fix the nesting. + for (i = 0; i < children.length; i++) { + if (!(token = children[i]) || token.type !== TokenType.OPEN) { + continue; + } + + if (!isInline(token) && insideInline) { + // if this is a blocklevel element inside an inline one then + // split the parent at the block level element + parent = last(parents); + right = parent.splitAt(token); + + parentParentChildren = parents.length > 1 ? + parents[parents.length - 2].children : rootArr; + + parentIndex = $.inArray(parent, parentParentChildren); + if (parentIndex > -1) { + // remove the block level token from the right side of + // the split inline element + right.children.splice( + $.inArray(token, right.children), 1); + + // insert the block level token and the right side after + // the left side of the inline token + parentParentChildren.splice( + parentIndex + 1, 0, token, right + ); + + // return to parents loop as the + // children have now increased + return; + } + + } + + parents.push(token); + + fixNesting( + token.children, + parents, + insideInline || isInline(token), + rootArr + ); + + parents.pop(token); + } + }; + + /** + * Fixes any invalid children. + * + * If it is an element which isn't allowed as a child of it's parent + * then it will be converted to content of the parent element. i.e. + * [code]Code [b]only[/b] allows text.[/code] + * Will become: + * Code [b]only[/b] allows text. + * Instead of: + * Code only allows text. + * + * @param {Array} children + * @param {Array} [parent] Null if there is no parents + * @private + */ + fixChildren = function (children, parent) { + var token, args; + + var i = children.length; + while (i--) { + if (!(token = children[i])) { + continue; + } + + if (!isChildAllowed(parent, token)) { + // if it is not then convert it to text and see if it + // is allowed + token.name = null; + token.type = TokenType.CONTENT; + + if (isChildAllowed(parent, token)) { + args = [i + 1, 0].concat(token.children); + + if (token.closing) { + token.closing.name = null; + token.closing.type = TokenType.CONTENT; + args.push(token.closing); + } + + i += args.length - 1; + Array.prototype.splice.apply(children, args); + } else { + parent.children.splice(i, 1); + } + } + + if (token.type === TokenType.OPEN) { + fixChildren(token.children, token); + } + } + }; + + /** + * Removes any empty BBCodes which are not allowed to be empty. + * + * @param {Array} tokens + * @private + */ + removeEmpty = function (tokens) { + var token, bbcode; + + /** + * Checks if all children are whitespace or not + * @private + */ + var isTokenWhiteSpace = function (children) { + var j = children.length; + + while (j--) { + var type = children[j].type; + + if (type === TokenType.OPEN || type === TokenType.CLOSE) { + return false; + } + + if (type === TokenType.CONTENT && + /\S|\u00A0/.test(children[j].val)) { + return false; + } + } + + return true; + }; + + var i = tokens.length; + while (i--) { + // So skip anything that isn't a tag since only tags can be + // empty, content can't + if (!(token = tokens[i]) || token.type !== TokenType.OPEN) { + continue; + } + + bbcode = base.bbcodes[token.name]; + + // Remove any empty children of this tag first so that if they + // are all removed this one doesn't think it's not empty. + removeEmpty(token.children); + + if (isTokenWhiteSpace(token.children) && bbcode && + !bbcode.isSelfClosing && !bbcode.allowsEmpty) { + tokens.splice.apply( + tokens, + $.merge([i, 1], token.children) + ); + } + } + }; + + /** + * Converts a BBCode string to HTML + * + * @param {String} str + * @param {Bool} preserveNewLines If to preserve all new lines, not + * strip any based on the passed + * formatting options + * @return {String} + * @memberOf jQuery.sceditor.BBCodeParser.prototype + */ + base.toHTML = function (str, preserveNewLines) { + return convertToHTML(base.parse(str, preserveNewLines), true); + }; + + /** + * @private + */ + convertToHTML = function (tokens, isRoot) { + var undef, token, bbcode, content, html, needsBlockWrap, + blockWrapOpen, isInline, lastChild, + ret = []; + + isInline = function (bbcode) { + return (!bbcode || (bbcode.isHtmlInline !== undef ? + bbcode.isHtmlInline : bbcode.isInline)) !== false; + }; + + while (tokens.length > 0) { + if (!(token = tokens.shift())) { + continue; + } + + if (token.type === TokenType.OPEN) { + lastChild = + token.children[token.children.length - 1] || {}; + bbcode = base.bbcodes[token.name]; + needsBlockWrap = isRoot && isInline(bbcode); + content = convertToHTML(token.children, false); + + if (bbcode && bbcode.html) { + // Only add a line break to the end if this is + // blocklevel and the last child wasn't block-level + if (!isInline(bbcode) && + isInline(base.bbcodes[lastChild.name]) && + !bbcode.isPreFormatted && + !bbcode.skipLastLineBreak) { + // Add placeholder br to end of block level elements + // in all browsers apart from IE < 9 which handle + // new lines differently and doesn't need one. + if (!IE_BR_FIX) { + content += '
    '; + } + } + + if (!$.isFunction(bbcode.html)) { + token.attrs['0'] = content; + html = sceditorPlugins.bbcode.formatBBCodeString( + bbcode.html, + token.attrs + ); + } else { + html = bbcode.html.call( + base, + token, + token.attrs, + content + ); + } + } else { + html = token.val + content + + (token.closing ? token.closing.val : ''); + } + } else if (token.type === TokenType.NEWLINE) { + if (!isRoot) { + ret.push('
    '); + continue; + } + + // If not already in a block wrap then start a new block + if (!blockWrapOpen) { + ret.push('
    '); + + // If it's an empty DIV and compatibility mode is below + // IE8 then we must add a non-breaking space to the div + // otherwise the div will be collapsed. Adding a BR + // works but when you press enter to make a newline it + // suddenly goes back to the normal IE div behavior and + // creates two lines, one for the newline and one for + // the BR. I'm sure there must be a better fix but I've + // yet to find one. + // Cannot do zoom: 1; or set a height on the div to fix + // it as that causes resize handles to be added to the + // div when it's clicked on. + if (IE_VER < 8 || (document.documentMode && + document.documentMode < 8)) { + ret.push('\u00a0'); + } + } + + // Putting BR in a div in IE causes it + // to do a double line break. + if (!IE_BR_FIX) { + ret.push('
    '); + } + + // Normally the div acts as a line-break with by moving + // whatever comes after onto a new line. + // If this is the last token, add an extra line-break so it + // shows as there will be nothing after it. + if (!tokens.length) { + ret.push('
    '); + } + + ret.push('
    \n'); + blockWrapOpen = false; + continue; + // content + } else { + needsBlockWrap = isRoot; + html = escapeEntities(token.val, true); + } + + if (needsBlockWrap && !blockWrapOpen) { + ret.push('
    '); + blockWrapOpen = true; + } else if (!needsBlockWrap && blockWrapOpen) { + ret.push('
    \n'); + blockWrapOpen = false; + } + + ret.push(html); + } + + if (blockWrapOpen) { + ret.push('
    \n'); + } + + return ret.join(''); + }; + + /** + * Takes a BBCode string, parses it then converts it back to BBCode. + * + * This will auto fix the BBCode and format it with the specified + * options. + * + * @param {String} str + * @param {Bool} preserveNewLines If to preserve all new lines, not + * strip any based on the passed + * formatting options + * @return {String} + * @memberOf jQuery.sceditor.BBCodeParser.prototype + */ + base.toBBCode = function (str, preserveNewLines) { + return convertToBBCode(base.parse(str, preserveNewLines)); + }; + + /** + * Converts parsed tokens back into BBCode with the + * formatting specified in the options and with any + * fixes specified. + * + * @param {Array} toks Array of parsed tokens from base.parse() + * @return {String} + * @private + */ + convertToBBCode = function (toks) { + var token, attr, bbcode, isBlock, isSelfClosing, quoteType, + breakBefore, breakStart, breakEnd, breakAfter, + // Create an array of strings which are joined together + // before being returned as this is faster in slow browsers. + // (Old versions of IE). + ret = []; + + while (toks.length > 0) { + if (!(token = toks.shift())) { + continue; + } +// TODO: tidy this + bbcode = base.bbcodes[token.name]; + isBlock = !(!bbcode || bbcode.isInline !== false); + isSelfClosing = bbcode && bbcode.isSelfClosing; + + breakBefore = (isBlock && base.opts.breakBeforeBlock && + bbcode.breakBefore !== false) || + (bbcode && bbcode.breakBefore); + + breakStart = (isBlock && !isSelfClosing && + base.opts.breakStartBlock && + bbcode.breakStart !== false) || + (bbcode && bbcode.breakStart); + + breakEnd = (isBlock && base.opts.breakEndBlock && + bbcode.breakEnd !== false) || + (bbcode && bbcode.breakEnd); + + breakAfter = (isBlock && base.opts.breakAfterBlock && + bbcode.breakAfter !== false) || + (bbcode && bbcode.breakAfter); + + quoteType = (bbcode ? bbcode.quoteType : null) || + base.opts.quoteType || BBCodeParser.QuoteType.auto; + + if (!bbcode && token.type === TokenType.OPEN) { + ret.push(token.val); + + if (token.children) { + ret.push(convertToBBCode(token.children)); + } + + if (token.closing) { + ret.push(token.closing.val); + } + } else if (token.type === TokenType.OPEN) { + if (breakBefore) { + ret.push('\n'); + } + + // Convert the tag and it's attributes to BBCode + ret.push('[' + token.name); + if (token.attrs) { + if (token.attrs.defaultattr) { + ret.push('=', quote( + token.attrs.defaultattr, + quoteType, + 'defaultattr' + )); + + delete token.attrs.defaultattr; + } + + for (attr in token.attrs) { + if (token.attrs.hasOwnProperty(attr)) { + ret.push(' ', attr, '=', + quote(token.attrs[attr], quoteType, attr)); + } + } + } + ret.push(']'); + + if (breakStart) { + ret.push('\n'); + } + + // Convert the tags children to BBCode + if (token.children) { + ret.push(convertToBBCode(token.children)); + } + + // add closing tag if not self closing + if (!isSelfClosing && !bbcode.excludeClosing) { + if (breakEnd) { + ret.push('\n'); + } + + ret.push('[/' + token.name + ']'); + } + + if (breakAfter) { + ret.push('\n'); + } + + // preserve whatever was recognized as the + // closing tag if it is a self closing tag + if (token.closing && isSelfClosing) { + ret.push(token.closing.val); + } + } else { + ret.push(token.val); + } + } + + return ret.join(''); + }; + + /** + * Quotes an attribute + * + * @param {String} str + * @param {BBCodeParser.QuoteType} quoteType + * @param {String} name + * @return {String} + * @private + */ + quote = function (str, quoteType, name) { + var QuoteTypes = BBCodeParser.QuoteType, + needsQuotes = /\s|=/.test(str); + + if ($.isFunction(quoteType)) { + return quoteType(str, name); + } + + if (quoteType === QuoteTypes.never || + (quoteType === QuoteTypes.auto && !needsQuotes)) { + return str; + } + + return '"' + str.replace('\\', '\\\\').replace('"', '\\"') + '"'; + }; + + /** + * Returns the last element of an array or null + * + * @param {Array} arr + * @return {Object} Last element + * @private + */ + last = function (arr) { + if (arr.length) { + return arr[arr.length - 1]; + } + + return null; + }; + + /** + * Converts a string to lowercase. + * + * @param {String} str + * @return {String} Lowercase version of str + * @private + */ + lower = function (str) { + return str.toLowerCase(); + }; + + init(); + }; + + /** + * Quote type + * @type {Object} + * @class QuoteType + * @name jQuery.sceditor.BBCodeParser.QuoteType + * @since v1.4.0 + */ + BBCodeParser.QuoteType = { + /** @lends jQuery.sceditor.BBCodeParser.QuoteType */ + /** + * Always quote the attribute value + * @type {Number} + */ + always: 1, + + /** + * Never quote the attributes value + * @type {Number} + */ + never: 2, + + /** + * Only quote the attributes value when it contains spaces to equals + * @type {Number} + */ + auto: 3 + }; + + /** + * Default BBCode parser options + * @type {Object} + */ + BBCodeParser.defaults = { + /** + * If to add a new line before block level elements + * + * @type {Boolean} + */ + breakBeforeBlock: false, + + /** + * If to add a new line after the start of block level elements + * + * @type {Boolean} + */ + breakStartBlock: false, + + /** + * If to add a new line before the end of block level elements + * + * @type {Boolean} + */ + breakEndBlock: false, + + /** + * If to add a new line after block level elements + * + * @type {Boolean} + */ + breakAfterBlock: true, + + /** + * If to remove empty tags + * + * @type {Boolean} + */ + removeEmptyTags: true, + + /** + * If to fix invalid nesting, + * i.e. block level elements inside inline elements. + * + * @type {Boolean} + */ + fixInvalidNesting: true, + + /** + * If to fix invalid children. + * i.e. A tag which is inside a parent that doesn't + * allow that type of tag. + * + * @type {Boolean} + */ + fixInvalidChildren: true, + + /** + * Attribute quote type + * + * @type {BBCodeParser.QuoteType} + * @since 1.4.1 + */ + quoteType: BBCodeParser.QuoteType.auto + }; + + /** + * Deprecated, use sceditorPlugins.bbcode + * + * @class sceditorBBCodePlugin + * @name jQuery.sceditor.sceditorBBCodePlugin + * @deprecated + */ + $.sceditorBBCodePlugin = + /** + * BBCode plugin for SCEditor + * + * @class bbcode + * @name jQuery.sceditor.plugins.bbcode + * @since 1.4.1 + */ + sceditorPlugins.bbcode = function () { + var base = this; + + /** + * Private methods + * @private + */ + var buildBbcodeCache, + handleStyles, + handleTags, + removeFirstLastDiv; + + base.bbcodes = sceditorPlugins.bbcode.bbcodes; + base.stripQuotes = _stripQuotes; + + /** + * cache of all the tags pointing to their bbcodes to enable + * faster lookup of which bbcode a tag should have + * @private + */ + var tagsToBBCodes = {}; + + /** + * Same as tagsToBBCodes but instead of HTML tags it's styles + * @private + */ + var stylesToBBCodes = {}; + + /** + * Allowed children of specific HTML tags. Empty array if no + * children other than text nodes are allowed + * @private + */ + var validChildren = { + ul: ['li', 'ol', 'ul'], + ol: ['li', 'ol', 'ul'], + table: ['tr'], + tr: ['td', 'th'], + code: ['br', 'p', 'div'] + }; + + /** + * Initializer + * @private + */ + base.init = function () { + base.opts = this.opts; + + // build the BBCode cache + buildBbcodeCache(); + + this.commands = $.extend( + true, {}, defaultCommandsOverrides, this.commands + ); + + // Add BBCode helper methods + this.toBBCode = base.signalToSource; + this.fromBBCode = base.signalToWysiwyg; + }; + + /** + * Populates tagsToBBCodes and stylesToBBCodes to enable faster lookups + * + * @private + */ + buildBbcodeCache = function () { + $.each(base.bbcodes, function (bbcode) { + var isBlock, + tags = base.bbcodes[bbcode].tags, + styles = base.bbcodes[bbcode].styles; + + if (tags) { + $.each(tags, function (tag, values) { + isBlock = base.bbcodes[bbcode].isInline === false; + + tagsToBBCodes[tag] = tagsToBBCodes[tag] || {}; + + tagsToBBCodes[tag][isBlock] = + tagsToBBCodes[tag][isBlock] || {}; + + tagsToBBCodes[tag][isBlock][bbcode] = values; + }); + } + + if (styles) { + $.each(styles, function (style, values) { + isBlock = base.bbcodes[bbcode].isInline === false; + + stylesToBBCodes[isBlock] = + stylesToBBCodes[isBlock] || {}; + + stylesToBBCodes[isBlock][style] = + stylesToBBCodes[isBlock][style] || {}; + + stylesToBBCodes[isBlock][style][bbcode] = values; + }); + } + }); + }; + + /** + * Checks if any bbcode styles match the elements styles + * + * @return string Content with any matching + * bbcode tags wrapped around it. + * @private + */ + handleStyles = function ($element, content, blockLevel) { + var styleValue, format, + getStyle = SCEditor.dom.getStyle; + + // convert blockLevel to boolean + blockLevel = !!blockLevel; + + if (!stylesToBBCodes[blockLevel]) { + return content; + } + + $.each(stylesToBBCodes[blockLevel], function (property, bbcodes) { + styleValue = getStyle($element[0], property); + + // if the parent has the same style use that instead of this one + // so you don't end up with [i]parent[i]child[/i][/i] + if (!styleValue || + getStyle($element.parent()[0], property) === styleValue) { + return; + } + + $.each(bbcodes, function (bbcode, values) { + if (!values || + $.inArray(styleValue.toString(), values) > -1) { + format = base.bbcodes[bbcode].format; + + if ($.isFunction(format)) { + content = format.call(base, $element, content); + } else { + content = _formatString(format, content); + } + } + }); + }); + + return content; + }; + + /** + * Handles a HTML tag and finds any matching bbcodes + * + * @param {jQuery} $element The element to convert + * @param {String} content The Tags text content + * @param {Bool} blockLevel If to convert block level tags + * @return {String} Content with any matching bbcode tags + * wrapped around it. + * @private + */ + handleTags = function ($element, content, blockLevel) { + var convertBBCode, format, + element = $element[0], + tag = element.nodeName.toLowerCase(); + + // convert blockLevel to boolean + blockLevel = !!blockLevel; + + if (tagsToBBCodes[tag] && tagsToBBCodes[tag][blockLevel]) { + // loop all bbcodes for this tag + $.each(tagsToBBCodes[tag][blockLevel], function ( + bbcode, bbcodeAttribs) { + // if the bbcode requires any attributes then check this has + // all needed + if (bbcodeAttribs) { + convertBBCode = false; + + // loop all the bbcode attribs + $.each(bbcodeAttribs, function (attrib, values) { + // Skip if the element doesn't have the attibue or + // the attribute doesn't match one of the require + // values + if (!$element.attr(attrib) || (values && + $.inArray($element.attr(attrib), values) < 0)) { + return; + } + + // break this loop as we have matched this bbcode + convertBBCode = true; + return false; + }); + + if (!convertBBCode) { + return; + } + } + + format = base.bbcodes[bbcode].format; + + if ($.isFunction(format)) { + content = format.call(base, $element, content); + } else { + content = _formatString(format, content); + } + }); + } + + var isInline = SCEditor.dom.isInline; + if (blockLevel && (!isInline(element, true) || tag === 'br')) { + var isLastBlockChild, parent, parentLastChild, + previousSibling = element.previousSibling; + + // Skips selection makers and ignored elements + // Skip empty inline elements + while (previousSibling && + previousSibling.nodeType === 1 && + !$(previousSibling).is('br') && + isInline(previousSibling, true) && + !previousSibling.firstChild) { + previousSibling = previousSibling.previousSibling; + } + + // If it's the last block of an inline that is the last + // child of a block then it shouldn't cause a line break + // except in IE < 11 + //
    + do { + parent = element.parentNode; + parentLastChild = parent.lastChild; + + isLastBlockChild = parentLastChild === element; + element = parent; + } while (parent && isLastBlockChild && isInline(parent, true)); + + // If this block is: + // * Not the last child of a block level element + // * Is a
  • tag (lists are blocks) + // * Is IE < 11 and the tag is BR. IE < 11 never collapses BR + // tags. + if (!isLastBlockChild || tag === 'li' || + (tag === 'br' && IE_BR_FIX)) { + content += '\n'; + } + + // Check for: + // texttext + // + // The second opening opening tag should cause a + // line break because the previous sibing is inline. + if (tag !== 'br' && previousSibling && + !$(previousSibling).is('br') && + isInline(previousSibling, true)) { + content = '\n' + content; + } + } + + return content; + }; + + /** + * Converts HTML to BBCode + * + * @param {String} html Html string, this function ignores this, + * it works off domBody + * @param {jQuery} $body Editors dom body object to convert + * @return {String} BBCode which has been converted from HTML + * @memberOf jQuery.plugins.bbcode.prototype + */ + base.signalToSource = function (html, $body) { + var $tmpContainer, bbcode, + parser = new BBCodeParser(base.opts.parserOptions); + + if (!$body) { + if (typeof html === 'string') { + $tmpContainer = $('
    ') + .css('visibility', 'hidden') + .appendTo(document.body) + .html(html); + + $body = $tmpContainer; + } else { + $body = $(html); + } + } + + if (!$body || !$body.jquery) { + return ''; + } + + SCEditor.dom.removeWhiteSpace($body[0]); + + // Remove all the stuff that is meant to be ignored + $('.sceditor-ignore', $body).remove(); + + bbcode = base.elementToBbcode($body); + + if ($tmpContainer) { + $tmpContainer.remove(); + } + + bbcode = parser.toBBCode(bbcode, true); + + if (base.opts.bbcodeTrim) { + bbcode = $.trim(bbcode); + } + + return bbcode; + }; + + /** + * Converts a HTML dom element to BBCode starting from + * the innermost element and working backwards + * + * @private + * @param {jQuery} $element The element to convert to BBCode + * @return {string} BBCode + * @memberOf jQuery.plugins.bbcode.prototype + */ + base.elementToBbcode = function ($element) { + var toBBCode = function (node, vChildren) { + var ret = ''; +// TODO: Move to BBCode class? + SCEditor.dom.traverse(node, function (node) { + var $node = $(node), + curTag = '', + nodeType = node.nodeType, + tag = node.nodeName.toLowerCase(), + vChild = validChildren[tag], + firstChild = node.firstChild, + isValidChild = true; + + if (typeof vChildren === 'object') { + isValidChild = $.inArray(tag, vChildren) > -1; + + // Emoticons should always be converted + if ($node.is('img') && + $node.data('sceditor-emoticon')) { + isValidChild = true; + } + + // if this tag is one of the parents allowed children + // then set this tags allowed children to whatever it + // allows, otherwise set to what the parent allows + if (!isValidChild) { + vChild = vChildren; + } + } + + // 3 = text and 1 = element + if (nodeType !== 3 && nodeType !== 1) { + return; + } + + if (nodeType === 1) { + // skip empty nlf elements (new lines automatically + // added after block level elements like quotes) + if ($node.hasClass('sceditor-nlf')) { + if (!firstChild || (!IE_BR_FIX && + node.childNodes.length === 1 && + /br/i.test(firstChild.nodeName))) { + return; + } + } + + // don't loop inside iframes + if (tag !== 'iframe') { + curTag = toBBCode(node, vChild); + } + +// TODO: isValidChild is no longer needed. Should use valid children bbcodes +// instead by creating BBCode tokens like the parser. + if (isValidChild) { + // code tags should skip most styles + if (tag !== 'code') { + // handle inline bbcodes + curTag = handleStyles($node, curTag); + curTag = handleTags($node, curTag); + + // handle blocklevel bbcodes + curTag = handleStyles($node, curTag, true); + } + + ret += handleTags($node, curTag, true); + } else { + ret += curTag; + } + } else { + ret += node.nodeValue; + } + }, false, true); + + return ret; + }; + + return toBBCode($element[0]); + }; + + /** + * Converts BBCode to HTML + * + * @param {String} text + * @param {Bool} asFragment + * @return {String} HTML + * @memberOf jQuery.plugins.bbcode.prototype + */ + base.signalToWysiwyg = function (text, asFragment) { + var parser = new BBCodeParser(base.opts.parserOptions), + html = parser.toHTML(base.opts.bbcodeTrim ? + $.trim(text) : text); + + return asFragment ? removeFirstLastDiv(html) : html; + }; + + /** + * Removes the first and last divs from the HTML. + * + * This is needed for pasting + * @param {String} html + * @return {String} + * @private + */ + removeFirstLastDiv = function (html) { + var node, next, removeDiv, + $output = $('
    ').hide().appendTo(document.body), + output = $output[0]; + + removeDiv = function (node, isFirst) { + // Don't remove divs that have styling + if (SCEditor.dom.hasStyling(node)) { + return; + } + + if (IE_BR_FIX || (node.childNodes.length !== 1 || + !$(node.firstChild).is('br'))) { + while ((next = node.firstChild)) { + output.insertBefore(next, node); + } + } + + if (isFirst) { + var lastChild = output.lastChild; + + if (node !== lastChild && $(lastChild).is('div') && + node.nextSibling === lastChild) { + output.insertBefore(document.createElement('br'), node); + } + } + + output.removeChild(node); + }; + + output.innerHTML = html.replace(/<\/div>\n/g, '
    '); + + if ((node = output.firstChild) && $(node).is('div')) { + removeDiv(node, true); + } + + if ((node = output.lastChild) && $(node).is('div')) { + removeDiv(node); + } + + output = output.innerHTML; + $output.remove(); + + return output; + }; + }; + + + + /** + * Formats a string replacing {name} with the values of + * obj.name properties. + * + * If there is no property for the specified {name} then + * it will be left intact. + * + * @param {String} str + * @param {Object} obj + * @return {String} + * @since 1.4.5 + */ + sceditorPlugins.bbcode.formatBBCodeString = function (str, obj) { + return str.replace(/\{([^}]+)\}/g, function (match, group) { + var undef, + escape = true; + + if (group.charAt(0) === '!') { + escape = false; + group = group.substring(1); + } + + if (group === '0') { + escape = false; + } + + if (obj[group] === undef) { + return match; + } + + return escape ? + escapeEntities(obj[group], true) : + obj[group]; + }); + }; + + /** + * Converts a number 0-255 to hex. + * + * Will return 00 if number is not a valid number. + * + * @param {Number} number + * @return {String} + * @private + */ + var toHex = function (number) { + number = parseInt(number, 10); + + if (isNaN(number)) { + return '00'; + } + + number = Math.max(0, Math.min(number, 255)).toString(16); + + return number.length < 2 ? '0' + number : number; + }; + + var _normaliseColour = function (colorStr) { + var match; + + colorStr = colorStr || '#000'; + + // rgb(n,n,n); + if ((match = + colorStr.match(/rgb\((\d{1,3}),\s*?(\d{1,3}),\s*?(\d{1,3})\)/i))) { + return '#' + + toHex(match[1]) + + toHex(match[2] - 0) + + toHex(match[3] - 0); + } + + // expand shorthand + if ((match = colorStr.match(/#([0-f])([0-f])([0-f])\s*?$/i))) { + return '#' + + match[1] + match[1] + + match[2] + match[2] + + match[3] + match[3]; + } + + return colorStr; + }; + + var bbcodes = { + // START_COMMAND: Bold + b: { + tags: { + b: null, + strong: null + }, + styles: { + // 401 is for FF 3.5 + 'font-weight': ['bold', 'bolder', '401', '700', '800', '900'] + }, + format: '[b]{0}[/b]', + html: '{0}' + }, + // END_COMMAND + + // START_COMMAND: Italic + i: { + tags: { + i: null, + em: null + }, + styles: { + 'font-style': ['italic', 'oblique'] + }, + format: '[i]{0}[/i]', + html: '{0}' + }, + // END_COMMAND + + // START_COMMAND: Underline + u: { + tags: { + u: null + }, + styles: { + 'text-decoration': ['underline'] + }, + format: '[u]{0}[/u]', + html: '{0}' + }, + // END_COMMAND + + // START_COMMAND: Strikethrough + s: { + tags: { + s: null, + strike: null + }, + styles: { + 'text-decoration': ['line-through'] + }, + format: '[s]{0}[/s]', + html: '{0}' + }, + // END_COMMAND + + // START_COMMAND: Subscript + sub: { + tags: { + sub: null + }, + format: '[sub]{0}[/sub]', + html: '{0}' + }, + // END_COMMAND + + // START_COMMAND: Superscript + sup: { + tags: { + sup: null + }, + format: '[sup]{0}[/sup]', + html: '{0}' + }, + // END_COMMAND + + // START_COMMAND: Font + font: { + tags: { + font: { + face: null + } + }, + styles: { + 'font-family': null + }, + quoteType: BBCodeParser.QuoteType.never, + format: function ($element, content) { + var font; + + if (!$element.is('font') || !(font = $element.attr('face'))) { + font = $element.css('font-family'); + } + + return '[font=' + _stripQuotes(font) + ']' + + content + '[/font]'; + }, + html: '{0}' + }, + // END_COMMAND + + // START_COMMAND: Size + size: { + tags: { + font: { + size: null + } + }, + styles: { + 'font-size': null + }, + format: function (element, content) { + var fontSize = element.attr('size'), + size = 2; + + if (!fontSize) { + fontSize = element.css('fontSize'); + } + + // Most browsers return px value but IE returns 1-7 + if (fontSize.indexOf('px') > -1) { + // convert size to an int + fontSize = fontSize.replace('px', '') - 0; + + if (fontSize < 12) { + size = 1; + } + if (fontSize > 15) { + size = 3; + } + if (fontSize > 17) { + size = 4; + } + if (fontSize > 23) { + size = 5; + } + if (fontSize > 31) { + size = 6; + } + if (fontSize > 47) { + size = 7; + } + } else { + size = fontSize; + } + + return '[size=' + size + ']' + content + '[/size]'; + }, + html: '{!0}' + }, + // END_COMMAND + + // START_COMMAND: Color + color: { + tags: { + font: { + color: null + } + }, + styles: { + color: null + }, + quoteType: BBCodeParser.QuoteType.never, + format: function ($element, content) { + var color; + + if (!$element.is('font') || !(color = $element.attr('color'))) { + color = $element[0].style.color || $element.css('color'); + } + + return '[color=' + _normaliseColour(color) + ']' + + content + '[/color]'; + }, + html: function (token, attrs, content) { + return '' + content + ''; + } + }, + // END_COMMAND + + // START_COMMAND: Lists + ul: { + tags: { + ul: null + }, + breakStart: true, + isInline: false, + skipLastLineBreak: true, + format: '[ul]{0}[/ul]', + html: '
      {0}
    ' + }, + list: { + breakStart: true, + isInline: false, + skipLastLineBreak: true, + html: '
      {0}
    ' + }, + ol: { + tags: { + ol: null + }, + breakStart: true, + isInline: false, + skipLastLineBreak: true, + format: '[ol]{0}[/ol]', + html: '
      {0}
    ' + }, + li: { + tags: { + li: null + }, + isInline: false, + closedBy: ['/ul', '/ol', '/list', '*', 'li'], + format: '[li]{0}[/li]', + html: '
  • {0}
  • ' + }, + '*': { + isInline: false, + closedBy: ['/ul', '/ol', '/list', '*', 'li'], + html: '
  • {0}
  • ' + }, + // END_COMMAND + + // START_COMMAND: Table + table: { + tags: { + table: null + }, + isInline: false, + isHtmlInline: true, + skipLastLineBreak: true, + format: '[table]{0}[/table]', + html: '{0}
    ' + }, + tr: { + tags: { + tr: null + }, + isInline: false, + skipLastLineBreak: true, + format: '[tr]{0}[/tr]', + html: '{0}' + }, + th: { + tags: { + th: null + }, + allowsEmpty: true, + isInline: false, + format: '[th]{0}[/th]', + html: '{0}' + }, + td: { + tags: { + td: null + }, + allowsEmpty: true, + isInline: false, + format: '[td]{0}[/td]', + html: '{0}' + }, + // END_COMMAND + + // START_COMMAND: Emoticons + emoticon: { + allowsEmpty: true, + tags: { + img: { + src: null, + 'data-sceditor-emoticon': null + } + }, + format: function ($elm, content) { + return $elm.data('sceditor-emoticon') + content; + }, + html: '{0}' + }, + // END_COMMAND + + // START_COMMAND: Horizontal Rule + hr: { + tags: { + hr: null + }, + allowsEmpty: true, + isSelfClosing: true, + isInline: false, + format: '[hr]{0}', + html: '
    ' + }, + // END_COMMAND + + // START_COMMAND: Image + img: { + allowsEmpty: true, + tags: { + img: { + src: null + } + }, + allowedChildren: ['#'], + quoteType: BBCodeParser.QuoteType.never, + format: function ($element, content) { + var width, height, + attribs = '', + element = $element[0], + style = function (name) { + return element.style ? element.style[name] : null; + }; + + // check if this is an emoticon image + if ($element.attr('data-sceditor-emoticon')) { + return content; + } + + width = $element.attr('width') || style('width'); + height = $element.attr('height') || style('height'); + + // only add width and height if one is specified + if ((element.complete && (width || height)) || + (width && height)) { + attribs = '=' + $element.width() + 'x' + $element.height(); + } + + return '[img' + attribs + ']' + $element.attr('src') + '[/img]'; + }, + html: function (token, attrs, content) { + var undef, width, height, match, + attribs = ''; + + // handle [img width=340 height=240]url[/img] + width = attrs.width; + height = attrs.height; + + // handle [img=340x240]url[/img] + if (attrs.defaultattr) { + match = attrs.defaultattr.split(/x/i); + + width = match[0]; + height = (match.length === 2 ? match[1] : match[0]); + } + + if (width !== undef) { + attribs += ' width="' + escapeEntities(width, true) + '"'; + } + + if (height !== undef) { + attribs += ' height="' + escapeEntities(height, true) + '"'; + } + + return ''; + } + }, + // END_COMMAND + + // START_COMMAND: URL + url: { + allowsEmpty: true, + tags: { + a: { + href: null + } + }, + quoteType: BBCodeParser.QuoteType.never, + format: function (element, content) { + var url = element.attr('href'); + + // make sure this link is not an e-mail, + // if it is return e-mail BBCode + if (url.substr(0, 7) === 'mailto:') { + return '[email="' + url.substr(7) + '"]' + + content + '[/email]'; + } + + return '[url=' + url + ']' + content + '[/url]'; + }, + html: function (token, attrs, content) { + attrs.defaultattr = + escapeEntities(attrs.defaultattr, true) || content; + + return '' + content + ''; + } + }, + // END_COMMAND + + // START_COMMAND: E-mail + email: { + quoteType: BBCodeParser.QuoteType.never, + html: function (token, attrs, content) { + return '' + content + ''; + } + }, + // END_COMMAND + + // START_COMMAND: Quote + quote: { + tags: { + blockquote: null + }, + isInline: false, + quoteType: BBCodeParser.QuoteType.never, + format: function (element, content) { + var author = ''; + var $elm = $(element); + var $cite = $elm.children('cite').first(); + + if ($cite.length === 1 || $elm.data('author')) { + author = $cite.text() || $elm.data('author'); + + $elm.data('author', author); + $cite.remove(); + + content = this.elementToBbcode($(element)); + author = '=' + author.replace(/(^\s+|\s+$)/g, ''); + + $elm.prepend($cite); + } + + return '[quote' + author + ']' + content + '[/quote]'; + }, + html: function (token, attrs, content) { + if (attrs.defaultattr) { + content = '' + escapeEntities(attrs.defaultattr) + + '' + content; + } + + return '
    ' + content + '
    '; + } + }, + // END_COMMAND + + // START_COMMAND: Code + code: { + tags: { + code: null + }, + isInline: false, + allowedChildren: ['#', '#newline'], + format: '[code]{0}[/code]', + html: '{0}' + }, + // END_COMMAND + + + // START_COMMAND: Left + left: { + styles: { + 'text-align': [ + 'left', + '-webkit-left', + '-moz-left', + '-khtml-left' + ] + }, + isInline: false, + format: '[left]{0}[/left]', + html: '
    {0}
    ' + }, + // END_COMMAND + + // START_COMMAND: Centre + center: { + styles: { + 'text-align': [ + 'center', + '-webkit-center', + '-moz-center', + '-khtml-center' + ] + }, + isInline: false, + format: '[center]{0}[/center]', + html: '
    {0}
    ' + }, + // END_COMMAND + + // START_COMMAND: Right + right: { + styles: { + 'text-align': [ + 'right', + '-webkit-right', + '-moz-right', + '-khtml-right' + ] + }, + isInline: false, + format: '[right]{0}[/right]', + html: '
    {0}
    ' + }, + // END_COMMAND + + // START_COMMAND: Justify + justify: { + styles: { + 'text-align': [ + 'justify', + '-webkit-justify', + '-moz-justify', + '-khtml-justify' + ] + }, + isInline: false, + format: '[justify]{0}[/justify]', + html: '
    {0}
    ' + }, + // END_COMMAND + + // START_COMMAND: YouTube + youtube: { + allowsEmpty: true, + tags: { + iframe: { + 'data-youtube-id': null + } + }, + format: function (element, content) { + element = element.attr('data-youtube-id'); + + return element ? '[youtube]' + element + '[/youtube]' : content; + }, + html: '' + }, + // END_COMMAND + + + // START_COMMAND: Rtl + rtl: { + styles: { + direction: ['rtl'] + }, + format: '[rtl]{0}[/rtl]', + html: '
    {0}
    ' + }, + // END_COMMAND + + // START_COMMAND: Ltr + ltr: { + styles: { + direction: ['ltr'] + }, + format: '[ltr]{0}[/ltr]', + html: '
    {0}
    ' + }, + // 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: {} + }; + + /** + * Static BBCode helper class + * @class command + * @name jQuery.plugins.bbcode.bbcode + */ + sceditorPlugins.bbcode.bbcode = + /** @lends jQuery.plugins.bbcode.bbcode */ + { + /** + * Gets a BBCode + * + * @param {String} name + * @return {Object|null} + * @since v1.3.5 + */ + get: function (name) { + return bbcodes[name] || null; + }, + + /** + *

    Adds a BBCode to the parser or updates an existing + * BBCode if a BBCode with the specified name already exists.

    + * + * @param {String} name + * @param {Object} bbcode + * @return {this|false} Returns false if name or bbcode is false + * @since v1.3.5 + */ + set: function (name, bbcode) { + if (!name || !bbcode) { + return false; + } + + // merge any existing command properties + bbcode = $.extend(bbcodes[name] || {}, bbcode); + + bbcode.remove = function () { + delete bbcodes[name]; + }; + + bbcodes[name] = bbcode; + + return this; + }, + + /** + * Renames a BBCode + * + * This does not change the format or HTML handling, those must be + * changed manually. + * + * @param {String} name [description] + * @param {String} newName [description] + * @return {this|false} + * @since v1.4.0 + */ + rename: function (name, newName) { + if (name in bbcodes) { + bbcodes[newName] = bbcodes[name]; + + delete bbcodes[name]; + } else { + return false; + } + + return this; + }, + + /** + * Removes a BBCode + * + * @param {String} name + * @return {this} + * @since v1.3.5 + */ + remove: function (name) { + if (name in bbcodes) { + delete bbcodes[name]; + } + + return this; + } + }; + + /** + * Deprecated, use plugins: option instead. I.e.: + * + * $('textarea').sceditor({ + * plugins: 'bbcode' + * }); + * + * @deprecated + */ + $.fn.sceditorBBCodePlugin = function (options) { + options = options || {}; + + if ($.isPlainObject(options)) { + options.plugins = (options.plugins || '') + 'bbcode'; + } + + return this.sceditor(options); + }; + + /** + * Converts CSS RGB and hex shorthand into hex + * + * @since v1.4.0 + * @param {String} colorStr + * @return {String} + * @deprecated + */ + sceditorPlugins.bbcode.normaliseColour = _normaliseColour; + sceditorPlugins.bbcode.formatString = _formatString; + sceditorPlugins.bbcode.stripQuotes = _stripQuotes; + sceditorPlugins.bbcode.bbcodes = bbcodes; + SCEditor.BBCodeParser = BBCodeParser; +})(jQuery, window, document); diff --git a/js/sceditor/development/jquery.sceditor.default.css b/js/sceditor/development/jquery.sceditor.default.css new file mode 100644 index 00000000..9c542d38 --- /dev/null +++ b/js/sceditor/development/jquery.sceditor.default.css @@ -0,0 +1,83 @@ +/*! SCEditor | (C) 2011-2013, Sam Clarke | sceditor.com/license */ +html, body, p, code:before, table { + margin: 0; + padding: 0; + font-family: Verdana, Arial, Helvetica, sans-serif; + font-size: 13px; + color: #111; +} +html { + height: 100%; + + /* Needed for iOS scrolling bug fix */ + overflow: auto; + -webkit-overflow-scrolling: touch; +} +body { + /* Needed for iOS scrolling bug fix */ + position: relative; + overflow: auto; + + /* Needed to make sure body covers the whole editor and that + long lines don't cause horizontal scrolling */ + min-height: 100%; + word-wrap: break-word; +} + +ul, ol { + margin-top: 0; + margin-bottom: 0; + padding-top: 0; + padding-bottom: 0; +} + +table, td { + border: 1px dotted #000; + empty-cells: show; +} + +code:before { + position: absolute; + content: 'Code:'; + top: -1.35em; + left: 0; +} +code { + margin-top: 1.5em; + position: relative; + background: #eee; + border: 1px solid #aaa; + white-space: pre; + padding: .25em; + display: block; +} +.ie6 code, .ie7 code { + margin-top: 0; +} +code:before, code { + display: block; + text-align: left; +} + +blockquote { + position: relative; + background: #fff6c7; + margin: .25em 0; + border: 1px solid #aaa; + padding: .25em; +} +blockquote cite { + font-weight: bold; + display: block; + font-size: 1em; + border-bottom: 1px solid #aaa; +} + +h1, h2, h3, h4, h5, h6 { + padding: 0; margin: 0; +} + +/* Prevent empty paragraphs from collapsing */ +div, p { + min-height: 1.25em; +} 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 = $('
    ') + .insertAfter($original) + .css('z-index', options.zIndex); + + // Add IE version to the container to allow IE specific CSS + // fixes without using CSS hacks or conditional comments + if (IE_VER) { + $editorContainer.addClass('ie ie' + IE_VER); + } + + isRequired = !!$original.attr('required'); + $original.removeAttr('required'); + + // create the editor + initPlugins(); + initEmoticons(); + initToolBar(); + initEditor(!!options.startInSourceMode); + initCommands(); + initOptions(); + initEvents(); + + // force into source mode if is a browser that can't handle + // full editing + if (!browser.isWysiwygSupported) { + base.toggleSourceMode(); + } + + updateActiveButtons(); + + var loaded = function () { + $globalWin.off('load', loaded); + + if (options.autofocus) { + autofocus(); + } + + if (options.autoExpand) { + base.expandToContent(); + } + + // Page width might have changed after CSS is loaded so + // call handleWindowResize to update any % based dimensions + handleWindowResize(); + + pluginManager.call('ready'); + }; + $globalWin.on('load', loaded); + if (globalDoc.readyState && globalDoc.readyState === 'complete') { + loaded(); + } + }; + + initPlugins = function () { + var plugins = options.plugins; + + plugins = plugins ? plugins.toString().split(',') : []; + pluginManager = new PluginManager(base); + + $.each(plugins, function (idx, plugin) { + pluginManager.register($.trim(plugin)); + }); + }; + + /** + * Init the locale variable with the specified locale if possible + * @private + * @return void + */ + initLocale = function () { + var lang; + + locale = SCEditor.locale[options.locale]; + + if (!locale) { + lang = options.locale.split('-'); + locale = SCEditor.locale[lang[0]]; + } + + // Locale DateTime format overrides any specified in the options + if (locale && locale.dateFormat) { + options.dateFormat = locale.dateFormat; + } + }; + + /** + * Creates the editor iframe and textarea + * @param {boolean} startInSourceMode Force loading the editor in this + * mode + * @private + */ + initEditor = function (startInSourceMode) { + var doc, tabIndex; + + $sourceEditor = $(''); + $wysiwygEditor = $( + '' + ); + + /* This needs to be done right after they are created because, + * for any reason, the user may not want the value to be tinkered + * by any filters. + */ + if (startInSourceMode) { + $editorContainer.addClass('sourceMode'); + $wysiwygEditor.hide(); + } else { + $editorContainer.addClass('wysiwygMode'); + $sourceEditor.hide(); + } + + if (!options.spellcheck) { + $sourceEditor.attr('spellcheck', 'false'); + } + + /*jshint scripturl: true*/ + if (globalWin.location.protocol === 'https:') { + $wysiwygEditor.attr('src', 'javascript:false'); + } + + // Add the editor to the container + $editorContainer.append($wysiwygEditor).append($sourceEditor); + wysiwygEditor = $wysiwygEditor[0]; + sourceEditor = $sourceEditor[0]; + + base.dimensions( + options.width || $original.width(), + options.height || $original.height() + ); + + doc = getWysiwygDoc(); + doc.open(); + doc.write(_tmpl('html', { + // Add IE version class to the HTML element so can apply + // conditional styling without CSS hacks + attrs: IE_VER ? ' class="ie ie' + IE_VER + '"' : '', + spellcheck: options.spellcheck ? '' : 'spellcheck="false"', + charset: options.charset, + style: options.style + })); + doc.close(); + + $wysiwygDoc = $(doc); + $wysiwygBody = $(doc.body); + + base.readOnly(!!options.readOnly); + + // iframe overflow fix for iOS, also fixes an IE issue with the + // editor not getting focus when clicking inside + if (browser.ios || IE_VER) { + $wysiwygBody.height('100%'); + + if (!IE_VER) { + $wysiwygBody.on('touchend', base.focus); + } + } + + tabIndex = $original.attr('tabindex'); + $sourceEditor.attr('tabindex', tabIndex); + $wysiwygEditor.attr('tabindex', tabIndex); + + rangeHelper = new RangeHelper(wysiwygEditor.contentWindow); + + // load any textarea value into the editor + base.val($original.hide().val()); + }; + + /** + * Initialises options + * @private + */ + initOptions = function () { + // auto-update original textbox on blur if option set to true + if (options.autoUpdate) { + $wysiwygBody.on('blur', autoUpdate); + $sourceEditor.on('blur', autoUpdate); + } + + if (options.rtl === null) { + options.rtl = $sourceEditor.css('direction') === 'rtl'; + } + + base.rtl(!!options.rtl); + + if (options.autoExpand) { + $wysiwygDoc.on('keyup', base.expandToContent); + } + + if (options.resizeEnabled) { + initResize(); + } + + $editorContainer.attr('id', options.id); + base.emoticons(options.emoticonsEnabled); + }; + + /** + * Initialises events + * @private + */ + initEvents = function () { + var CHECK_SELECTION_EVENTS = IE_VER ? + 'selectionchange' : + 'keyup focus blur contextmenu mouseup touchend click'; + + var EVENTS_TO_FORWARD = 'keydown keyup keypress ' + + 'focus blur contextmenu'; + + $globalDoc.click(handleDocumentClick); + + $(original.form) + .on('reset', handleFormReset) + .submit(base.updateOriginal); + + $globalWin.on('resize orientationChanged', handleWindowResize); + + $wysiwygBody + .keypress(handleKeyPress) + .keydown(handleKeyDown) + .keydown(handleBackSpace) + .keyup(appendNewLine) + .blur(valueChangedBlur) + .keyup(valueChangedKeyUp) + .on('paste', handlePasteEvt) + .on(CHECK_SELECTION_EVENTS, checkSelectionChanged) + .on(EVENTS_TO_FORWARD, handleEvent); + + if (options.emoticonsCompat && globalWin.getSelection) { + $wysiwygBody.keyup(emoticonsCheckWhitespace); + } + + $sourceEditor + .blur(valueChangedBlur) + .keyup(valueChangedKeyUp) + .keydown(handleKeyDown) + .on(EVENTS_TO_FORWARD, handleEvent); + + $wysiwygDoc + .mousedown(handleMouseDown) + .blur(valueChangedBlur) + .on(CHECK_SELECTION_EVENTS, checkSelectionChanged) + .on('beforedeactivate keyup mouseup', saveRange) + .keyup(appendNewLine) + .focus(function () { + lastRange = null; + }); + + $editorContainer + .on('selectionchanged', checkNodeChanged) + .on('selectionchanged', updateActiveButtons) + .on('selectionchanged valuechanged nodechanged', handleEvent); + }; + + /** + * Creates the toolbar and appends it to the container + * @private + */ + initToolBar = function () { + var $group, + commands = base.commands, + exclude = (options.toolbarExclude || '').split(','), + groups = options.toolbar.split('|'); + + $toolbar = $('
    '); + + $.each(groups, function (idx, group) { + $group = $('
    '); + + $.each(group.split(','), function (idx, commandName) { + var $button, shortcut, + command = commands[commandName]; + + // The commandName must be a valid command and not excluded + if (!command || $.inArray(commandName, exclude) > -1) { + return; + } + + shortcut = command.shortcut; + $button = _tmpl('toolbarButton', { + name: commandName, + dispName: base._(command.name || + command.tooltip || commandName) + }, true); + + $button + .data('sceditor-txtmode', !!command.txtExec) + .data('sceditor-wysiwygmode', !!command.exec) + .toggleClass('disabled', !command.exec) + .mousedown(function () { + // IE < 8 supports unselectable attribute + // so don't need this + if (!IE_VER || IE_VER < 9) { + autoUpdateCanceled = true; + } + }) + .click(function () { + var $this = $(this); + + if (!$this.hasClass('disabled')) { + handleCommand($this, command); + } + + updateActiveButtons(); + return false; + }); + + if (command.tooltip) { + $button.attr( + 'title', + base._(command.tooltip) + + (shortcut ? ' (' + shortcut + ')' : '') + ); + } + + if (shortcut) { + base.addShortcut(shortcut, commandName); + } + + if (command.state) { + btnStateHandlers.push({ + name: commandName, + state: command.state + }); + // exec string commands can be passed to queryCommandState + } else if (typeof command.exec === 'string') { + btnStateHandlers.push({ + name: commandName, + state: command.exec + }); + } + + $group.append($button); + toolbarButtons[commandName] = $button; + }); + + // Exclude empty groups + if ($group[0].firstChild) { + $toolbar.append($group); + } + }); + + // Append the toolbar to the toolbarContainer option if given + $(options.toolbarContainer || $editorContainer).append($toolbar); + }; + + /** + * Creates an array of all the key press functions + * like emoticons, ect. + * @private + */ + initCommands = function () { + $.each(base.commands, function (name, cmd) { + if (cmd.forceNewLineAfter && $.isArray(cmd.forceNewLineAfter)) { + requireNewLineFix = $.merge( + requireNewLineFix, + cmd.forceNewLineAfter + ); + } + }); + + appendNewLine(); + }; + + /** + * Creates the resizer. + * @private + */ + initResize = function () { + var minHeight, maxHeight, minWidth, maxWidth, + mouseMoveFunc, mouseUpFunc, + $grip = $('
    '), + // Cover is used to cover the editor iframe so document + // still gets mouse move events + $cover = $('
    '), + moveEvents = 'touchmove mousemove', + endEvents = 'touchcancel touchend mouseup', + startX = 0, + startY = 0, + newX = 0, + newY = 0, + startWidth = 0, + startHeight = 0, + origWidth = $editorContainer.width(), + origHeight = $editorContainer.height(), + isDragging = false, + rtl = base.rtl(); + + minHeight = options.resizeMinHeight || origHeight / 1.5; + maxHeight = options.resizeMaxHeight || origHeight * 2.5; + minWidth = options.resizeMinWidth || origWidth / 1.25; + maxWidth = options.resizeMaxWidth || origWidth * 1.25; + + mouseMoveFunc = function (e) { + // iOS uses window.event + if (e.type === 'touchmove') { + e = globalWin.event; + newX = e.changedTouches[0].pageX; + newY = e.changedTouches[0].pageY; + } else { + newX = e.pageX; + newY = e.pageY; + } + + var newHeight = startHeight + (newY - startY), + newWidth = rtl ? + startWidth - (newX - startX) : + startWidth + (newX - startX); + + if (maxWidth > 0 && newWidth > maxWidth) { + newWidth = maxWidth; + } + if (minWidth > 0 && newWidth < minWidth) { + newWidth = minWidth; + } + if (!options.resizeWidth) { + newWidth = false; + } + + if (maxHeight > 0 && newHeight > maxHeight) { + newHeight = maxHeight; + } + if (minHeight > 0 && newHeight < minHeight) { + newHeight = minHeight; + } + if (!options.resizeHeight) { + newHeight = false; + } + + if (newWidth || newHeight) { + base.dimensions(newWidth, newHeight); + + // The resize cover will not fill the container + // in IE6 unless a height is specified. + if (IE_VER < 7) { + $editorContainer.height(newHeight); + } + } + + e.preventDefault(); + }; + + mouseUpFunc = function (e) { + if (!isDragging) { + return; + } + + isDragging = false; + + $cover.hide(); + $editorContainer.removeClass('resizing').height('auto'); + $globalDoc.off(moveEvents, mouseMoveFunc); + $globalDoc.off(endEvents, mouseUpFunc); + + e.preventDefault(); + }; + + $editorContainer.append($grip); + $editorContainer.append($cover.hide()); + + $grip.on('touchstart mousedown', function (e) { + // iOS uses window.event + if (e.type === 'touchstart') { + e = globalWin.event; + startX = e.touches[0].pageX; + startY = e.touches[0].pageY; + } else { + startX = e.pageX; + startY = e.pageY; + } + + startWidth = $editorContainer.width(); + startHeight = $editorContainer.height(); + isDragging = true; + + $editorContainer.addClass('resizing'); + $cover.show(); + $globalDoc.on(moveEvents, mouseMoveFunc); + $globalDoc.on(endEvents, mouseUpFunc); + + // The resize cover will not fill the container in + // IE6 unless a height is specified. + if (IE_VER < 7) { + $editorContainer.height(startHeight); + } + + e.preventDefault(); + }); + }; + + /** + * Prefixes and preloads the emoticon images + * @private + */ + initEmoticons = function () { + var emoticon, + emoticons = options.emoticons, + root = options.emoticonsRoot; + + if (!$.isPlainObject(emoticons) || !options.emoticonsEnabled) { + return; + } + + $.each(emoticons, function (idx, val) { + $.each(val, function (key, url) { + // Prefix emoticon root to emoticon urls + if (root) { + url = { + url: root + (url.url || url), + tooltip: url.tooltip || key + }; + + emoticons[idx][key] = url; + } + + // Preload the emoticon + emoticon = globalDoc.createElement('img'); + emoticon.src = url.url || url; + preLoadCache.push(emoticon); + }); + }); + }; + + /** + * Autofocus the editor + * @private + */ + autofocus = function () { + var range, txtPos, + doc = $wysiwygDoc[0], + body = $wysiwygBody[0], + node = body.firstChild, + focusEnd = !!options.autofocusEnd; + + // Can't focus invisible elements + if (!$editorContainer.is(':visible')) { + return; + } + + if (base.sourceMode()) { + txtPos = focusEnd ? sourceEditor.value.length : 0; + + if (sourceEditor.setSelectionRange) { + sourceEditor.setSelectionRange(txtPos, txtPos); + } else { + range = sourceEditor.createTextRange(); + range.moveEnd('character', txtPos); + range.collapse(false); + range.select(); + } + + return; + } + + dom.removeWhiteSpace(body); + + if (focusEnd) { + if (!(node = body.lastChild)) { + node = doc.createElement('p'); + $wysiwygBody.append(node); + } + + while (node.lastChild) { + node = node.lastChild; + + // IE < 11 should place the cursor after the
    as + // it will show it as a newline. IE >= 11 and all + // other browsers should place the cursor before. + if (!IE_BR_FIX && $(node).is('br') && + node.previousSibling) { + node = node.previousSibling; + } + } + } + + if (doc.createRange) { + range = doc.createRange(); + + if (!dom.canHaveChildren(node)) { + range.setStartBefore(node); + + if (focusEnd) { + range.setStartAfter(node); + } + } else { + range.selectNodeContents(node); + } + } else { + range = body.createTextRange(); + range.moveToElementText(node.nodeType !== 3 ? + node : node.parentNode); + } + + range.collapse(!focusEnd); + rangeHelper.selectRange(range); + currentSelection = range; + + if (focusEnd) { + $wysiwygDoc.scrollTop(body.scrollHeight); + $wysiwygBody.scrollTop(body.scrollHeight); + } + + base.focus(); + }; + + /** + * Gets if the editor is read only + * + * @since 1.3.5 + * @function + * @memberOf jQuery.sceditor.prototype + * @name readOnly + * @return {Boolean} + */ + /** + * Sets if the editor is read only + * + * @param {boolean} readOnly + * @since 1.3.5 + * @function + * @memberOf jQuery.sceditor.prototype + * @name readOnly^2 + * @return {this} + */ + base.readOnly = function (readOnly) { + if (typeof readOnly !== 'boolean') { + return $sourceEditor.attr('readonly') === 'readonly'; + } + + $wysiwygBody[0].contentEditable = !readOnly; + + if (!readOnly) { + $sourceEditor.removeAttr('readonly'); + } else { + $sourceEditor.attr('readonly', 'readonly'); + } + + updateToolBar(readOnly); + + return base; + }; + + /** + * Gets if the editor is in RTL mode + * + * @since 1.4.1 + * @function + * @memberOf jQuery.sceditor.prototype + * @name rtl + * @return {Boolean} + */ + /** + * Sets if the editor is in RTL mode + * + * @param {boolean} rtl + * @since 1.4.1 + * @function + * @memberOf jQuery.sceditor.prototype + * @name rtl^2 + * @return {this} + */ + base.rtl = function (rtl) { + var dir = rtl ? 'rtl' : 'ltr'; + + if (typeof rtl !== 'boolean') { + return $sourceEditor.attr('dir') === 'rtl'; + } + + $wysiwygBody.attr('dir', dir); + $sourceEditor.attr('dir', dir); + + $editorContainer + .removeClass('rtl') + .removeClass('ltr') + .addClass(dir); + + return base; + }; + + /** + * Updates the toolbar to disable/enable the appropriate buttons + * @private + */ + updateToolBar = function (disable) { + var mode = base.inSourceMode() ? 'txtmode' : 'wysiwygmode'; + + $.each(toolbarButtons, function (idx, $button) { + if (disable === true || !$button.data('sceditor-' + mode)) { + $button.addClass('disabled'); + } else { + $button.removeClass('disabled'); + } + }); + }; + + /** + * Gets the width of the editor in pixels + * + * @since 1.3.5 + * @function + * @memberOf jQuery.sceditor.prototype + * @name width + * @return {int} + */ + /** + * Sets the width of the editor + * + * @param {int} width Width in pixels + * @since 1.3.5 + * @function + * @memberOf jQuery.sceditor.prototype + * @name width^2 + * @return {this} + */ + /** + * Sets the width of the editor + * + * The saveWidth specifies if to save the width. The stored width can be + * used for things like restoring from maximized state. + * + * @param {int} width Width in pixels + * @param {boolean} [saveWidth=true] If to store the width + * @since 1.4.1 + * @function + * @memberOf jQuery.sceditor.prototype + * @name width^3 + * @return {this} + */ + base.width = function (width, saveWidth) { + if (!width && width !== 0) { + return $editorContainer.width(); + } + + base.dimensions(width, null, saveWidth); + + return base; + }; + + /** + * Returns an object with the properties width and height + * which are the width and height of the editor in px. + * + * @since 1.4.1 + * @function + * @memberOf jQuery.sceditor.prototype + * @name dimensions + * @return {object} + */ + /** + *

    Sets the width and/or height of the editor.

    + * + *

    If width or height is not numeric it is ignored.

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

    Sets the width and/or height of the editor.

    + * + *

    If width or height is not numeric it is ignored.

    + * + *

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

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

    Inserts HTML into WYSIWYG editor.

    + * + *

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

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

    Inserts text into the WYSIWYG or source editor depending on which + * mode the editor is in.

    + * + *

    If endText is specified any selected text will be placed between + * text and endText. If no text is selected text and endText will + * just be concatenate together.

    + * + * @param {String} text + * @param {String} [endText=null] + * @since 1.3.5 + * @function + * @name insertText + * @memberOf jQuery.sceditor.prototype + */ + base.insertText = function (text, endText) { + if (base.inSourceMode()) { + base.sourceEditorInsertText(text, endText); + } else { + base.wysiwygEditorInsertText(text, endText); + } + + return base; + }; + + /** + *

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

    + * + *

    If endText is specified any selected text will be placed between + * text and endText. If no text is selected text and endText will + * just be concatenate together.

    + * + *

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

    + * + *

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

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

    Gets the value of the editor.

    + * + *

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

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

    Sets the value of the editor.

    + * + *

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

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

    Inserts HTML/BBCode into the editor

    + * + *

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

    + * + *

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

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

    Inserts HTML/BBCode into the editor

    + * + *

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

    + * + *

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

    + * + *

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

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

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

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

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

    + * + *

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

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

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

    + * + *

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

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

    or

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

    Binds a handler to the specified events

    + * + *

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

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

    + * + *

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

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

    Adds a handler to the node changed event.

    + * + *

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

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

    Adds a handler to the selection changed event

    + * + *

    Happens whenever the selection changes in WYSIWYG mode.

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

    Adds a handler to the value changed event

    + * + *

    Happens whenever the current editor value changes.

    + * + *

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

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

    ' + + '', + + toolbarButton: '' + + '
    {dispName}
    ', + + emoticon: '', + + fontOpt: '{font}', + + sizeOpt: '{size}', + + pastetext: + '
    ' + + '
    ' + + '
    ' + + '
    ', + + table: + '
    ' + + '
    ' + + '
    ', + + image: + '
    ' + + '
    ' + + '
    ' + + '
    ' + + '
    ' + + '
    ' + + '
    ' + + '
    ', + + email: + '
    ' + + '
    ' + + '
    ' + + '
    ' + + '
    ' + + '
    ', + + link: + '
    ' + + '
    ' + + '
    ' + + '
    ' + + '
    ', + + youtubeMenu: + '
    ' + + '
    ' + + '
    ' + + '
    ', + + youtube: + '' + }; + + /** + *

    Replaces any params in a template with the passed params.

    + * + *

    If createHtml is passed it will use jQuery to create the HTML. The + * same as doing: $(editor.tmpl("html", {params...}));

    + * + * @param {string} name + * @param {Object} params + * @param {Boolean} createHtml + * @private + */ + return function (name, params, createHtml) { + var template = _templates[name]; + + $.each(params, function (name, val) { + template = template.replace( + new RegExp('\\{' + name + '\\}', 'g'), val + ); + }); + + if (createHtml) { + template = $(template); + } + + return template; + }; + }.call(exports, __webpack_require__, exports, module), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__)); + + +/***/ }, +/* 9 */ +/***/ function(module, exports, __webpack_require__) { + + var __WEBPACK_AMD_DEFINE_RESULT__;!(__WEBPACK_AMD_DEFINE_RESULT__ = function (require) { + 'use strict'; + + var $ = __webpack_require__(1); + var IE_VER = __webpack_require__(6).ie; + var _tmpl = __webpack_require__(8); + + // In IE < 11 a BR at the end of a block level element + // causes a line break. In all other browsers it's collapsed. + var IE_BR_FIX = IE_VER && IE_VER < 11; + + + /** + * Map of all the commands for SCEditor + * @type {Object} + * @name commands + * @memberOf jQuery.sceditor + */ + var defaultCommnds = { + // START_COMMAND: Bold + bold: { + exec: 'bold', + tooltip: 'Bold', + shortcut: 'Ctrl+B' + }, + // END_COMMAND + // START_COMMAND: Italic + italic: { + exec: 'italic', + tooltip: 'Italic', + shortcut: 'Ctrl+I' + }, + // END_COMMAND + // START_COMMAND: Underline + underline: { + exec: 'underline', + tooltip: 'Underline', + shortcut: 'Ctrl+U' + }, + // END_COMMAND + // START_COMMAND: Strikethrough + strike: { + exec: 'strikethrough', + tooltip: 'Strikethrough' + }, + // END_COMMAND + // START_COMMAND: Subscript + subscript: { + exec: 'subscript', + tooltip: 'Subscript' + }, + // END_COMMAND + // START_COMMAND: Superscript + superscript: { + exec: 'superscript', + tooltip: 'Superscript' + }, + // END_COMMAND + + // START_COMMAND: Left + left: { + exec: 'justifyleft', + tooltip: 'Align left' + }, + // END_COMMAND + // START_COMMAND: Centre + center: { + exec: 'justifycenter', + tooltip: 'Center' + }, + // END_COMMAND + // START_COMMAND: Right + right: { + exec: 'justifyright', + tooltip: 'Align right' + }, + // END_COMMAND + // START_COMMAND: Justify + justify: { + exec: 'justifyfull', + tooltip: 'Justify' + }, + // END_COMMAND + + // START_COMMAND: Font + font: { + _dropDown: function (editor, caller, callback) { + var fontIdx = 0, + fonts = editor.opts.fonts.split(','), + content = $('
    '), + /** @private */ + clickFunc = function () { + callback($(this).data('font')); + editor.closeDropDown(true); + return false; + }; + + for (; fontIdx < fonts.length; fontIdx++) { + content.append( + _tmpl('fontOpt', { + font: fonts[fontIdx] + }, true).click(clickFunc) + ); + } + + editor.createDropDown(caller, 'font-picker', content); + }, + exec: function (caller) { + var editor = this; + + defaultCommnds.font._dropDown( + editor, + caller, + function (fontName) { + editor.execCommand('fontname', fontName); + } + ); + }, + tooltip: 'Font Name' + }, + // END_COMMAND + // START_COMMAND: Size + size: { + _dropDown: function (editor, caller, callback) { + var content = $('
    '), + /** @private */ + clickFunc = function (e) { + callback($(this).data('size')); + editor.closeDropDown(true); + e.preventDefault(); + }; + + for (var i = 1; i <= 7; i++) { + content.append(_tmpl('sizeOpt', { + size: i + }, true).click(clickFunc)); + } + + editor.createDropDown(caller, 'fontsize-picker', content); + }, + exec: function (caller) { + var editor = this; + + defaultCommnds.size._dropDown( + editor, + caller, + function (fontSize) { + editor.execCommand('fontsize', fontSize); + } + ); + }, + tooltip: 'Font Size' + }, + // END_COMMAND + // START_COMMAND: Colour + color: { + _dropDown: function (editor, caller, callback) { + var i, x, color, colors, + genColor = {r: 255, g: 255, b: 255}, + content = $('
    '), + colorColumns = editor.opts.colors ? + editor.opts.colors.split('|') : new Array(21), + // IE is slow at string concatenation so use an array + html = [], + cmd = defaultCommnds.color; + + if (!cmd._htmlCache) { + for (i = 0; i < colorColumns.length; ++i) { + colors = colorColumns[i] ? + colorColumns[i].split(',') : new Array(21); + + html.push('
    '); + + for (x = 0; x < colors.length; ++x) { + // use pre defined colour if can otherwise use the + // generated color + color = colors[x] || '#' + + genColor.r.toString(16) + + genColor.g.toString(16) + + genColor.b.toString(16); + + html.push( + '' + ); + + if (x % 5 === 0) { + genColor.g -= 51; + genColor.b = 255; + } else { + genColor.b -= 51; + } + } + + html.push('
    '); + + if (i % 5 === 0) { + genColor.r -= 51; + genColor.g = 255; + genColor.b = 255; + } else { + genColor.g = 255; + genColor.b = 255; + } + } + + cmd._htmlCache = html.join(''); + } + + content.append(cmd._htmlCache) + .find('a') + .click(function (e) { + callback($(this).attr('data-color')); + editor.closeDropDown(true); + e.preventDefault(); + }); + + editor.createDropDown(caller, 'color-picker', content); + }, + exec: function (caller) { + var editor = this; + + defaultCommnds.color._dropDown( + editor, + caller, + function (color) { + editor.execCommand('forecolor', color); + } + ); + }, + tooltip: 'Font Color' + }, + // END_COMMAND + // START_COMMAND: Remove Format + removeformat: { + exec: 'removeformat', + tooltip: 'Remove Formatting' + }, + // END_COMMAND + + // START_COMMAND: Cut + cut: { + exec: 'cut', + tooltip: 'Cut', + errorMessage: 'Your browser does not allow the cut command. ' + + 'Please use the keyboard shortcut Ctrl/Cmd-X' + }, + // END_COMMAND + // START_COMMAND: Copy + copy: { + exec: 'copy', + tooltip: 'Copy', + errorMessage: 'Your browser does not allow the copy command. ' + + 'Please use the keyboard shortcut Ctrl/Cmd-C' + }, + // END_COMMAND + // START_COMMAND: Paste + paste: { + exec: 'paste', + tooltip: 'Paste', + errorMessage: 'Your browser does not allow the paste command. ' + + 'Please use the keyboard shortcut Ctrl/Cmd-V' + }, + // END_COMMAND + // START_COMMAND: Paste Text + pastetext: { + exec: function (caller) { + var val, content, + editor = this; + + content = _tmpl('pastetext', { + label: editor._( + 'Paste your text inside the following box:' + ), + insert: editor._('Insert') + }, true); + + content.find('.button').click(function (e) { + val = content.find('#txt').val(); + + if (val) { + editor.wysiwygEditorInsertText(val); + } + + editor.closeDropDown(true); + e.preventDefault(); + }); + + editor.createDropDown(caller, 'pastetext', content); + }, + tooltip: 'Paste Text' + }, + // END_COMMAND + // START_COMMAND: Bullet List + bulletlist: { + exec: 'insertunorderedlist', + tooltip: 'Bullet list' + }, + // END_COMMAND + // START_COMMAND: Ordered List + orderedlist: { + exec: 'insertorderedlist', + tooltip: 'Numbered list' + }, + // END_COMMAND + // START_COMMAND: Indent + indent: { + state: function (parents, firstBlock) { + // Only works with lists, for now + // This is a nested list, so it will always work + var range, startParent, endParent, + $firstBlock = $(firstBlock), + parentLists = $firstBlock.parents('ul,ol,menu'), + parentList = parentLists.first(); + + // in case it's a list with only a single
  • + if (parentLists.length > 1 || + parentList.children().length > 1) { + return 0; + } + + if ($firstBlock.is('ul,ol,menu')) { + // if the whole list is selected, then this must be + // invalidated because the browser will place a + //
    there + range = this.getRangeHelper().selectedRange(); + + if (window.Range && range instanceof Range) { + startParent = range.startContainer.parentNode; + endParent = range.endContainer.parentNode; + + // TODO: could use nodeType for this? + // Maybe just check the firstBlock contains both the start and end containers + // Select the tag, not the textNode + // (that's why the parentNode) + if (startParent !== + startParent.parentNode.firstElementChild || + // work around a bug in FF + ($(endParent).is('li') && endParent !== + endParent.parentNode.lastElementChild)) { + return 0; + } + // it's IE... As it is impossible to know well when to + // accept, better safe than sorry + } else { + return $firstBlock.is('li,ul,ol,menu') ? 0 : -1; + } + } + + return -1; + }, + exec: function () { + var editor = this, + $elm = $(editor.getRangeHelper().getFirstBlockParent()); + + editor.focus(); + + // An indent system is quite complicated as there are loads + // of complications and issues around how to indent text + // As default, let's just stay with indenting the lists, + // at least, for now. + if ($elm.parents('ul,ol,menu')) { + editor.execCommand('indent'); + } + }, + tooltip: 'Add indent' + }, + // END_COMMAND + // START_COMMAND: Outdent + outdent: { + state: function (parents, firstBlock) { + return $(firstBlock).is('ul,ol,menu') || + $(firstBlock).parents('ul,ol,menu').length > 0 ? 0 : -1; + }, + exec: function () { + var editor = this, + $elm = $(editor.getRangeHelper().getFirstBlockParent()); + + if ($elm.parents('ul,ol,menu')) { + editor.execCommand('outdent'); + } + }, + tooltip: 'Remove one indent' + }, + // END_COMMAND + + // START_COMMAND: Table + table: { + forceNewLineAfter: ['table'], + exec: function (caller) { + var editor = this, + content = _tmpl('table', { + rows: editor._('Rows:'), + cols: editor._('Cols:'), + insert: editor._('Insert') + }, true); + + content.find('.button').click(function (e) { + var row, col, + rows = content.find('#rows').val() - 0, + cols = content.find('#cols').val() - 0, + html = ''; + + if (rows < 1 || cols < 1) { + return; + } + + for (row = 0; row < rows; row++) { + html += ''; + + for (col = 0; col < cols; col++) { + html += ''; + } + + html += ''; + } + + html += '
    ' + + (IE_BR_FIX ? '' : '
    ') + + '
    '; + + editor.wysiwygEditorInsertHtml(html); + editor.closeDropDown(true); + e.preventDefault(); + }); + + editor.createDropDown(caller, 'inserttable', content); + }, + tooltip: 'Insert a table' + }, + // END_COMMAND + + // START_COMMAND: Horizontal Rule + horizontalrule: { + exec: 'inserthorizontalrule', + tooltip: 'Insert a horizontal rule' + }, + // END_COMMAND + + // START_COMMAND: Code + code: { + forceNewLineAfter: ['code'], + exec: function () { + this.wysiwygEditorInsertHtml( + '', + (IE_BR_FIX ? '' : '
    ') + '
    ' + ); + }, + tooltip: 'Code' + }, + // END_COMMAND + + // START_COMMAND: Image + image: { + exec: function (caller) { + var editor = this, + content = _tmpl('image', { + url: editor._('URL:'), + width: editor._('Width (optional):'), + height: editor._('Height (optional):'), + insert: editor._('Insert') + }, true); + + content.find('.button').click(function (e) { + var val = content.find('#image').val(), + width = content.find('#width').val(), + height = content.find('#height').val(), + attrs = ''; + + if (width) { + attrs += ' width="' + width + '"'; + } + + if (height) { + attrs += ' height="' + height + '"'; + } + + if (val) { + editor.wysiwygEditorInsertHtml( + '' + ); + } + + editor.closeDropDown(true); + e.preventDefault(); + }); + + editor.createDropDown(caller, 'insertimage', content); + }, + tooltip: 'Insert an image' + }, + // END_COMMAND + + // START_COMMAND: E-mail + email: { + exec: function (caller) { + var editor = this, + content = _tmpl('email', { + label: editor._('E-mail:'), + desc: editor._('Description (optional):'), + insert: editor._('Insert') + }, true); + + content.find('.button').click(function (e) { + var val = content.find('#email').val(), + description = content.find('#des').val(); + + if (val) { + // needed for IE to reset the last range + editor.focus(); + + if (!editor.getRangeHelper().selectedHtml() || + description) { + description = description || val; + + editor.wysiwygEditorInsertHtml( + '' + + description + + '' + ); + } else { + editor.execCommand('createlink', 'mailto:' + val); + } + } + + editor.closeDropDown(true); + e.preventDefault(); + }); + + editor.createDropDown(caller, 'insertemail', content); + }, + tooltip: 'Insert an email' + }, + // END_COMMAND + + // START_COMMAND: Link + link: { + exec: function (caller) { + var editor = this, + content = _tmpl('link', { + url: editor._('URL:'), + desc: editor._('Description (optional):'), + ins: editor._('Insert') + }, true); + + content.find('.button').click(function (e) { + var val = content.find('#link').val(), + description = content.find('#des').val(); + + if (val) { + // needed for IE to restore the last range + editor.focus(); + + // If there is no selected text then must set the URL as + // the text. Most browsers do this automatically, sadly + // IE doesn't. + if (!editor.getRangeHelper().selectedHtml() || + description) { + description = description || val; + + editor.wysiwygEditorInsertHtml( + '' + description + '' + ); + } else { + editor.execCommand('createlink', val); + } + } + + editor.closeDropDown(true); + e.preventDefault(); + }); + + editor.createDropDown(caller, 'insertlink', content); + }, + tooltip: 'Insert a link' + }, + // END_COMMAND + + // START_COMMAND: Unlink + unlink: { + state: function () { + var $current = $(this.currentNode()); + return $current.is('a') || + $current.parents('a').length > 0 ? 0 : -1; + }, + exec: function () { + var $current = $(this.currentNode()), + $anchor = $current.is('a') ? $current : + $current.parents('a').first(); + + if ($anchor.length) { + $anchor.replaceWith($anchor.contents()); + } + }, + tooltip: 'Unlink' + }, + // END_COMMAND + + + // START_COMMAND: Quote + quote: { + forceNewLineAfter: ['blockquote'], + exec: function (caller, html, author) { + var before = '
    ', + end = '
    '; + + // if there is HTML passed set end to null so any selected + // text is replaced + if (html) { + author = (author ? '' + author + '' : ''); + before = before + author + html + end; + end = null; + // if not add a newline to the end of the inserted quote + } else if (this.getRangeHelper().selectedHtml() === '') { + end = (IE_BR_FIX ? '' : '
    ') + end; + } + + this.wysiwygEditorInsertHtml(before, end); + }, + tooltip: 'Insert a Quote' + }, + // END_COMMAND + + // START_COMMAND: Emoticons + emoticon: { + exec: function (caller) { + var editor = this; + + var createContent = function (includeMore) { + var $moreLink, + emoticonsCompat = editor.opts.emoticonsCompat, + rangeHelper = editor.getRangeHelper(), + startSpace = emoticonsCompat && + rangeHelper.getOuterText(true, 1) !== ' ' ? + ' ' : '', + endSpace = emoticonsCompat && + rangeHelper.getOuterText(false, 1) !== ' ' ? + ' ' : '', + $content = $('
    '), + $line = $('
    ').appendTo($content), + perLine = 0, + emoticons = $.extend( + {}, + editor.opts.emoticons.dropdown, + includeMore ? editor.opts.emoticons.more : {} + ); + + $.each(emoticons, function () { + perLine++; + }); + perLine = Math.sqrt(perLine); + + $.each(emoticons, function (code, emoticon) { + $line.append( + $('').attr({ + src: emoticon.url || emoticon, + alt: code, + title: emoticon.tooltip || code + }).click(function () { + editor.insert(startSpace + $(this).attr('alt') + + endSpace, null, false).closeDropDown(true); + + return false; + }) + ); + + if ($line.children().length >= perLine) { + $line = $('
    ').appendTo($content); + } + }); + + if (!includeMore && editor.opts.emoticons.more) { + $moreLink = $( + '' + + editor._('More') + '' + ).click(function () { + editor.createDropDown( + caller, + 'more-emoticons', + createContent(true) + ); + + return false; + }); + + $content.append($moreLink); + } + + return $content; + }; + + editor.createDropDown( + caller, + 'emoticons', + createContent(false) + ); + }, + txtExec: function (caller) { + defaultCommnds.emoticon.exec.call(this, caller); + }, + tooltip: 'Insert an emoticon' + }, + // END_COMMAND + + // START_COMMAND: YouTube + youtube: { + _dropDown: function (editor, caller, handleIdFunc) { + var matches, + content = _tmpl('youtubeMenu', { + label: editor._('Video URL:'), + insert: editor._('Insert') + }, true); + + content.find('.button').click(function (e) { + var val = content + .find('#link') + .val(); + + if (val) { + matches = val.match( + /(?:v=|v\/|embed\/|youtu.be\/)(.{11})/ + ); + + if (matches) { + val = matches[1]; + } + + if (/^[a-zA-Z0-9_\-]{11}$/.test(val)) { + handleIdFunc(val); + } else { + /*global alert:false*/ + alert('Invalid YouTube video'); + } + } + + editor.closeDropDown(true); + e.preventDefault(); + }); + + editor.createDropDown(caller, 'insertlink', content); + }, + exec: function (caller) { + var editor = this; + + defaultCommnds.youtube._dropDown( + editor, + caller, + function (id) { + editor.wysiwygEditorInsertHtml(_tmpl('youtube', { + id: id + })); + } + ); + }, + tooltip: 'Insert a YouTube video' + }, + // END_COMMAND + + // START_COMMAND: Date + date: { + _date: function (editor) { + var now = new Date(), + year = now.getYear(), + month = now.getMonth() + 1, + day = now.getDate(); + + if (year < 2000) { + year = 1900 + year; + } + + if (month < 10) { + month = '0' + month; + } + + if (day < 10) { + day = '0' + day; + } + + return editor.opts.dateFormat + .replace(/year/i, year) + .replace(/month/i, month) + .replace(/day/i, day); + }, + exec: function () { + this.insertText(defaultCommnds.date._date(this)); + }, + txtExec: function () { + this.insertText(defaultCommnds.date._date(this)); + }, + tooltip: 'Insert current date' + }, + // END_COMMAND + + // START_COMMAND: Time + time: { + _time: function () { + var now = new Date(), + hours = now.getHours(), + mins = now.getMinutes(), + secs = now.getSeconds(); + + if (hours < 10) { + hours = '0' + hours; + } + + if (mins < 10) { + mins = '0' + mins; + } + + if (secs < 10) { + secs = '0' + secs; + } + + return hours + ':' + mins + ':' + secs; + }, + exec: function () { + this.insertText(defaultCommnds.time._time()); + }, + txtExec: function () { + this.insertText(defaultCommnds.time._time()); + }, + tooltip: 'Insert current time' + }, + // END_COMMAND + + + // START_COMMAND: Ltr + ltr: { + state: function (parents, firstBlock) { + return firstBlock && firstBlock.style.direction === 'ltr'; + }, + exec: function () { + var editor = this, + elm = editor.getRangeHelper().getFirstBlockParent(), + $elm = $(elm); + + editor.focus(); + + if (!elm || $elm.is('body')) { + editor.execCommand('formatBlock', 'p'); + + elm = editor.getRangeHelper().getFirstBlockParent(); + $elm = $(elm); + + if (!elm || $elm.is('body')) { + return; + } + } + + if ($elm.css('direction') === 'ltr') { + $elm.css('direction', ''); + } else { + $elm.css('direction', 'ltr'); + } + }, + tooltip: 'Left-to-Right' + }, + // END_COMMAND + + // START_COMMAND: Rtl + rtl: { + state: function (parents, firstBlock) { + return firstBlock && firstBlock.style.direction === 'rtl'; + }, + exec: function () { + var editor = this, + elm = editor.getRangeHelper().getFirstBlockParent(), + $elm = $(elm); + + editor.focus(); + + if (!elm || $elm.is('body')) { + editor.execCommand('formatBlock', 'p'); + + elm = editor.getRangeHelper().getFirstBlockParent(); + $elm = $(elm); + + if (!elm || $elm.is('body')) { + return; + } + } + + if ($elm.css('direction') === 'rtl') { + $elm.css('direction', ''); + } else { + $elm.css('direction', 'rtl'); + } + }, + tooltip: 'Right-to-Left' + }, + // END_COMMAND + + + // START_COMMAND: Print + print: { + exec: 'print', + tooltip: 'Print' + }, + // END_COMMAND + + // START_COMMAND: Maximize + maximize: { + state: function () { + return this.maximize(); + }, + exec: function () { + this.maximize(!this.maximize()); + }, + txtExec: function () { + this.maximize(!this.maximize()); + }, + tooltip: 'Maximize', + shortcut: 'Ctrl+Shift+M' + }, + // END_COMMAND + + // START_COMMAND: Source + source: { + state: function () { + return this.sourceMode(); + }, + exec: function () { + this.toggleSourceMode(); + }, + txtExec: function () { + this.toggleSourceMode(); + }, + tooltip: 'View source', + shortcut: 'Ctrl+Shift+S' + }, + // END_COMMAND + + // this is here so that commands above can be removed + // without having to remove the , after the last one. + // Needed for IE. + ignore: {} + }; + + return defaultCommnds; + }.call(exports, __webpack_require__, exports, module), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__)); + + +/***/ }, +/* 10 */ +/***/ function(module, exports, __webpack_require__) { + + var __WEBPACK_AMD_DEFINE_RESULT__;!(__WEBPACK_AMD_DEFINE_RESULT__ = function (require) { + 'use strict'; + + var $ = __webpack_require__(1); + + + /** + * Default options for SCEditor + * @type {Object} + */ + return { + /** @lends jQuery.sceditor.defaultOptions */ + /** + * Toolbar buttons order and groups. Should be comma separated and + * have a bar | to separate groups + * + * @type {String} + */ + toolbar: 'bold,italic,underline,strike,subscript,superscript|' + + 'left,center,right,justify|font,size,color,removeformat|' + + 'cut,copy,paste,pastetext|bulletlist,orderedlist,indent,outdent|' + + 'table|code,quote|horizontalrule,image,email,link,unlink|' + + 'emoticon,youtube,date,time|ltr,rtl|print,maximize,source', + + /** + * Comma separated list of commands to excludes from the toolbar + * + * @type {String} + */ + toolbarExclude: null, + + /** + * Stylesheet to include in the WYSIWYG editor. This is what will style + * the WYSIWYG elements + * + * @type {String} + */ + style: 'jquery.sceditor.default.css', + + /** + * Comma separated list of fonts for the font selector + * + * @type {String} + */ + fonts: 'Arial,Arial Black,Comic Sans MS,Courier New,Georgia,Impact,' + + 'Sans-serif,Serif,Times New Roman,Trebuchet MS,Verdana', + + /** + * Colors should be comma separated and have a bar | to signal a new + * column. + * + * If null the colors will be auto generated. + * + * @type {string} + */ + colors: null, + + /** + * The locale to use. + * @type {String} + */ + locale: $('html').attr('lang') || 'en', + + /** + * The Charset to use + * @type {String} + */ + charset: 'utf-8', + + /** + * Compatibility mode for emoticons. + * + * Helps if you have emoticons such as :/ which would put an emoticon + * inside http:// + * + * This mode requires emoticons to be surrounded by whitespace or end of + * line chars. This mode has limited As You Type emoticon conversion + * support. It will not replace AYT for end of line chars, only + * emoticons surrounded by whitespace. They will still be replaced + * correctly when loaded just not AYT. + * + * @type {Boolean} + */ + emoticonsCompat: false, + + /** + * If to enable emoticons. Can be changes at runtime using the + * emoticons() method. + * + * @type {Boolean} + * @since 1.4.2 + */ + emoticonsEnabled: true, + + /** + * Emoticon root URL + * + * @type {String} + */ + emoticonsRoot: '', + emoticons: { + dropdown: { + ':)': 'emoticons/smile.png', + ':angel:': 'emoticons/angel.png', + ':angry:': 'emoticons/angry.png', + '8-)': 'emoticons/cool.png', + ':\'(': 'emoticons/cwy.png', + ':ermm:': 'emoticons/ermm.png', + ':D': 'emoticons/grin.png', + '<3': 'emoticons/heart.png', + ':(': 'emoticons/sad.png', + ':O': 'emoticons/shocked.png', + ':P': 'emoticons/tongue.png', + ';)': 'emoticons/wink.png' + }, + more: { + ':alien:': 'emoticons/alien.png', + ':blink:': 'emoticons/blink.png', + ':blush:': 'emoticons/blush.png', + ':cheerful:': 'emoticons/cheerful.png', + ':devil:': 'emoticons/devil.png', + ':dizzy:': 'emoticons/dizzy.png', + ':getlost:': 'emoticons/getlost.png', + ':happy:': 'emoticons/happy.png', + ':kissing:': 'emoticons/kissing.png', + ':ninja:': 'emoticons/ninja.png', + ':pinch:': 'emoticons/pinch.png', + ':pouty:': 'emoticons/pouty.png', + ':sick:': 'emoticons/sick.png', + ':sideways:': 'emoticons/sideways.png', + ':silly:': 'emoticons/silly.png', + ':sleeping:': 'emoticons/sleeping.png', + ':unsure:': 'emoticons/unsure.png', + ':woot:': 'emoticons/w00t.png', + ':wassat:': 'emoticons/wassat.png' + }, + hidden: { + ':whistling:': 'emoticons/whistling.png', + ':love:': 'emoticons/wub.png' + } + }, + + /** + * Width of the editor. Set to null for automatic with + * + * @type {int} + */ + width: null, + + /** + * Height of the editor including toolbar. Set to null for automatic + * height + * + * @type {int} + */ + height: null, + + /** + * If to allow the editor to be resized + * + * @type {Boolean} + */ + resizeEnabled: true, + + /** + * Min resize to width, set to null for half textarea width or -1 for + * unlimited + * + * @type {int} + */ + resizeMinWidth: null, + /** + * Min resize to height, set to null for half textarea height or -1 for + * unlimited + * + * @type {int} + */ + resizeMinHeight: null, + /** + * Max resize to height, set to null for double textarea height or -1 + * for unlimited + * + * @type {int} + */ + resizeMaxHeight: null, + /** + * Max resize to width, set to null for double textarea width or -1 for + * unlimited + * + * @type {int} + */ + resizeMaxWidth: null, + /** + * If resizing by height is enabled + * + * @type {Boolean} + */ + resizeHeight: true, + /** + * If resizing by width is enabled + * + * @type {Boolean} + */ + resizeWidth: true, + + /** + * Date format, will be overridden if locale specifies one. + * + * The words year, month and day will be replaced with the users current + * year, month and day. + * + * @type {String} + */ + dateFormat: 'year-month-day', + + /** + * Element to inset the toolbar into. + * + * @type {HTMLElement} + */ + toolbarContainer: null, + + /** + * If to enable paste filtering. This is currently experimental, please + * report any issues. + * + * @type {Boolean} + */ + enablePasteFiltering: false, + + /** + * If to completely disable pasting into the editor + * + * @type {Boolean} + */ + disablePasting: false, + + /** + * If the editor is read only. + * + * @type {Boolean} + */ + readOnly: false, + + /** + * If to set the editor to right-to-left mode. + * + * If set to null the direction will be automatically detected. + * + * @type {Boolean} + */ + rtl: false, + + /** + * If to auto focus the editor on page load + * + * @type {Boolean} + */ + autofocus: false, + + /** + * If to auto focus the editor to the end of the content + * + * @type {Boolean} + */ + autofocusEnd: true, + + /** + * If to auto expand the editor to fix the content + * + * @type {Boolean} + */ + autoExpand: false, + + /** + * If to auto update original textbox on blur + * + * @type {Boolean} + */ + autoUpdate: false, + + /** + * If to enable the browsers built in spell checker + * + * @type {Boolean} + */ + spellcheck: true, + + /** + * If to run the source editor when there is no WYSIWYG support. Only + * really applies to mobile OS's. + * + * @type {Boolean} + */ + runWithoutWysiwygSupport: false, + + /** + * If to load the editor in source mode and still allow switching + * between WYSIWYG and source mode + * + * @type {Boolean} + */ + startInSourceMode: false, + + /** + * Optional ID to give the editor. + * + * @type {String} + */ + id: null, + + /** + * Comma separated list of plugins + * + * @type {String} + */ + plugins: '', + + /** + * z-index to set the editor container to. Needed for jQuery UI dialog. + * + * @type {Int} + */ + zIndex: null, + + /** + * If to trim the BBCode. Removes any spaces at the start and end of the + * BBCode string. + * + * @type {Boolean} + */ + bbcodeTrim: false, + + /** + * If to disable removing block level elements by pressing backspace at + * the start of them + * + * @type {Boolean} + */ + disableBlockRemove: false, + + /** + * BBCode parser options, only applies if using the editor in BBCode + * mode. + * + * See SCEditor.BBCodeParser.defaults for list of valid options + * + * @type {Object} + */ + parserOptions: { }, + + /** + * CSS that will be added to the to dropdown menu (eg. z-index) + * + * @type {Object} + */ + dropDownCss: { } + }; + }.call(exports, __webpack_require__, exports, module), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__)); + + +/***/ } +/******/ ]); \ No newline at end of file diff --git a/js/sceditor/development/jquery.sceditor.xhtml.js b/js/sceditor/development/jquery.sceditor.xhtml.js new file mode 100644 index 00000000..948e648f --- /dev/null +++ b/js/sceditor/development/jquery.sceditor.xhtml.js @@ -0,0 +1,8548 @@ +/******/ (function(modules) { // webpackBootstrap +/******/ // The module cache +/******/ var installedModules = {}; + +/******/ // The require function +/******/ function __webpack_require__(moduleId) { + +/******/ // Check if module is in cache +/******/ if(installedModules[moduleId]) +/******/ return installedModules[moduleId].exports; + +/******/ // Create a new module (and put it into the cache) +/******/ var module = installedModules[moduleId] = { +/******/ exports: {}, +/******/ id: moduleId, +/******/ loaded: false +/******/ }; + +/******/ // Execute the module function +/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); + +/******/ // Flag the module as loaded +/******/ module.loaded = true; + +/******/ // Return the exports of the module +/******/ return module.exports; +/******/ } + + +/******/ // expose the modules object (__webpack_modules__) +/******/ __webpack_require__.m = modules; + +/******/ // expose the module cache +/******/ __webpack_require__.c = installedModules; + +/******/ // __webpack_public_path__ +/******/ __webpack_require__.p = ""; + +/******/ // Load entry module and return exports +/******/ return __webpack_require__(0); +/******/ }) +/************************************************************************/ +/******/ ([ +/* 0 */ +/***/ function(module, exports, __webpack_require__) { + + var __WEBPACK_AMD_DEFINE_RESULT__;/** + * SCEditor + * http://www.sceditor.com/ + * + * Copyright (C) 2014, Sam Clarke (samclarke.com) + * + * SCEditor is licensed under the MIT license: + * http://www.opensource.org/licenses/mit-license.php + * + * @fileoverview SCEditor - A lightweight WYSIWYG BBCode and HTML editor + * @author Sam Clarke + * @requires jQuery + */ + !(__WEBPACK_AMD_DEFINE_RESULT__ = function (require) { + 'use strict'; + + var $ = __webpack_require__(1); + var SCEditor = __webpack_require__(2); + var PluginManager = __webpack_require__(3); + var browser = __webpack_require__(6); + var escape = __webpack_require__(7); + + + // For backwards compatibility + $.sceditor = SCEditor; + + SCEditor.commands = __webpack_require__(9); + SCEditor.defaultOptions = __webpack_require__(10); + SCEditor.RangeHelper = __webpack_require__(4); + SCEditor.dom = __webpack_require__(5); + + SCEditor.ie = browser.ie; + SCEditor.ios = browser.ios; + SCEditor.isWysiwygSupported = browser.isWysiwygSupported; + + SCEditor.regexEscape = escape.regex; + SCEditor.escapeEntities = escape.entities; + SCEditor.escapeUriScheme = escape.uriScheme; + + SCEditor.PluginManager = PluginManager; + SCEditor.plugins = PluginManager.plugins; + + + /** + * Creates an instance of sceditor on all textareas + * matched by the jQuery selector. + * + * If options is set to "state" it will return bool value + * indicating if the editor has been initialised on the + * matched textarea(s). If there is only one textarea + * it will return the bool value for that textarea. + * If more than one textarea is matched it will + * return an array of bool values for each textarea. + * + * If options is set to "instance" it will return the + * current editor instance for the textarea(s). Like the + * state option, if only one textarea is matched this will + * return just the instance for that textarea. If more than + * one textarea is matched it will return an array of + * instances each textarea. + * + * @param {Object|String} options Should either be an Object of options or + * the strings "state" or "instance" + * @return {this|Array|jQuery.sceditor|Bool} + */ + $.fn.sceditor = function (options) { + var $this, instance, + ret = []; + + options = options || {}; + + if (!options.runWithoutWysiwygSupport && !browser.isWysiwygSupported) { + return; + } + + this.each(function () { + $this = this.jquery ? this : $(this); + instance = $this.data('sceditor'); + + // Don't allow the editor to be initialised + // on it's own source editor + if ($this.parents('.sceditor-container').length > 0) { + return; + } + + // Add state of instance to ret if that is what options is set to + if (options === 'state') { + ret.push(!!instance); + } else if (options === 'instance') { + ret.push(instance); + } else if (!instance) { + /*jshint -W031*/ + (new SCEditor(this, options)); + } + }); + + // If nothing in the ret array then must be init so return this + if (!ret.length) { + return this; + } + + return ret.length === 1 ? ret[0] : $(ret); + }; + }.call(exports, __webpack_require__, exports, module), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__)); + + +/***/ }, +/* 1 */ +/***/ function(module, exports) { + + module.exports = jQuery; + +/***/ }, +/* 2 */ +/***/ function(module, exports, __webpack_require__) { + + var __WEBPACK_AMD_DEFINE_RESULT__;!(__WEBPACK_AMD_DEFINE_RESULT__ = function (require) { + 'use strict'; + + var $ = __webpack_require__(1); + var PluginManager = __webpack_require__(3); + var RangeHelper = __webpack_require__(4); + var dom = __webpack_require__(5); + var escape = __webpack_require__(7); + var browser = __webpack_require__(6); + var _tmpl = __webpack_require__(8); + + var globalWin = window; + var globalDoc = document; + var $globalWin = $(globalWin); + var $globalDoc = $(globalDoc); + + var IE_VER = browser.ie; + + // In IE < 11 a BR at the end of a block level element + // causes a line break. In all other browsers it's collapsed. + var IE_BR_FIX = IE_VER && IE_VER < 11; + + + /** + * SCEditor - A lightweight WYSIWYG editor + * + * @param {Element} el The textarea to be converted + * @return {Object} options + * @class sceditor + * @name jQuery.sceditor + */ + var SCEditor = function (el, options) { + /** + * Alias of this + * + * @private + */ + var base = this; + + /** + * The textarea element being replaced + * + * @private + */ + var original = el.get ? el.get(0) : el; + var $original = $(original); + + /** + * The div which contains the editor and toolbar + * + * @private + */ + var $editorContainer; + + /** + * The editors toolbar + * + * @private + */ + var $toolbar; + + /** + * The editors iframe which should be in design mode + * + * @private + */ + var $wysiwygEditor; + var wysiwygEditor; + + /** + * The WYSIWYG editors body element + * + * @private + */ + var $wysiwygBody; + + /** + * The WYSIWYG editors document + * + * @private + */ + var $wysiwygDoc; + + /** + * The editors textarea for viewing source + * + * @private + */ + var $sourceEditor; + var sourceEditor; + + /** + * The current dropdown + * + * @private + */ + var $dropdown; + + /** + * Store the last cursor position. Needed for IE because it forgets + * + * @private + */ + var lastRange; + + /** + * The editors locale + * + * @private + */ + var locale; + + /** + * Stores a cache of preloaded images + * + * @private + * @type {Array} + */ + var preLoadCache = []; + + /** + * The editors rangeHelper instance + * + * @type {jQuery.sceditor.rangeHelper} + * @private + */ + var rangeHelper; + + /** + * Tags which require the new line fix + * + * @type {Array} + * @private + */ + var requireNewLineFix = []; + + /** + * An array of button state handlers + * + * @type {Array} + * @private + */ + var btnStateHandlers = []; + + /** + * Plugin manager instance + * + * @type {jQuery.sceditor.PluginManager} + * @private + */ + var pluginManager; + + /** + * The current node containing the selection/caret + * + * @type {Node} + * @private + */ + var currentNode; + + /** + * The first block level parent of the current node + * + * @type {node} + * @private + */ + var currentBlockNode; + + /** + * The current node selection/caret + * + * @type {Object} + * @private + */ + var currentSelection; + + /** + * Used to make sure only 1 selection changed + * check is called every 100ms. + * + * Helps improve performance as it is checked a lot. + * + * @type {Boolean} + * @private + */ + var isSelectionCheckPending; + + /** + * If content is required (equivalent to the HTML5 required attribute) + * + * @type {Boolean} + * @private + */ + var isRequired; + + /** + * The inline CSS style element. Will be undefined + * until css() is called for the first time. + * + * @type {HTMLElement} + * @private + */ + var inlineCss; + + /** + * Object containing a list of shortcut handlers + * + * @type {Object} + * @private + */ + var shortcutHandlers = {}; + + /** + * An array of all the current emoticons. + * + * Only used or populated when emoticonsCompat is enabled. + * + * @type {Array} + * @private + */ + var currentEmoticons = []; + + /** + * Cache of the current toolbar buttons + * + * @type {Object} + * @private + */ + var toolbarButtons = {}; + + /** + * If the current autoUpdate action is canceled. + * + * @type {Boolean} + * @private + */ + var autoUpdateCanceled; + + /** + * Private functions + * @private + */ + var init, + replaceEmoticons, + handleCommand, + saveRange, + initEditor, + initPlugins, + initLocale, + initToolBar, + initOptions, + initEvents, + initCommands, + initResize, + initEmoticons, + getWysiwygDoc, + handlePasteEvt, + handlePasteData, + handleKeyDown, + handleBackSpace, + handleKeyPress, + handleFormReset, + handleMouseDown, + handleEvent, + handleDocumentClick, + handleWindowResize, + updateToolBar, + updateActiveButtons, + sourceEditorSelectedText, + appendNewLine, + checkSelectionChanged, + checkNodeChanged, + autofocus, + emoticonsKeyPress, + emoticonsCheckWhitespace, + currentStyledBlockNode, + triggerValueChanged, + valueChangedBlur, + valueChangedKeyUp, + autoUpdate; + + /** + * All the commands supported by the editor + * @name commands + * @memberOf jQuery.sceditor.prototype + */ + base.commands = $.extend( + true, + {}, + (options.commands || SCEditor.commands) + ); + + /** + * Options for this editor instance + * @name opts + * @memberOf jQuery.sceditor.prototype + */ + base.opts = options = $.extend({}, SCEditor.defaultOptions, options); + + + /** + * Creates the editor iframe and textarea + * @private + */ + init = function () { + $original.data('sceditor', base); + + // Clone any objects in options + $.each(options, function (key, val) { + if ($.isPlainObject(val)) { + options[key] = $.extend(true, {}, val); + } + }); + + // Load locale + if (options.locale && options.locale !== 'en') { + initLocale(); + } + + $editorContainer = $('
    ') + .insertAfter($original) + .css('z-index', options.zIndex); + + // Add IE version to the container to allow IE specific CSS + // fixes without using CSS hacks or conditional comments + if (IE_VER) { + $editorContainer.addClass('ie ie' + IE_VER); + } + + isRequired = !!$original.attr('required'); + $original.removeAttr('required'); + + // create the editor + initPlugins(); + initEmoticons(); + initToolBar(); + initEditor(!!options.startInSourceMode); + initCommands(); + initOptions(); + initEvents(); + + // force into source mode if is a browser that can't handle + // full editing + if (!browser.isWysiwygSupported) { + base.toggleSourceMode(); + } + + updateActiveButtons(); + + var loaded = function () { + $globalWin.off('load', loaded); + + if (options.autofocus) { + autofocus(); + } + + if (options.autoExpand) { + base.expandToContent(); + } + + // Page width might have changed after CSS is loaded so + // call handleWindowResize to update any % based dimensions + handleWindowResize(); + + pluginManager.call('ready'); + }; + $globalWin.on('load', loaded); + if (globalDoc.readyState && globalDoc.readyState === 'complete') { + loaded(); + } + }; + + initPlugins = function () { + var plugins = options.plugins; + + plugins = plugins ? plugins.toString().split(',') : []; + pluginManager = new PluginManager(base); + + $.each(plugins, function (idx, plugin) { + pluginManager.register($.trim(plugin)); + }); + }; + + /** + * Init the locale variable with the specified locale if possible + * @private + * @return void + */ + initLocale = function () { + var lang; + + locale = SCEditor.locale[options.locale]; + + if (!locale) { + lang = options.locale.split('-'); + locale = SCEditor.locale[lang[0]]; + } + + // Locale DateTime format overrides any specified in the options + if (locale && locale.dateFormat) { + options.dateFormat = locale.dateFormat; + } + }; + + /** + * Creates the editor iframe and textarea + * @param {boolean} startInSourceMode Force loading the editor in this + * mode + * @private + */ + initEditor = function (startInSourceMode) { + var doc, tabIndex; + + $sourceEditor = $(''); + $wysiwygEditor = $( + '' + ); + + /* This needs to be done right after they are created because, + * for any reason, the user may not want the value to be tinkered + * by any filters. + */ + if (startInSourceMode) { + $editorContainer.addClass('sourceMode'); + $wysiwygEditor.hide(); + } else { + $editorContainer.addClass('wysiwygMode'); + $sourceEditor.hide(); + } + + if (!options.spellcheck) { + $sourceEditor.attr('spellcheck', 'false'); + } + + /*jshint scripturl: true*/ + if (globalWin.location.protocol === 'https:') { + $wysiwygEditor.attr('src', 'javascript:false'); + } + + // Add the editor to the container + $editorContainer.append($wysiwygEditor).append($sourceEditor); + wysiwygEditor = $wysiwygEditor[0]; + sourceEditor = $sourceEditor[0]; + + base.dimensions( + options.width || $original.width(), + options.height || $original.height() + ); + + doc = getWysiwygDoc(); + doc.open(); + doc.write(_tmpl('html', { + // Add IE version class to the HTML element so can apply + // conditional styling without CSS hacks + attrs: IE_VER ? ' class="ie ie' + IE_VER + '"' : '', + spellcheck: options.spellcheck ? '' : 'spellcheck="false"', + charset: options.charset, + style: options.style + })); + doc.close(); + + $wysiwygDoc = $(doc); + $wysiwygBody = $(doc.body); + + base.readOnly(!!options.readOnly); + + // iframe overflow fix for iOS, also fixes an IE issue with the + // editor not getting focus when clicking inside + if (browser.ios || IE_VER) { + $wysiwygBody.height('100%'); + + if (!IE_VER) { + $wysiwygBody.on('touchend', base.focus); + } + } + + tabIndex = $original.attr('tabindex'); + $sourceEditor.attr('tabindex', tabIndex); + $wysiwygEditor.attr('tabindex', tabIndex); + + rangeHelper = new RangeHelper(wysiwygEditor.contentWindow); + + // load any textarea value into the editor + base.val($original.hide().val()); + }; + + /** + * Initialises options + * @private + */ + initOptions = function () { + // auto-update original textbox on blur if option set to true + if (options.autoUpdate) { + $wysiwygBody.on('blur', autoUpdate); + $sourceEditor.on('blur', autoUpdate); + } + + if (options.rtl === null) { + options.rtl = $sourceEditor.css('direction') === 'rtl'; + } + + base.rtl(!!options.rtl); + + if (options.autoExpand) { + $wysiwygDoc.on('keyup', base.expandToContent); + } + + if (options.resizeEnabled) { + initResize(); + } + + $editorContainer.attr('id', options.id); + base.emoticons(options.emoticonsEnabled); + }; + + /** + * Initialises events + * @private + */ + initEvents = function () { + var CHECK_SELECTION_EVENTS = IE_VER ? + 'selectionchange' : + 'keyup focus blur contextmenu mouseup touchend click'; + + var EVENTS_TO_FORWARD = 'keydown keyup keypress ' + + 'focus blur contextmenu'; + + $globalDoc.click(handleDocumentClick); + + $(original.form) + .on('reset', handleFormReset) + .submit(base.updateOriginal); + + $globalWin.on('resize orientationChanged', handleWindowResize); + + $wysiwygBody + .keypress(handleKeyPress) + .keydown(handleKeyDown) + .keydown(handleBackSpace) + .keyup(appendNewLine) + .blur(valueChangedBlur) + .keyup(valueChangedKeyUp) + .on('paste', handlePasteEvt) + .on(CHECK_SELECTION_EVENTS, checkSelectionChanged) + .on(EVENTS_TO_FORWARD, handleEvent); + + if (options.emoticonsCompat && globalWin.getSelection) { + $wysiwygBody.keyup(emoticonsCheckWhitespace); + } + + $sourceEditor + .blur(valueChangedBlur) + .keyup(valueChangedKeyUp) + .keydown(handleKeyDown) + .on(EVENTS_TO_FORWARD, handleEvent); + + $wysiwygDoc + .mousedown(handleMouseDown) + .blur(valueChangedBlur) + .on(CHECK_SELECTION_EVENTS, checkSelectionChanged) + .on('beforedeactivate keyup mouseup', saveRange) + .keyup(appendNewLine) + .focus(function () { + lastRange = null; + }); + + $editorContainer + .on('selectionchanged', checkNodeChanged) + .on('selectionchanged', updateActiveButtons) + .on('selectionchanged valuechanged nodechanged', handleEvent); + }; + + /** + * Creates the toolbar and appends it to the container + * @private + */ + initToolBar = function () { + var $group, + commands = base.commands, + exclude = (options.toolbarExclude || '').split(','), + groups = options.toolbar.split('|'); + + $toolbar = $('
    '); + + $.each(groups, function (idx, group) { + $group = $('
    '); + + $.each(group.split(','), function (idx, commandName) { + var $button, shortcut, + command = commands[commandName]; + + // The commandName must be a valid command and not excluded + if (!command || $.inArray(commandName, exclude) > -1) { + return; + } + + shortcut = command.shortcut; + $button = _tmpl('toolbarButton', { + name: commandName, + dispName: base._(command.name || + command.tooltip || commandName) + }, true); + + $button + .data('sceditor-txtmode', !!command.txtExec) + .data('sceditor-wysiwygmode', !!command.exec) + .toggleClass('disabled', !command.exec) + .mousedown(function () { + // IE < 8 supports unselectable attribute + // so don't need this + if (!IE_VER || IE_VER < 9) { + autoUpdateCanceled = true; + } + }) + .click(function () { + var $this = $(this); + + if (!$this.hasClass('disabled')) { + handleCommand($this, command); + } + + updateActiveButtons(); + return false; + }); + + if (command.tooltip) { + $button.attr( + 'title', + base._(command.tooltip) + + (shortcut ? ' (' + shortcut + ')' : '') + ); + } + + if (shortcut) { + base.addShortcut(shortcut, commandName); + } + + if (command.state) { + btnStateHandlers.push({ + name: commandName, + state: command.state + }); + // exec string commands can be passed to queryCommandState + } else if (typeof command.exec === 'string') { + btnStateHandlers.push({ + name: commandName, + state: command.exec + }); + } + + $group.append($button); + toolbarButtons[commandName] = $button; + }); + + // Exclude empty groups + if ($group[0].firstChild) { + $toolbar.append($group); + } + }); + + // Append the toolbar to the toolbarContainer option if given + $(options.toolbarContainer || $editorContainer).append($toolbar); + }; + + /** + * Creates an array of all the key press functions + * like emoticons, ect. + * @private + */ + initCommands = function () { + $.each(base.commands, function (name, cmd) { + if (cmd.forceNewLineAfter && $.isArray(cmd.forceNewLineAfter)) { + requireNewLineFix = $.merge( + requireNewLineFix, + cmd.forceNewLineAfter + ); + } + }); + + appendNewLine(); + }; + + /** + * Creates the resizer. + * @private + */ + initResize = function () { + var minHeight, maxHeight, minWidth, maxWidth, + mouseMoveFunc, mouseUpFunc, + $grip = $('
    '), + // Cover is used to cover the editor iframe so document + // still gets mouse move events + $cover = $('
    '), + moveEvents = 'touchmove mousemove', + endEvents = 'touchcancel touchend mouseup', + startX = 0, + startY = 0, + newX = 0, + newY = 0, + startWidth = 0, + startHeight = 0, + origWidth = $editorContainer.width(), + origHeight = $editorContainer.height(), + isDragging = false, + rtl = base.rtl(); + + minHeight = options.resizeMinHeight || origHeight / 1.5; + maxHeight = options.resizeMaxHeight || origHeight * 2.5; + minWidth = options.resizeMinWidth || origWidth / 1.25; + maxWidth = options.resizeMaxWidth || origWidth * 1.25; + + mouseMoveFunc = function (e) { + // iOS uses window.event + if (e.type === 'touchmove') { + e = globalWin.event; + newX = e.changedTouches[0].pageX; + newY = e.changedTouches[0].pageY; + } else { + newX = e.pageX; + newY = e.pageY; + } + + var newHeight = startHeight + (newY - startY), + newWidth = rtl ? + startWidth - (newX - startX) : + startWidth + (newX - startX); + + if (maxWidth > 0 && newWidth > maxWidth) { + newWidth = maxWidth; + } + if (minWidth > 0 && newWidth < minWidth) { + newWidth = minWidth; + } + if (!options.resizeWidth) { + newWidth = false; + } + + if (maxHeight > 0 && newHeight > maxHeight) { + newHeight = maxHeight; + } + if (minHeight > 0 && newHeight < minHeight) { + newHeight = minHeight; + } + if (!options.resizeHeight) { + newHeight = false; + } + + if (newWidth || newHeight) { + base.dimensions(newWidth, newHeight); + + // The resize cover will not fill the container + // in IE6 unless a height is specified. + if (IE_VER < 7) { + $editorContainer.height(newHeight); + } + } + + e.preventDefault(); + }; + + mouseUpFunc = function (e) { + if (!isDragging) { + return; + } + + isDragging = false; + + $cover.hide(); + $editorContainer.removeClass('resizing').height('auto'); + $globalDoc.off(moveEvents, mouseMoveFunc); + $globalDoc.off(endEvents, mouseUpFunc); + + e.preventDefault(); + }; + + $editorContainer.append($grip); + $editorContainer.append($cover.hide()); + + $grip.on('touchstart mousedown', function (e) { + // iOS uses window.event + if (e.type === 'touchstart') { + e = globalWin.event; + startX = e.touches[0].pageX; + startY = e.touches[0].pageY; + } else { + startX = e.pageX; + startY = e.pageY; + } + + startWidth = $editorContainer.width(); + startHeight = $editorContainer.height(); + isDragging = true; + + $editorContainer.addClass('resizing'); + $cover.show(); + $globalDoc.on(moveEvents, mouseMoveFunc); + $globalDoc.on(endEvents, mouseUpFunc); + + // The resize cover will not fill the container in + // IE6 unless a height is specified. + if (IE_VER < 7) { + $editorContainer.height(startHeight); + } + + e.preventDefault(); + }); + }; + + /** + * Prefixes and preloads the emoticon images + * @private + */ + initEmoticons = function () { + var emoticon, + emoticons = options.emoticons, + root = options.emoticonsRoot; + + if (!$.isPlainObject(emoticons) || !options.emoticonsEnabled) { + return; + } + + $.each(emoticons, function (idx, val) { + $.each(val, function (key, url) { + // Prefix emoticon root to emoticon urls + if (root) { + url = { + url: root + (url.url || url), + tooltip: url.tooltip || key + }; + + emoticons[idx][key] = url; + } + + // Preload the emoticon + emoticon = globalDoc.createElement('img'); + emoticon.src = url.url || url; + preLoadCache.push(emoticon); + }); + }); + }; + + /** + * Autofocus the editor + * @private + */ + autofocus = function () { + var range, txtPos, + doc = $wysiwygDoc[0], + body = $wysiwygBody[0], + node = body.firstChild, + focusEnd = !!options.autofocusEnd; + + // Can't focus invisible elements + if (!$editorContainer.is(':visible')) { + return; + } + + if (base.sourceMode()) { + txtPos = focusEnd ? sourceEditor.value.length : 0; + + if (sourceEditor.setSelectionRange) { + sourceEditor.setSelectionRange(txtPos, txtPos); + } else { + range = sourceEditor.createTextRange(); + range.moveEnd('character', txtPos); + range.collapse(false); + range.select(); + } + + return; + } + + dom.removeWhiteSpace(body); + + if (focusEnd) { + if (!(node = body.lastChild)) { + node = doc.createElement('p'); + $wysiwygBody.append(node); + } + + while (node.lastChild) { + node = node.lastChild; + + // IE < 11 should place the cursor after the
    as + // it will show it as a newline. IE >= 11 and all + // other browsers should place the cursor before. + if (!IE_BR_FIX && $(node).is('br') && + node.previousSibling) { + node = node.previousSibling; + } + } + } + + if (doc.createRange) { + range = doc.createRange(); + + if (!dom.canHaveChildren(node)) { + range.setStartBefore(node); + + if (focusEnd) { + range.setStartAfter(node); + } + } else { + range.selectNodeContents(node); + } + } else { + range = body.createTextRange(); + range.moveToElementText(node.nodeType !== 3 ? + node : node.parentNode); + } + + range.collapse(!focusEnd); + rangeHelper.selectRange(range); + currentSelection = range; + + if (focusEnd) { + $wysiwygDoc.scrollTop(body.scrollHeight); + $wysiwygBody.scrollTop(body.scrollHeight); + } + + base.focus(); + }; + + /** + * Gets if the editor is read only + * + * @since 1.3.5 + * @function + * @memberOf jQuery.sceditor.prototype + * @name readOnly + * @return {Boolean} + */ + /** + * Sets if the editor is read only + * + * @param {boolean} readOnly + * @since 1.3.5 + * @function + * @memberOf jQuery.sceditor.prototype + * @name readOnly^2 + * @return {this} + */ + base.readOnly = function (readOnly) { + if (typeof readOnly !== 'boolean') { + return $sourceEditor.attr('readonly') === 'readonly'; + } + + $wysiwygBody[0].contentEditable = !readOnly; + + if (!readOnly) { + $sourceEditor.removeAttr('readonly'); + } else { + $sourceEditor.attr('readonly', 'readonly'); + } + + updateToolBar(readOnly); + + return base; + }; + + /** + * Gets if the editor is in RTL mode + * + * @since 1.4.1 + * @function + * @memberOf jQuery.sceditor.prototype + * @name rtl + * @return {Boolean} + */ + /** + * Sets if the editor is in RTL mode + * + * @param {boolean} rtl + * @since 1.4.1 + * @function + * @memberOf jQuery.sceditor.prototype + * @name rtl^2 + * @return {this} + */ + base.rtl = function (rtl) { + var dir = rtl ? 'rtl' : 'ltr'; + + if (typeof rtl !== 'boolean') { + return $sourceEditor.attr('dir') === 'rtl'; + } + + $wysiwygBody.attr('dir', dir); + $sourceEditor.attr('dir', dir); + + $editorContainer + .removeClass('rtl') + .removeClass('ltr') + .addClass(dir); + + return base; + }; + + /** + * Updates the toolbar to disable/enable the appropriate buttons + * @private + */ + updateToolBar = function (disable) { + var mode = base.inSourceMode() ? 'txtmode' : 'wysiwygmode'; + + $.each(toolbarButtons, function (idx, $button) { + if (disable === true || !$button.data('sceditor-' + mode)) { + $button.addClass('disabled'); + } else { + $button.removeClass('disabled'); + } + }); + }; + + /** + * Gets the width of the editor in pixels + * + * @since 1.3.5 + * @function + * @memberOf jQuery.sceditor.prototype + * @name width + * @return {int} + */ + /** + * Sets the width of the editor + * + * @param {int} width Width in pixels + * @since 1.3.5 + * @function + * @memberOf jQuery.sceditor.prototype + * @name width^2 + * @return {this} + */ + /** + * Sets the width of the editor + * + * The saveWidth specifies if to save the width. The stored width can be + * used for things like restoring from maximized state. + * + * @param {int} width Width in pixels + * @param {boolean} [saveWidth=true] If to store the width + * @since 1.4.1 + * @function + * @memberOf jQuery.sceditor.prototype + * @name width^3 + * @return {this} + */ + base.width = function (width, saveWidth) { + if (!width && width !== 0) { + return $editorContainer.width(); + } + + base.dimensions(width, null, saveWidth); + + return base; + }; + + /** + * Returns an object with the properties width and height + * which are the width and height of the editor in px. + * + * @since 1.4.1 + * @function + * @memberOf jQuery.sceditor.prototype + * @name dimensions + * @return {object} + */ + /** + *

    Sets the width and/or height of the editor.

    + * + *

    If width or height is not numeric it is ignored.

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

    Sets the width and/or height of the editor.

    + * + *

    If width or height is not numeric it is ignored.

    + * + *

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

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

    Inserts HTML into WYSIWYG editor.

    + * + *

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

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

    Inserts text into the WYSIWYG or source editor depending on which + * mode the editor is in.

    + * + *

    If endText is specified any selected text will be placed between + * text and endText. If no text is selected text and endText will + * just be concatenate together.

    + * + * @param {String} text + * @param {String} [endText=null] + * @since 1.3.5 + * @function + * @name insertText + * @memberOf jQuery.sceditor.prototype + */ + base.insertText = function (text, endText) { + if (base.inSourceMode()) { + base.sourceEditorInsertText(text, endText); + } else { + base.wysiwygEditorInsertText(text, endText); + } + + return base; + }; + + /** + *

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

    + * + *

    If endText is specified any selected text will be placed between + * text and endText. If no text is selected text and endText will + * just be concatenate together.

    + * + *

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

    + * + *

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

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

    Gets the value of the editor.

    + * + *

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

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

    Sets the value of the editor.

    + * + *

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

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

    Inserts HTML/BBCode into the editor

    + * + *

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

    + * + *

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

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

    Inserts HTML/BBCode into the editor

    + * + *

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

    + * + *

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

    + * + *

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

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

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

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

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

    + * + *

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

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

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

    + * + *

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

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

    or

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

    Binds a handler to the specified events

    + * + *

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

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

    + * + *

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

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

    Adds a handler to the node changed event.

    + * + *

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

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

    Adds a handler to the selection changed event

    + * + *

    Happens whenever the selection changes in WYSIWYG mode.

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

    Adds a handler to the value changed event

    + * + *

    Happens whenever the current editor value changes.

    + * + *

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

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

    ' + + '', + + toolbarButton: '' + + '
    {dispName}
    ', + + emoticon: '', + + fontOpt: '{font}', + + sizeOpt: '{size}', + + pastetext: + '
    ' + + '
    ' + + '
    ' + + '
    ', + + table: + '
    ' + + '
    ' + + '
    ', + + image: + '
    ' + + '
    ' + + '
    ' + + '
    ' + + '
    ' + + '
    ' + + '
    ' + + '
    ', + + email: + '
    ' + + '
    ' + + '
    ' + + '
    ' + + '
    ' + + '
    ', + + link: + '
    ' + + '
    ' + + '
    ' + + '
    ' + + '
    ', + + youtubeMenu: + '
    ' + + '
    ' + + '
    ' + + '
    ', + + youtube: + '' + }; + + /** + *

    Replaces any params in a template with the passed params.

    + * + *

    If createHtml is passed it will use jQuery to create the HTML. The + * same as doing: $(editor.tmpl("html", {params...}));

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

    ', root.ownerDocument) + .insertBefore(adjacentInlines[0]) + .append(adjacentInlines); + + adjacentInlines = []; + } + }; + + // Strip empty text nodes so they don't get wrapped. + dom.removeWhiteSpace(root); + + var node = root.firstChild; + while (node) { + if (dom.isInline(node) && !$(node).is('.sceditor-ignore')) { + adjacentInlines.push(node); + } else { + wrapAdjacents(); + } + + node = node.nextSibling; + } + + wrapAdjacents(); + }; + + /** + * Removes any attributes that are not white listed or + * if no attributes are white listed it will remove + * any attributes that are black listed. + * @param {Node} node + * @return {Void} + * @private + */ + removeAttribs = function (node) { + var tagName, attr, attrName, attrsLength, validValues, remove, + allowedAttribs = sceditorPlugins.xhtml.allowedAttribs, + isAllowed = allowedAttribs && + !$.isEmptyObject(allowedAttribs), + disallowedAttribs = sceditorPlugins.xhtml.disallowedAttribs, + isDisallowed = disallowedAttribs && + !$.isEmptyObject(disallowedAttribs); + + attrsCache = {}; + + dom.traverse(node, function (node) { + if (!node.attributes) { + return; + } + + tagName = node.nodeName.toLowerCase(); + attrsLength = node.attributes.length; + + if (attrsLength) { + if (!attrsCache[tagName]) { + if (isAllowed) { + attrsCache[tagName] = mergeAttribsFilters( + allowedAttribs['*'], + allowedAttribs[tagName] + ); + } else { + attrsCache[tagName] = mergeAttribsFilters( + disallowedAttribs['*'], + disallowedAttribs[tagName] + ); + } + } + + while (attrsLength--) { + attr = node.attributes[attrsLength]; + attrName = attr.name; + validValues = attrsCache[tagName][attrName]; + remove = false; + + if (isAllowed) { + remove = validValues !== null && + (!$.isArray(validValues) || + $.inArray(attr.value, validValues) < 0); + } else if (isDisallowed) { + remove = validValues === null || + ($.isArray(validValues) && + $.inArray(attr.value, validValues) > -1); + } + + if (remove) { + node.removeAttribute(attrName); + } + } + } + }); + }; + }; + + /** + * Tag conveters, a converter is applied to all + * tags that match the criteria. + * @type {Array} + * @name jQuery.sceditor.plugins.xhtml.converters + * @since v1.4.1 + */ + sceditorPlugins.xhtml.converters = [ + { + tags: { + '*': { + width: null + } + }, + conv: function (node, $node) { + $node.css('width', $node.attr('width')).removeAttr('width'); + } + }, + { + tags: { + '*': { + height: null + } + }, + conv: function (node, $node) { + $node.css('height', $node.attr('height')).removeAttr('height'); + } + }, + { + tags: { + 'li': { + value: null + } + }, + conv: function (node, $node) { + if (SCEditor.ie < 8) { + node.removeAttribute('value'); + } else { + $node.removeAttr('value'); + } + } + }, + { + tags: { + '*': { + text: null + } + }, + conv: function (node, $node) { + $node.css('color', $node.attr('text')).removeAttr('text'); + } + }, + { + tags: { + '*': { + color: null + } + }, + conv: function (node, $node) { + $node.css('color', $node.attr('color')).removeAttr('color'); + } + }, + { + tags: { + '*': { + face: null + } + }, + conv: function (node, $node) { + $node.css('fontFamily', $node.attr('face')).removeAttr('face'); + } + }, + { + tags: { + '*': { + align: null + } + }, + conv: function (node, $node) { + $node.css('textAlign', $node.attr('align')).removeAttr('align'); + } + }, + { + tags: { + '*': { + border: null + } + }, + conv: function (node, $node) { + $node + .css('borderWidth',$node.attr('border')) + .removeAttr('border'); + } + }, + { + tags: { + applet: { + name: null + }, + img: { + name: null + }, + layer: { + name: null + }, + map: { + name: null + }, + object: { + name: null + }, + param: { + name: null + } + }, + conv: function (node, $node) { + if (!$node.attr('id')) { + $node.attr('id', $node.attr('name')); + } + + $node.removeAttr('name'); + } + }, + { + tags: { + '*': { + vspace: null + } + }, + conv: function (node, $node) { + $node + .css('marginTop', $node.attr('vspace') - 0) + .css('marginBottom', $node.attr('vspace') - 0) + .removeAttr('vspace'); + } + }, + { + tags: { + '*': { + hspace: null + } + }, + conv: function (node, $node) { + $node + .css('marginLeft', $node.attr('hspace') - 0) + .css('marginRight', $node.attr('hspace') - 0) + .removeAttr('hspace'); + } + }, + { + tags: { + 'hr': { + noshade: null + } + }, + conv: function (node, $node) { + $node.css('borderStyle', 'solid').removeAttr('noshade'); + } + }, + { + tags: { + '*': { + nowrap: null + } + }, + conv: function (node, $node) { + $node.css('white-space', 'nowrap').removeAttr('nowrap'); + } + }, + { + tags: { + big: null + }, + conv: function (node) { + $(this.convertTagTo(node, 'span')).css('fontSize', 'larger'); + } + }, + { + tags: { + small: null + }, + conv: function (node) { + $(this.convertTagTo(node, 'span')).css('fontSize', 'smaller'); + } + }, + { + tags: { + b: null + }, + conv: function (node) { + $(this.convertTagTo(node, 'strong')); + } + }, + { + tags: { + u: null + }, + conv: function (node) { + $(this.convertTagTo(node, 'span')) + .css('textDecoration', 'underline'); + } + }, + { + tags: { + i: null + }, + conv: function (node) { + $(this.convertTagTo(node, 'em')); + } + }, + { + tags: { + s: null, + strike: null + }, + conv: function (node) { + $(this.convertTagTo(node, 'span')) + .css('textDecoration', 'line-through'); + } + }, + { + tags: { + dir: null + }, + conv: function (node) { + this.convertTagTo(node, 'ul'); + } + }, + { + tags: { + center: null + }, + conv: function (node) { + $(this.convertTagTo(node, 'div')) + .css('textAlign', 'center'); + } + }, + { + tags: { + font: { + size: null + } + }, + conv: function (node, $node) { + var size = $node.css('fontSize'), + fontSize = size; + + // IE < 8 sets a font tag with no size to +0 so + // should just skip it. + if (fontSize !== '+0') { + // IE 8 and below incorrectly returns the value of the size + // attribute instead of the px value so must convert it + if (SCEditor.ie < 9) { + fontSize = 10; + + if (size > 1) { + fontSize = 13; + } + if (size > 2) { + fontSize = 16; + } + if (size > 3) { + fontSize = 18; + } + if (size > 4) { + fontSize = 24; + } + if (size > 5) { + fontSize = 32; + } + if (size > 6) { + fontSize = 48; + } + } + + $node.css('fontSize', fontSize); + } + + $node.removeAttr('size'); + } + }, + { + tags: { + font: null + }, + conv: function (node) { + // All it's attributes will be converted + // by the attribute converters + this.convertTagTo(node, 'span'); + } + }, + { + tags: { + '*': { + type: ['_moz'] + } + }, + conv: function (node, $node) { + $node.removeAttr('type'); + } + }, + { + tags: { + '*': { + '_moz_dirty': null + } + }, + conv: function (node, $node) { + $node.removeAttr('_moz_dirty'); + } + }, + { + tags: { + '*': { + '_moz_editor_bogus_node': null + } + }, + conv: function (node, $node) { + $node.remove(); + } + } + ]; + + /** + * Allowed attributes map. + * + * To allow an attribute for all tags use * as the tag name. + * + * Leave empty or null to allow all attributes. (the disallow + * list will be used to filter them instead) + * @type {Object} + * @name jQuery.sceditor.plugins.xhtml.allowedAttribs + * @since v1.4.1 + */ + sceditorPlugins.xhtml.allowedAttribs = {}; + + /** + * Attributes that are not allowed. + * + * Only used if allowed attributes is null or empty. + * @type {Object} + * @name jQuery.sceditor.plugins.xhtml.disallowedAttribs + * @since v1.4.1 + */ + sceditorPlugins.xhtml.disallowedAttribs = {}; + + /** + * Array containing all the allowed tags. + * + * If null or empty all tags will be allowed. + * @type {Array} + * @name jQuery.sceditor.plugins.xhtml.allowedTags + * @since v1.4.1 + */ + sceditorPlugins.xhtml.allowedTags = []; + + /** + * Array containing all the disallowed tags. + * + * Only used if allowed tags is null or empty. + * @type {Array} + * @name jQuery.sceditor.plugins.xhtml.disallowedTags + * @since v1.4.1 + */ + sceditorPlugins.xhtml.disallowedTags = []; +}(jQuery)); diff --git a/js/sceditor/development/plugins/bbcode.js b/js/sceditor/development/plugins/bbcode.js new file mode 100644 index 00000000..f1c81bab --- /dev/null +++ b/js/sceditor/development/plugins/bbcode.js @@ -0,0 +1,2810 @@ +/** + * SCEditor BBCode Plugin + * http://www.sceditor.com/ + * + * Copyright (C) 2011-2014, Sam Clarke (samclarke.com) + * + * SCEditor is licensed under the MIT license: + * http://www.opensource.org/licenses/mit-license.php + * + * @fileoverview SCEditor BBCode Plugin + * @author Sam Clarke + * @requires jQuery + */ +/*global prompt: true*/ +/*jshint maxdepth: false*/ +// TODO: Tidy this code up and consider seperating the BBCode parser into a +// standalone module that can be used with other JS/NodeJS +(function ($, window, document) { + 'use strict'; + + var SCEditor = $.sceditor; + var sceditorPlugins = SCEditor.plugins; + var escapeEntities = SCEditor.escapeEntities; + var escapeUriScheme = SCEditor.escapeUriScheme; + + var IE_VER = SCEditor.ie; + + // In IE < 11 a BR at the end of a block level element + // causes a double line break. + var IE_BR_FIX = IE_VER && IE_VER < 11; + + + + var getEditorCommand = SCEditor.command.get; + + var defaultCommandsOverrides = { + bold: { + txtExec: ['[b]', '[/b]'] + }, + italic: { + txtExec: ['[i]', '[/i]'] + }, + underline: { + txtExec: ['[u]', '[/u]'] + }, + strike: { + txtExec: ['[s]', '[/s]'] + }, + subscript: { + txtExec: ['[sub]', '[/sub]'] + }, + superscript: { + txtExec: ['[sup]', '[/sup]'] + }, + left: { + txtExec: ['[left]', '[/left]'] + }, + center: { + txtExec: ['[center]', '[/center]'] + }, + right: { + txtExec: ['[right]', '[/right]'] + }, + justify: { + txtExec: ['[justify]', '[/justify]'] + }, + font: { + txtExec: function (caller) { + var editor = this; + + getEditorCommand('font')._dropDown( + editor, + caller, + function (fontName) { + editor.insertText( + '[font=' + fontName + ']', + '[/font]' + ); + } + ); + } + }, + size: { + txtExec: function (caller) { + var editor = this; + + getEditorCommand('size')._dropDown( + editor, + caller, + function (fontSize) { + editor.insertText( + '[size=' + fontSize + ']', + '[/size]' + ); + } + ); + } + }, + color: { + txtExec: function (caller) { + var editor = this; + + getEditorCommand('color')._dropDown( + editor, + caller, + function (color) { + editor.insertText( + '[color=' + color + ']', + '[/color]' + ); + } + ); + } + }, + bulletlist: { + txtExec: function (caller, selected) { + var content = ''; + + $.each(selected.split(/\r?\n/), function () { + content += (content ? '\n' : '') + + '[li]' + this + '[/li]'; + }); + + this.insertText('[ul]\n' + content + '\n[/ul]'); + } + }, + orderedlist: { + txtExec: function (caller, selected) { + var content = ''; + + $.each(selected.split(/\r?\n/), function () { + content += (content ? '\n' : '') + + '[li]' + this + '[/li]'; + }); + + sceditorPlugins.bbcode.bbcode.get(''); + + this.insertText('[ol]\n' + content + '\n[/ol]'); + } + }, + table: { + txtExec: ['[table][tr][td]', '[/td][/tr][/table]'] + }, + horizontalrule: { + txtExec: ['[hr]'] + }, + code: { + txtExec: ['[code]', '[/code]'] + }, + image: { + txtExec: function (caller, selected) { + var editor = this, + url = prompt(editor._('Enter the image URL:'), selected); + + if (url) { + editor.insertText('[img]' + url + '[/img]'); + } + } + }, + email: { + txtExec: function (caller, selected) { + var editor = this, + display = selected && selected.indexOf('@') > -1 ? + null : selected, + email = prompt(editor._('Enter the e-mail address:'), + (display ? '' : selected)), + text = prompt(editor._('Enter the displayed text:'), + display || email) || email; + + if (email) { + editor.insertText('[email=' + email + ']' + + text + '[/email]'); + } + } + }, + link: { + txtExec: function (caller, selected) { + var editor = this, + display = /^[a-z]+:\/\//i.test($.trim(selected)) ? + null : selected, + url = prompt(editor._('Enter URL:'), + (display ? 'http://' : $.trim(selected))), + text = prompt(editor._('Enter the displayed text:'), + display || url) || url; + + if (url) { + editor.insertText('[url=' + url + ']' + text + '[/url]'); + } + } + }, + quote: { + txtExec: ['[quote]', '[/quote]'] + }, + youtube: { + txtExec: function (caller) { + var editor = this; + + getEditorCommand('youtube')._dropDown( + editor, + caller, + function (id) { + editor.insertText('[youtube]' + id + '[/youtube]'); + } + ); + } + }, + rtl: { + txtExec: ['[rtl]', '[/rtl]'] + }, + ltr: { + txtExec: ['[ltr]', '[/ltr]'] + } + }; + + /** + * Removes any leading or trailing quotes ('") + * + * @return string + * @since v1.4.0 + */ + var _stripQuotes = function (str) { + return str ? + str.replace(/\\(.)/g, '$1').replace(/^(["'])(.*?)\1$/, '$2') : str; + }; + + /** + * Formats a string replacing {0}, {1}, {2}, ect. with + * the params provided + * + * @param {String} str The string to format + * @param {string} args... The strings to replace + * @return {String} + * @since v1.4.0 + */ + var _formatString = function () { + var undef; + var args = arguments; + + return args[0].replace(/\{(\d+)\}/g, function (str, p1) { + return args[p1 - 0 + 1] !== undef ? + args[p1 - 0 + 1] : + '{' + p1 + '}'; + }); + }; + + /** + * Enum of valid token types + * @type {Object} + * @private + */ + var TokenType = { + OPEN: 'open', + CONTENT: 'content', + NEWLINE: 'newline', + CLOSE: 'close' + }; + + + /** + * Tokenize token object + * + * @param {String} type The type of token this is, + * should be one of tokenType + * @param {String} name The name of this token + * @param {String} val The originally matched string + * @param {Array} attrs Any attributes. Only set on + * TokenType.OPEN tokens + * @param {Array} children Any children of this token + * @param {TokenizeToken} closing This tokens closing tag. + * Only set on TokenType.OPEN tokens + * @class TokenizeToken + * @name TokenizeToken + * @memberOf jQuery.sceditor.BBCodeParser.prototype + */ + var TokenizeToken = function ( + /*jshint maxparams: false*/ + type, name, val, attrs, children, closing + ) { + var base = this; + + base.type = type; + base.name = name; + base.val = val; + base.attrs = attrs || {}; + base.children = children || []; + base.closing = closing || null; + }; + + TokenizeToken.prototype = { + /** @lends jQuery.sceditor.BBCodeParser.prototype.TokenizeToken */ + /** + * Clones this token + * + * @param {Bool} includeChildren If to include the children in + * the clone. Defaults to false. + * @return {TokenizeToken} + */ + clone: function (includeChildren) { + var base = this; + + return new TokenizeToken( + base.type, + base.name, + base.val, + base.attrs, + includeChildren ? base.children : [], + base.closing ? base.closing.clone() : null + ); + }, + /** + * Splits this token at the specified child + * + * @param {TokenizeToken|Int} splitAt The child to split at or the + * index of the child + * @return {TokenizeToken} The right half of the split token or + * null if failed + */ + splitAt: function (splitAt) { + var clone; + var base = this; + var splitAtLength = 0; + var childrenLen = base.children.length; + + if (typeof splitAt !== 'number') { + splitAt = $.inArray(splitAt, base.children); + } + + if (splitAt < 0 || splitAt > childrenLen) { + return null; + } + + // Work out how many items are on the right side of the split + // to pass to splice() + while (childrenLen--) { + if (childrenLen >= splitAt) { + splitAtLength++; + } else { + childrenLen = 0; + } + } + + clone = base.clone(); + clone.children = base.children.splice(splitAt, splitAtLength); + return clone; + } + }; + + + /** + * SCEditor BBCode parser class + * + * @param {Object} options + * @class BBCodeParser + * @name jQuery.sceditor.BBCodeParser + * @since v1.4.0 + */ + var BBCodeParser = function (options) { + // make sure this is not being called as a function + if (!(this instanceof BBCodeParser)) { + return new BBCodeParser(options); + } + + var base = this; + + // Private methods + var init, + tokenizeTag, + tokenizeAttrs, + parseTokens, + normaliseNewLines, + fixNesting, + isChildAllowed, + removeEmpty, + fixChildren, + convertToHTML, + convertToBBCode, + hasTag, + quote, + lower, + last; + + + init = function () { + base.bbcodes = sceditorPlugins.bbcode.bbcodes; + base.opts = $.extend( + {}, + BBCodeParser.defaults, + options + ); + }; + + /** + * Takes a BBCode string and splits it into open, + * content and close tags. + * + * It does no checking to verify a tag has a matching open + * or closing tag or if the tag is valid child of any tag + * before it. For that the tokens should be passed to the + * parse function. + * + * @param {String} str + * @return {Array} + * @memberOf jQuery.sceditor.BBCodeParser.prototype + */ + base.tokenize = function (str) { + var matches, type, i; + var toks = []; + var tokens = [ + // Close must come before open as they are + // the same except close has a / at the start. + { + type: TokenType.CLOSE, + regex: /^\[\/[^\[\]]+\]/ + }, + { + type: TokenType.OPEN, + regex: /^\[[^\[\]]+\]/ + }, + { + type: TokenType.NEWLINE, + regex: /^(\r\n|\r|\n)/ + }, + { + type: TokenType.CONTENT, + regex: /^([^\[\r\n]+|\[)/ + } + ]; + + tokens.reverse(); + + strloop: + while (str.length) { + i = tokens.length; + while (i--) { + type = tokens[i].type; + + // Check if the string matches any of the tokens + if (!(matches = str.match(tokens[i].regex)) || + !matches[0]) { + continue; + } + + // Add the match to the tokens list + toks.push(tokenizeTag(type, matches[0])); + + // Remove the match from the string + str = str.substr(matches[0].length); + + // The token has been added so start again + continue strloop; + } + + // If there is anything left in the string which doesn't match + // any of the tokens then just assume it's content and add it. + if (str.length) { + toks.push(tokenizeTag(TokenType.CONTENT, str)); + } + + str = ''; + } + + return toks; + }; + + /** + * Extracts the name an params from a tag + * + * @param {tokenType} type + * @param {string} val + * @return {Object} + * @private + */ + tokenizeTag = function (type, val) { + var matches, attrs, name, + openRegex = /\[([^\]\s=]+)(?:([^\]]+))?\]/, + closeRegex = /\[\/([^\[\]]+)\]/; + + // Extract the name and attributes from opening tags and + // just the name from closing tags. + if (type === TokenType.OPEN && (matches = val.match(openRegex))) { + name = lower(matches[1]); + + if (matches[2] && (matches[2] = $.trim(matches[2]))) { + attrs = tokenizeAttrs(matches[2]); + } + } + + if (type === TokenType.CLOSE && (matches = val.match(closeRegex))) { + name = lower(matches[1]); + } + + if (type === TokenType.NEWLINE) { + name = '#newline'; + } + + // Treat all tokens without a name and + // all unknown BBCodes as content + if (!name || + ((type === TokenType.OPEN || type === TokenType.CLOSE) && + !sceditorPlugins.bbcode.bbcodes[name])) { + type = TokenType.CONTENT; + name = '#'; + } + + return new TokenizeToken(type, name, val, attrs); + }; + + /** + * Extracts the individual attributes from a string containing + * all the attributes. + * + * @param {String} attrs + * @return {Array} Assoc array of attributes + * @private + */ + tokenizeAttrs = function (attrs) { + var matches, + /* + ([^\s=]+) Anything that's not a space or equals + = Equals sign = + (?: + (?: + (["']) The opening quote + ( + (?:\\\2|[^\2])*? Anything that isn't the + unescaped opening quote + ) + \2 The opening quote again which + will close the string + ) + | If not a quoted string then match + ( + (?:.(?!\s\S+=))*.? Anything that isn't part of + [space][non-space][=] which + would be a new attribute + ) + ) + */ + attrRegex = + /([^\s=]+)=(?:(?:(["'])((?:\\\2|[^\2])*?)\2)|((?:.(?!\s\S+=))*.))/g, + ret = {}; + + // if only one attribute then remove the = from the start and + // strip any quotes + if (attrs.charAt(0) === '=' && attrs.indexOf('=', 1) < 0) { + ret.defaultattr = _stripQuotes(attrs.substr(1)); + } else { + if (attrs.charAt(0) === '=') { + attrs = 'defaultattr' + attrs; + } + + // No need to strip quotes here, the regex will do that. + while ((matches = attrRegex.exec(attrs))) { + ret[lower(matches[1])] = + _stripQuotes(matches[3]) || matches[4]; + } + } + + return ret; + }; + + /** + * Parses a string into an array of BBCodes + * + * @param {string} str + * @param {boolean} preserveNewLines If to preserve all new lines, not + * strip any based on the passed + * formatting options + * @return {Array} Array of BBCode objects + * @memberOf jQuery.sceditor.BBCodeParser.prototype + */ + base.parse = function (str, preserveNewLines) { + var ret = parseTokens(base.tokenize(str)); + var opts = base.opts; + + if (opts.fixInvalidChildren) { + fixChildren(ret); + } + + if (opts.removeEmptyTags) { + removeEmpty(ret); + } + + if (opts.fixInvalidNesting) { + fixNesting(ret); + } + + normaliseNewLines(ret, null, preserveNewLines); + + if (opts.removeEmptyTags) { + removeEmpty(ret); + } + + return ret; + }; + + /** + * Checks if an array of TokenizeToken's contains the + * specified token. + * + * Checks the tokens name and type match another tokens + * name and type in the array. + * + * @param {string} name + * @param {tokenType} type + * @param {Array} arr + * @return {Boolean} + * @private + */ + hasTag = function (name, type, arr) { + var i = arr.length; + + while (i--) { + if (arr[i].type === type && arr[i].name === name) { + return true; + } + } + + return false; + }; + + /** + * Checks if the child tag is allowed as one + * of the parent tags children. + * + * @param {TokenizeToken} parent + * @param {TokenizeToken} child + * @return {Boolean} + * @private + */ + isChildAllowed = function (parent, child) { + var parentBBCode = parent ? base.bbcodes[parent.name] : {}, + allowedChildren = parentBBCode.allowedChildren; + + if (base.opts.fixInvalidChildren && allowedChildren) { + return $.inArray(child.name || '#', allowedChildren) > -1; + } + + return true; + }; + +// TODO: Tidy this parseTokens() function up a bit. + /** + * Parses an array of tokens created by tokenize() + * + * @param {Array} toks + * @return {Array} Parsed tokens + * @see tokenize() + * @private + */ + parseTokens = function (toks) { + var token, bbcode, curTok, clone, i, previous, next, + cloned = [], + output = [], + openTags = [], + /** + * Returns the currently open tag or undefined + * @return {TokenizeToken} + */ + currentOpenTag = function () { + return last(openTags); + }, + /** + * Adds a tag to either the current tags children + * or to the output array. + * @param {TokenizeToken} token + * @private + */ + addTag = function (token) { + if (currentOpenTag()) { + currentOpenTag().children.push(token); + } else { + output.push(token); + } + }, + /** + * Checks if this tag closes the current tag + * @param {String} name + * @return {Void} + */ + closesCurrentTag = function (name) { + return currentOpenTag() && + (bbcode = base.bbcodes[currentOpenTag().name]) && + bbcode.closedBy && + $.inArray(name, bbcode.closedBy) > -1; + }; + + while ((token = toks.shift())) { + next = toks[0]; + + /* jshint indent:false */ + switch (token.type) { + case TokenType.OPEN: + // Check it this closes a parent, + // e.g. for lists [*]one [*]two + if (closesCurrentTag(token.name)) { + openTags.pop(); + } + + addTag(token); + bbcode = base.bbcodes[token.name]; + + // If this tag is not self closing and it has a closing + // tag then it is open and has children so add it to the + // list of open tags. If has the closedBy property then + // it is closed by other tags so include everything as + // it's children until one of those tags is reached. + if ((!bbcode || !bbcode.isSelfClosing) && + (bbcode.closedBy || + hasTag(token.name, TokenType.CLOSE, toks))) { + openTags.push(token); + } else if (!bbcode || !bbcode.isSelfClosing) { + token.type = TokenType.CONTENT; + } + break; + + case TokenType.CLOSE: + // check if this closes the current tag, + // e.g. [/list] would close an open [*] + if (currentOpenTag() && + token.name !== currentOpenTag().name && + closesCurrentTag('/' + token.name)) { + openTags.pop(); + } + + // If this is closing the currently open tag just pop + // the close tag off the open tags array + if (currentOpenTag() && + token.name === currentOpenTag().name) { + currentOpenTag().closing = token; + openTags.pop(); + + // If this is closing an open tag that is the parent of + // the current tag then clone all the tags including the + // current one until reaching the parent that is being + // closed. Close the parent and then add the clones back + // in. + } else if (hasTag(token.name, TokenType.OPEN, + openTags)) { + + // Remove the tag from the open tags + while ((curTok = openTags.pop())) { + + // If it's the tag that is being closed then + // discard it and break the loop. + if (curTok.name === token.name) { + curTok.closing = token; + break; + } + + // Otherwise clone this tag and then add any + // previously cloned tags as it's children + clone = curTok.clone(); + + if (cloned.length) { + clone.children.push(last(cloned)); + } + + cloned.push(clone); + } + + // Add the last cloned child to the now current tag + // (the parent of the tag which was being closed) + addTag(last(cloned)); + + // Add all the cloned tags to the open tags list + i = cloned.length; + while (i--) { + openTags.push(cloned[i]); + } + + cloned.length = 0; + + // This tag is closing nothing so treat it as content + } else { + token.type = TokenType.CONTENT; + addTag(token); + } + break; + + case TokenType.NEWLINE: + // handle things like + // [*]list\nitem\n[*]list1 + // where it should come out as + // [*]list\nitem[/*]\n[*]list1[/*] + // instead of + // [*]list\nitem\n[/*][*]list1[/*] + if (currentOpenTag() && next && + closesCurrentTag( + (next.type === TokenType.CLOSE ? '/' : '') + + next.name + )) { + // skip if the next tag is the closing tag for + // the option tag, i.e. [/*] + if (!(next.type === TokenType.CLOSE && + next.name === currentOpenTag().name)) { + bbcode = base.bbcodes[currentOpenTag().name]; + + if (bbcode && bbcode.breakAfter) { + openTags.pop(); + } else if (bbcode && + bbcode.isInline === false && + base.opts.breakAfterBlock && + bbcode.breakAfter !== false) { + openTags.pop(); + } + } + } + + addTag(token); + break; + + default: // content + addTag(token); + break; + } + + previous = token; + } + + return output; + }; + + /** + * Normalise all new lines + * + * Removes any formatting new lines from the BBCode + * leaving only content ones. I.e. for a list: + * + * [list] + * [*] list item one + * with a line break + * [*] list item two + * [/list] + * + * would become + * + * [list] [*] list item one + * with a line break [*] list item two [/list] + * + * Which makes it easier to convert to HTML or add + * the formatting new lines back in when converting + * back to BBCode + * + * @param {Array} children + * @param {TokenizeToken} parent + * @param {Bool} onlyRemoveBreakAfter + * @return {void} + */ + normaliseNewLines = function (children, parent, onlyRemoveBreakAfter) { + var token, left, right, parentBBCode, bbcode, + removedBreakEnd, removedBreakBefore, remove; + var childrenLength = children.length; +// TODO: this function really needs tidying up + if (parent) { + parentBBCode = base.bbcodes[parent.name]; + } + + var i = childrenLength; + while (i--) { + if (!(token = children[i])) { + continue; + } + + if (token.type === TokenType.NEWLINE) { + left = i > 0 ? children[i - 1] : null; + right = i < childrenLength - 1 ? children[i + 1] : null; + remove = false; + + // Handle the start and end new lines + // e.g. [tag]\n and \n[/tag] + if (!onlyRemoveBreakAfter && parentBBCode && + parentBBCode.isSelfClosing !== true) { + // First child of parent so must be opening line break + // (breakStartBlock, breakStart) e.g. [tag]\n + if (!left) { + if (parentBBCode.isInline === false && + base.opts.breakStartBlock && + parentBBCode.breakStart !== false) { + remove = true; + } + + if (parentBBCode.breakStart) { + remove = true; + } + // Last child of parent so must be end line break + // (breakEndBlock, breakEnd) + // e.g. \n[/tag] + // remove last line break (breakEndBlock, breakEnd) + } else if (!removedBreakEnd && !right) { + if (parentBBCode.isInline === false && + base.opts.breakEndBlock && + parentBBCode.breakEnd !== false) { + remove = true; + } + + if (parentBBCode.breakEnd) { + remove = true; + } + + removedBreakEnd = remove; + } + } + + if (left && left.type === TokenType.OPEN) { + if ((bbcode = base.bbcodes[left.name])) { + if (!onlyRemoveBreakAfter) { + if (bbcode.isInline === false && + base.opts.breakAfterBlock && + bbcode.breakAfter !== false) { + remove = true; + } + + if (bbcode.breakAfter) { + remove = true; + } + } else if (bbcode.isInline === false) { + remove = true; + } + } + } + + if (!onlyRemoveBreakAfter && !removedBreakBefore && + right && right.type === TokenType.OPEN) { + + if ((bbcode = base.bbcodes[right.name])) { + if (bbcode.isInline === false && + base.opts.breakBeforeBlock && + bbcode.breakBefore !== false) { + remove = true; + } + + if (bbcode.breakBefore) { + remove = true; + } + + removedBreakBefore = remove; + + if (remove) { + children.splice(i, 1); + continue; + } + } + } + + if (remove) { + children.splice(i, 1); + } + + // reset double removedBreakBefore removal protection. + // This is needed for cases like \n\n[\tag] where + // only 1 \n should be removed but without this they both + // would be. + removedBreakBefore = false; + } else if (token.type === TokenType.OPEN) { + normaliseNewLines(token.children, token, + onlyRemoveBreakAfter); + } + } + }; + + /** + * Fixes any invalid nesting. + * + * If it is a block level element inside 1 or more inline elements + * then those inline elements will be split at the point where the + * block level is and the block level element placed between the split + * parts. i.e. + * [inline]A[blocklevel]B[/blocklevel]C[/inline] + * Will become: + * [inline]A[/inline][blocklevel]B[/blocklevel][inline]C[/inline] + * + * @param {Array} children + * @param {Array} [parents] Null if there is no parents + * @param {Array} [insideInline] Boolean, if inside an inline element + * @param {Array} [rootArr] Root array if there is one + * @return {Array} + * @private + */ + fixNesting = function (children, parents, insideInline, rootArr) { + var token, i, parent, parentIndex, parentParentChildren, right; + + var isInline = function (token) { + var bbcode = base.bbcodes[token.name]; + + return !bbcode || bbcode.isInline !== false; + }; + + parents = parents || []; + rootArr = rootArr || children; + + // This must check the length each time as it can change when + // tokens are moved to fix the nesting. + for (i = 0; i < children.length; i++) { + if (!(token = children[i]) || token.type !== TokenType.OPEN) { + continue; + } + + if (!isInline(token) && insideInline) { + // if this is a blocklevel element inside an inline one then + // split the parent at the block level element + parent = last(parents); + right = parent.splitAt(token); + + parentParentChildren = parents.length > 1 ? + parents[parents.length - 2].children : rootArr; + + parentIndex = $.inArray(parent, parentParentChildren); + if (parentIndex > -1) { + // remove the block level token from the right side of + // the split inline element + right.children.splice( + $.inArray(token, right.children), 1); + + // insert the block level token and the right side after + // the left side of the inline token + parentParentChildren.splice( + parentIndex + 1, 0, token, right + ); + + // return to parents loop as the + // children have now increased + return; + } + + } + + parents.push(token); + + fixNesting( + token.children, + parents, + insideInline || isInline(token), + rootArr + ); + + parents.pop(token); + } + }; + + /** + * Fixes any invalid children. + * + * If it is an element which isn't allowed as a child of it's parent + * then it will be converted to content of the parent element. i.e. + * [code]Code [b]only[/b] allows text.[/code] + * Will become: + * Code [b]only[/b] allows text. + * Instead of: + * Code only allows text. + * + * @param {Array} children + * @param {Array} [parent] Null if there is no parents + * @private + */ + fixChildren = function (children, parent) { + var token, args; + + var i = children.length; + while (i--) { + if (!(token = children[i])) { + continue; + } + + if (!isChildAllowed(parent, token)) { + // if it is not then convert it to text and see if it + // is allowed + token.name = null; + token.type = TokenType.CONTENT; + + if (isChildAllowed(parent, token)) { + args = [i + 1, 0].concat(token.children); + + if (token.closing) { + token.closing.name = null; + token.closing.type = TokenType.CONTENT; + args.push(token.closing); + } + + i += args.length - 1; + Array.prototype.splice.apply(children, args); + } else { + parent.children.splice(i, 1); + } + } + + if (token.type === TokenType.OPEN) { + fixChildren(token.children, token); + } + } + }; + + /** + * Removes any empty BBCodes which are not allowed to be empty. + * + * @param {Array} tokens + * @private + */ + removeEmpty = function (tokens) { + var token, bbcode; + + /** + * Checks if all children are whitespace or not + * @private + */ + var isTokenWhiteSpace = function (children) { + var j = children.length; + + while (j--) { + var type = children[j].type; + + if (type === TokenType.OPEN || type === TokenType.CLOSE) { + return false; + } + + if (type === TokenType.CONTENT && + /\S|\u00A0/.test(children[j].val)) { + return false; + } + } + + return true; + }; + + var i = tokens.length; + while (i--) { + // So skip anything that isn't a tag since only tags can be + // empty, content can't + if (!(token = tokens[i]) || token.type !== TokenType.OPEN) { + continue; + } + + bbcode = base.bbcodes[token.name]; + + // Remove any empty children of this tag first so that if they + // are all removed this one doesn't think it's not empty. + removeEmpty(token.children); + + if (isTokenWhiteSpace(token.children) && bbcode && + !bbcode.isSelfClosing && !bbcode.allowsEmpty) { + tokens.splice.apply( + tokens, + $.merge([i, 1], token.children) + ); + } + } + }; + + /** + * Converts a BBCode string to HTML + * + * @param {String} str + * @param {Bool} preserveNewLines If to preserve all new lines, not + * strip any based on the passed + * formatting options + * @return {String} + * @memberOf jQuery.sceditor.BBCodeParser.prototype + */ + base.toHTML = function (str, preserveNewLines) { + return convertToHTML(base.parse(str, preserveNewLines), true); + }; + + /** + * @private + */ + convertToHTML = function (tokens, isRoot) { + var undef, token, bbcode, content, html, needsBlockWrap, + blockWrapOpen, isInline, lastChild, + ret = []; + + isInline = function (bbcode) { + return (!bbcode || (bbcode.isHtmlInline !== undef ? + bbcode.isHtmlInline : bbcode.isInline)) !== false; + }; + + while (tokens.length > 0) { + if (!(token = tokens.shift())) { + continue; + } + + if (token.type === TokenType.OPEN) { + lastChild = + token.children[token.children.length - 1] || {}; + bbcode = base.bbcodes[token.name]; + needsBlockWrap = isRoot && isInline(bbcode); + content = convertToHTML(token.children, false); + + if (bbcode && bbcode.html) { + // Only add a line break to the end if this is + // blocklevel and the last child wasn't block-level + if (!isInline(bbcode) && + isInline(base.bbcodes[lastChild.name]) && + !bbcode.isPreFormatted && + !bbcode.skipLastLineBreak) { + // Add placeholder br to end of block level elements + // in all browsers apart from IE < 9 which handle + // new lines differently and doesn't need one. + if (!IE_BR_FIX) { + content += '
    '; + } + } + + if (!$.isFunction(bbcode.html)) { + token.attrs['0'] = content; + html = sceditorPlugins.bbcode.formatBBCodeString( + bbcode.html, + token.attrs + ); + } else { + html = bbcode.html.call( + base, + token, + token.attrs, + content + ); + } + } else { + html = token.val + content + + (token.closing ? token.closing.val : ''); + } + } else if (token.type === TokenType.NEWLINE) { + if (!isRoot) { + ret.push('
    '); + continue; + } + + // If not already in a block wrap then start a new block + if (!blockWrapOpen) { + ret.push('

    '); + + // If it's an empty DIV and compatibility mode is below + // IE8 then we must add a non-breaking space to the div + // otherwise the div will be collapsed. Adding a BR + // works but when you press enter to make a newline it + // suddenly goes back to the normal IE div behavior and + // creates two lines, one for the newline and one for + // the BR. I'm sure there must be a better fix but I've + // yet to find one. + // Cannot do zoom: 1; or set a height on the div to fix + // it as that causes resize handles to be added to the + // div when it's clicked on. + if (IE_VER < 8 || (document.documentMode && + document.documentMode < 8)) { + ret.push('\u00a0'); + } + } + + // Putting BR in a div in IE causes it + // to do a double line break. + if (!IE_BR_FIX) { + ret.push('
    '); + } + + // Normally the div acts as a line-break with by moving + // whatever comes after onto a new line. + // If this is the last token, add an extra line-break so it + // shows as there will be nothing after it. + if (!tokens.length) { + ret.push('
    '); + } + + ret.push('
    \n'); + blockWrapOpen = false; + continue; + // content + } else { + needsBlockWrap = isRoot; + html = escapeEntities(token.val, true); + } + + if (needsBlockWrap && !blockWrapOpen) { + ret.push('
    '); + blockWrapOpen = true; + } else if (!needsBlockWrap && blockWrapOpen) { + ret.push('
    \n'); + blockWrapOpen = false; + } + + ret.push(html); + } + + if (blockWrapOpen) { + ret.push('
    \n'); + } + + return ret.join(''); + }; + + /** + * Takes a BBCode string, parses it then converts it back to BBCode. + * + * This will auto fix the BBCode and format it with the specified + * options. + * + * @param {String} str + * @param {Bool} preserveNewLines If to preserve all new lines, not + * strip any based on the passed + * formatting options + * @return {String} + * @memberOf jQuery.sceditor.BBCodeParser.prototype + */ + base.toBBCode = function (str, preserveNewLines) { + return convertToBBCode(base.parse(str, preserveNewLines)); + }; + + /** + * Converts parsed tokens back into BBCode with the + * formatting specified in the options and with any + * fixes specified. + * + * @param {Array} toks Array of parsed tokens from base.parse() + * @return {String} + * @private + */ + convertToBBCode = function (toks) { + var token, attr, bbcode, isBlock, isSelfClosing, quoteType, + breakBefore, breakStart, breakEnd, breakAfter, + // Create an array of strings which are joined together + // before being returned as this is faster in slow browsers. + // (Old versions of IE). + ret = []; + + while (toks.length > 0) { + if (!(token = toks.shift())) { + continue; + } +// TODO: tidy this + bbcode = base.bbcodes[token.name]; + isBlock = !(!bbcode || bbcode.isInline !== false); + isSelfClosing = bbcode && bbcode.isSelfClosing; + + breakBefore = (isBlock && base.opts.breakBeforeBlock && + bbcode.breakBefore !== false) || + (bbcode && bbcode.breakBefore); + + breakStart = (isBlock && !isSelfClosing && + base.opts.breakStartBlock && + bbcode.breakStart !== false) || + (bbcode && bbcode.breakStart); + + breakEnd = (isBlock && base.opts.breakEndBlock && + bbcode.breakEnd !== false) || + (bbcode && bbcode.breakEnd); + + breakAfter = (isBlock && base.opts.breakAfterBlock && + bbcode.breakAfter !== false) || + (bbcode && bbcode.breakAfter); + + quoteType = (bbcode ? bbcode.quoteType : null) || + base.opts.quoteType || BBCodeParser.QuoteType.auto; + + if (!bbcode && token.type === TokenType.OPEN) { + ret.push(token.val); + + if (token.children) { + ret.push(convertToBBCode(token.children)); + } + + if (token.closing) { + ret.push(token.closing.val); + } + } else if (token.type === TokenType.OPEN) { + if (breakBefore) { + ret.push('\n'); + } + + // Convert the tag and it's attributes to BBCode + ret.push('[' + token.name); + if (token.attrs) { + if (token.attrs.defaultattr) { + ret.push('=', quote( + token.attrs.defaultattr, + quoteType, + 'defaultattr' + )); + + delete token.attrs.defaultattr; + } + + for (attr in token.attrs) { + if (token.attrs.hasOwnProperty(attr)) { + ret.push(' ', attr, '=', + quote(token.attrs[attr], quoteType, attr)); + } + } + } + ret.push(']'); + + if (breakStart) { + ret.push('\n'); + } + + // Convert the tags children to BBCode + if (token.children) { + ret.push(convertToBBCode(token.children)); + } + + // add closing tag if not self closing + if (!isSelfClosing && !bbcode.excludeClosing) { + if (breakEnd) { + ret.push('\n'); + } + + ret.push('[/' + token.name + ']'); + } + + if (breakAfter) { + ret.push('\n'); + } + + // preserve whatever was recognized as the + // closing tag if it is a self closing tag + if (token.closing && isSelfClosing) { + ret.push(token.closing.val); + } + } else { + ret.push(token.val); + } + } + + return ret.join(''); + }; + + /** + * Quotes an attribute + * + * @param {String} str + * @param {BBCodeParser.QuoteType} quoteType + * @param {String} name + * @return {String} + * @private + */ + quote = function (str, quoteType, name) { + var QuoteTypes = BBCodeParser.QuoteType, + needsQuotes = /\s|=/.test(str); + + if ($.isFunction(quoteType)) { + return quoteType(str, name); + } + + if (quoteType === QuoteTypes.never || + (quoteType === QuoteTypes.auto && !needsQuotes)) { + return str; + } + + return '"' + str.replace('\\', '\\\\').replace('"', '\\"') + '"'; + }; + + /** + * Returns the last element of an array or null + * + * @param {Array} arr + * @return {Object} Last element + * @private + */ + last = function (arr) { + if (arr.length) { + return arr[arr.length - 1]; + } + + return null; + }; + + /** + * Converts a string to lowercase. + * + * @param {String} str + * @return {String} Lowercase version of str + * @private + */ + lower = function (str) { + return str.toLowerCase(); + }; + + init(); + }; + + /** + * Quote type + * @type {Object} + * @class QuoteType + * @name jQuery.sceditor.BBCodeParser.QuoteType + * @since v1.4.0 + */ + BBCodeParser.QuoteType = { + /** @lends jQuery.sceditor.BBCodeParser.QuoteType */ + /** + * Always quote the attribute value + * @type {Number} + */ + always: 1, + + /** + * Never quote the attributes value + * @type {Number} + */ + never: 2, + + /** + * Only quote the attributes value when it contains spaces to equals + * @type {Number} + */ + auto: 3 + }; + + /** + * Default BBCode parser options + * @type {Object} + */ + BBCodeParser.defaults = { + /** + * If to add a new line before block level elements + * + * @type {Boolean} + */ + breakBeforeBlock: false, + + /** + * If to add a new line after the start of block level elements + * + * @type {Boolean} + */ + breakStartBlock: false, + + /** + * If to add a new line before the end of block level elements + * + * @type {Boolean} + */ + breakEndBlock: false, + + /** + * If to add a new line after block level elements + * + * @type {Boolean} + */ + breakAfterBlock: true, + + /** + * If to remove empty tags + * + * @type {Boolean} + */ + removeEmptyTags: true, + + /** + * If to fix invalid nesting, + * i.e. block level elements inside inline elements. + * + * @type {Boolean} + */ + fixInvalidNesting: true, + + /** + * If to fix invalid children. + * i.e. A tag which is inside a parent that doesn't + * allow that type of tag. + * + * @type {Boolean} + */ + fixInvalidChildren: true, + + /** + * Attribute quote type + * + * @type {BBCodeParser.QuoteType} + * @since 1.4.1 + */ + quoteType: BBCodeParser.QuoteType.auto + }; + + /** + * Deprecated, use sceditorPlugins.bbcode + * + * @class sceditorBBCodePlugin + * @name jQuery.sceditor.sceditorBBCodePlugin + * @deprecated + */ + $.sceditorBBCodePlugin = + /** + * BBCode plugin for SCEditor + * + * @class bbcode + * @name jQuery.sceditor.plugins.bbcode + * @since 1.4.1 + */ + sceditorPlugins.bbcode = function () { + var base = this; + + /** + * Private methods + * @private + */ + var buildBbcodeCache, + handleStyles, + handleTags, + removeFirstLastDiv; + + base.bbcodes = sceditorPlugins.bbcode.bbcodes; + base.stripQuotes = _stripQuotes; + + /** + * cache of all the tags pointing to their bbcodes to enable + * faster lookup of which bbcode a tag should have + * @private + */ + var tagsToBBCodes = {}; + + /** + * Same as tagsToBBCodes but instead of HTML tags it's styles + * @private + */ + var stylesToBBCodes = {}; + + /** + * Allowed children of specific HTML tags. Empty array if no + * children other than text nodes are allowed + * @private + */ + var validChildren = { + ul: ['li', 'ol', 'ul'], + ol: ['li', 'ol', 'ul'], + table: ['tr'], + tr: ['td', 'th'], + code: ['br', 'p', 'div'] + }; + + /** + * Initializer + * @private + */ + base.init = function () { + base.opts = this.opts; + + // build the BBCode cache + buildBbcodeCache(); + + this.commands = $.extend( + true, {}, defaultCommandsOverrides, this.commands + ); + + // Add BBCode helper methods + this.toBBCode = base.signalToSource; + this.fromBBCode = base.signalToWysiwyg; + }; + + /** + * Populates tagsToBBCodes and stylesToBBCodes to enable faster lookups + * + * @private + */ + buildBbcodeCache = function () { + $.each(base.bbcodes, function (bbcode) { + var isBlock, + tags = base.bbcodes[bbcode].tags, + styles = base.bbcodes[bbcode].styles; + + if (tags) { + $.each(tags, function (tag, values) { + isBlock = base.bbcodes[bbcode].isInline === false; + + tagsToBBCodes[tag] = tagsToBBCodes[tag] || {}; + + tagsToBBCodes[tag][isBlock] = + tagsToBBCodes[tag][isBlock] || {}; + + tagsToBBCodes[tag][isBlock][bbcode] = values; + }); + } + + if (styles) { + $.each(styles, function (style, values) { + isBlock = base.bbcodes[bbcode].isInline === false; + + stylesToBBCodes[isBlock] = + stylesToBBCodes[isBlock] || {}; + + stylesToBBCodes[isBlock][style] = + stylesToBBCodes[isBlock][style] || {}; + + stylesToBBCodes[isBlock][style][bbcode] = values; + }); + } + }); + }; + + /** + * Checks if any bbcode styles match the elements styles + * + * @return string Content with any matching + * bbcode tags wrapped around it. + * @private + */ + handleStyles = function ($element, content, blockLevel) { + var styleValue, format, + getStyle = SCEditor.dom.getStyle; + + // convert blockLevel to boolean + blockLevel = !!blockLevel; + + if (!stylesToBBCodes[blockLevel]) { + return content; + } + + $.each(stylesToBBCodes[blockLevel], function (property, bbcodes) { + styleValue = getStyle($element[0], property); + + // if the parent has the same style use that instead of this one + // so you don't end up with [i]parent[i]child[/i][/i] + if (!styleValue || + getStyle($element.parent()[0], property) === styleValue) { + return; + } + + $.each(bbcodes, function (bbcode, values) { + if (!values || + $.inArray(styleValue.toString(), values) > -1) { + format = base.bbcodes[bbcode].format; + + if ($.isFunction(format)) { + content = format.call(base, $element, content); + } else { + content = _formatString(format, content); + } + } + }); + }); + + return content; + }; + + /** + * Handles a HTML tag and finds any matching bbcodes + * + * @param {jQuery} $element The element to convert + * @param {String} content The Tags text content + * @param {Bool} blockLevel If to convert block level tags + * @return {String} Content with any matching bbcode tags + * wrapped around it. + * @private + */ + handleTags = function ($element, content, blockLevel) { + var convertBBCode, format, + element = $element[0], + tag = element.nodeName.toLowerCase(); + + // convert blockLevel to boolean + blockLevel = !!blockLevel; + + if (tagsToBBCodes[tag] && tagsToBBCodes[tag][blockLevel]) { + // loop all bbcodes for this tag + $.each(tagsToBBCodes[tag][blockLevel], function ( + bbcode, bbcodeAttribs) { + // if the bbcode requires any attributes then check this has + // all needed + if (bbcodeAttribs) { + convertBBCode = false; + + // loop all the bbcode attribs + $.each(bbcodeAttribs, function (attrib, values) { + // Skip if the element doesn't have the attibue or + // the attribute doesn't match one of the require + // values + if (!$element.attr(attrib) || (values && + $.inArray($element.attr(attrib), values) < 0)) { + return; + } + + // break this loop as we have matched this bbcode + convertBBCode = true; + return false; + }); + + if (!convertBBCode) { + return; + } + } + + format = base.bbcodes[bbcode].format; + + if ($.isFunction(format)) { + content = format.call(base, $element, content); + } else { + content = _formatString(format, content); + } + }); + } + + var isInline = SCEditor.dom.isInline; + if (blockLevel && (!isInline(element, true) || tag === 'br')) { + var isLastBlockChild, parent, parentLastChild, + previousSibling = element.previousSibling; + + // Skips selection makers and ignored elements + // Skip empty inline elements + while (previousSibling && + previousSibling.nodeType === 1 && + !$(previousSibling).is('br') && + isInline(previousSibling, true) && + !previousSibling.firstChild) { + previousSibling = previousSibling.previousSibling; + } + + // If it's the last block of an inline that is the last + // child of a block then it shouldn't cause a line break + // except in IE < 11 + //
    + do { + parent = element.parentNode; + parentLastChild = parent.lastChild; + + isLastBlockChild = parentLastChild === element; + element = parent; + } while (parent && isLastBlockChild && isInline(parent, true)); + + // If this block is: + // * Not the last child of a block level element + // * Is a
  • tag (lists are blocks) + // * Is IE < 11 and the tag is BR. IE < 11 never collapses BR + // tags. + if (!isLastBlockChild || tag === 'li' || + (tag === 'br' && IE_BR_FIX)) { + content += '\n'; + } + + // Check for: + // texttext + // + // The second opening opening tag should cause a + // line break because the previous sibing is inline. + if (tag !== 'br' && previousSibling && + !$(previousSibling).is('br') && + isInline(previousSibling, true)) { + content = '\n' + content; + } + } + + return content; + }; + + /** + * Converts HTML to BBCode + * + * @param {String} html Html string, this function ignores this, + * it works off domBody + * @param {jQuery} $body Editors dom body object to convert + * @return {String} BBCode which has been converted from HTML + * @memberOf jQuery.plugins.bbcode.prototype + */ + base.signalToSource = function (html, $body) { + var $tmpContainer, bbcode, + parser = new BBCodeParser(base.opts.parserOptions); + + if (!$body) { + if (typeof html === 'string') { + $tmpContainer = $('
    ') + .css('visibility', 'hidden') + .appendTo(document.body) + .html(html); + + $body = $tmpContainer; + } else { + $body = $(html); + } + } + + if (!$body || !$body.jquery) { + return ''; + } + + SCEditor.dom.removeWhiteSpace($body[0]); + + // Remove all the stuff that is meant to be ignored + $('.sceditor-ignore', $body).remove(); + + bbcode = base.elementToBbcode($body); + + if ($tmpContainer) { + $tmpContainer.remove(); + } + + bbcode = parser.toBBCode(bbcode, true); + + if (base.opts.bbcodeTrim) { + bbcode = $.trim(bbcode); + } + + return bbcode; + }; + + /** + * Converts a HTML dom element to BBCode starting from + * the innermost element and working backwards + * + * @private + * @param {jQuery} $element The element to convert to BBCode + * @return {string} BBCode + * @memberOf jQuery.plugins.bbcode.prototype + */ + base.elementToBbcode = function ($element) { + var toBBCode = function (node, vChildren) { + var ret = ''; +// TODO: Move to BBCode class? + SCEditor.dom.traverse(node, function (node) { + var $node = $(node), + curTag = '', + nodeType = node.nodeType, + tag = node.nodeName.toLowerCase(), + vChild = validChildren[tag], + firstChild = node.firstChild, + isValidChild = true; + + if (typeof vChildren === 'object') { + isValidChild = $.inArray(tag, vChildren) > -1; + + // Emoticons should always be converted + if ($node.is('img') && + $node.data('sceditor-emoticon')) { + isValidChild = true; + } + + // if this tag is one of the parents allowed children + // then set this tags allowed children to whatever it + // allows, otherwise set to what the parent allows + if (!isValidChild) { + vChild = vChildren; + } + } + + // 3 = text and 1 = element + if (nodeType !== 3 && nodeType !== 1) { + return; + } + + if (nodeType === 1) { + // skip empty nlf elements (new lines automatically + // added after block level elements like quotes) + if ($node.hasClass('sceditor-nlf')) { + if (!firstChild || (!IE_BR_FIX && + node.childNodes.length === 1 && + /br/i.test(firstChild.nodeName))) { + return; + } + } + + // don't loop inside iframes + if (tag !== 'iframe') { + curTag = toBBCode(node, vChild); + } + +// TODO: isValidChild is no longer needed. Should use valid children bbcodes +// instead by creating BBCode tokens like the parser. + if (isValidChild) { + // code tags should skip most styles + if (tag !== 'code') { + // handle inline bbcodes + curTag = handleStyles($node, curTag); + curTag = handleTags($node, curTag); + + // handle blocklevel bbcodes + curTag = handleStyles($node, curTag, true); + } + + ret += handleTags($node, curTag, true); + } else { + ret += curTag; + } + } else { + ret += node.nodeValue; + } + }, false, true); + + return ret; + }; + + return toBBCode($element[0]); + }; + + /** + * Converts BBCode to HTML + * + * @param {String} text + * @param {Bool} asFragment + * @return {String} HTML + * @memberOf jQuery.plugins.bbcode.prototype + */ + base.signalToWysiwyg = function (text, asFragment) { + var parser = new BBCodeParser(base.opts.parserOptions), + html = parser.toHTML(base.opts.bbcodeTrim ? + $.trim(text) : text); + + return asFragment ? removeFirstLastDiv(html) : html; + }; + + /** + * Removes the first and last divs from the HTML. + * + * This is needed for pasting + * @param {String} html + * @return {String} + * @private + */ + removeFirstLastDiv = function (html) { + var node, next, removeDiv, + $output = $('
    ').hide().appendTo(document.body), + output = $output[0]; + + removeDiv = function (node, isFirst) { + // Don't remove divs that have styling + if (SCEditor.dom.hasStyling(node)) { + return; + } + + if (IE_BR_FIX || (node.childNodes.length !== 1 || + !$(node.firstChild).is('br'))) { + while ((next = node.firstChild)) { + output.insertBefore(next, node); + } + } + + if (isFirst) { + var lastChild = output.lastChild; + + if (node !== lastChild && $(lastChild).is('div') && + node.nextSibling === lastChild) { + output.insertBefore(document.createElement('br'), node); + } + } + + output.removeChild(node); + }; + + output.innerHTML = html.replace(/<\/div>\n/g, '
    '); + + if ((node = output.firstChild) && $(node).is('div')) { + removeDiv(node, true); + } + + if ((node = output.lastChild) && $(node).is('div')) { + removeDiv(node); + } + + output = output.innerHTML; + $output.remove(); + + return output; + }; + }; + + + + /** + * Formats a string replacing {name} with the values of + * obj.name properties. + * + * If there is no property for the specified {name} then + * it will be left intact. + * + * @param {String} str + * @param {Object} obj + * @return {String} + * @since 1.4.5 + */ + sceditorPlugins.bbcode.formatBBCodeString = function (str, obj) { + return str.replace(/\{([^}]+)\}/g, function (match, group) { + var undef, + escape = true; + + if (group.charAt(0) === '!') { + escape = false; + group = group.substring(1); + } + + if (group === '0') { + escape = false; + } + + if (obj[group] === undef) { + return match; + } + + return escape ? + escapeEntities(obj[group], true) : + obj[group]; + }); + }; + + /** + * Converts a number 0-255 to hex. + * + * Will return 00 if number is not a valid number. + * + * @param {Number} number + * @return {String} + * @private + */ + var toHex = function (number) { + number = parseInt(number, 10); + + if (isNaN(number)) { + return '00'; + } + + number = Math.max(0, Math.min(number, 255)).toString(16); + + return number.length < 2 ? '0' + number : number; + }; + + var _normaliseColour = function (colorStr) { + var match; + + colorStr = colorStr || '#000'; + + // rgb(n,n,n); + if ((match = + colorStr.match(/rgb\((\d{1,3}),\s*?(\d{1,3}),\s*?(\d{1,3})\)/i))) { + return '#' + + toHex(match[1]) + + toHex(match[2] - 0) + + toHex(match[3] - 0); + } + + // expand shorthand + if ((match = colorStr.match(/#([0-f])([0-f])([0-f])\s*?$/i))) { + return '#' + + match[1] + match[1] + + match[2] + match[2] + + match[3] + match[3]; + } + + return colorStr; + }; + + var bbcodes = { + // START_COMMAND: Bold + b: { + tags: { + b: null, + strong: null + }, + styles: { + // 401 is for FF 3.5 + 'font-weight': ['bold', 'bolder', '401', '700', '800', '900'] + }, + format: '[b]{0}[/b]', + html: '{0}' + }, + // END_COMMAND + + // START_COMMAND: Italic + i: { + tags: { + i: null, + em: null + }, + styles: { + 'font-style': ['italic', 'oblique'] + }, + format: '[i]{0}[/i]', + html: '{0}' + }, + // END_COMMAND + + // START_COMMAND: Underline + u: { + tags: { + u: null + }, + styles: { + 'text-decoration': ['underline'] + }, + format: '[u]{0}[/u]', + html: '{0}' + }, + // END_COMMAND + + // START_COMMAND: Strikethrough + s: { + tags: { + s: null, + strike: null + }, + styles: { + 'text-decoration': ['line-through'] + }, + format: '[s]{0}[/s]', + html: '{0}' + }, + // END_COMMAND + + // START_COMMAND: Subscript + sub: { + tags: { + sub: null + }, + format: '[sub]{0}[/sub]', + html: '{0}' + }, + // END_COMMAND + + // START_COMMAND: Superscript + sup: { + tags: { + sup: null + }, + format: '[sup]{0}[/sup]', + html: '{0}' + }, + // END_COMMAND + + // START_COMMAND: Font + font: { + tags: { + font: { + face: null + } + }, + styles: { + 'font-family': null + }, + quoteType: BBCodeParser.QuoteType.never, + format: function ($element, content) { + var font; + + if (!$element.is('font') || !(font = $element.attr('face'))) { + font = $element.css('font-family'); + } + + return '[font=' + _stripQuotes(font) + ']' + + content + '[/font]'; + }, + html: '{0}' + }, + // END_COMMAND + + // START_COMMAND: Size + size: { + tags: { + font: { + size: null + } + }, + styles: { + 'font-size': null + }, + format: function (element, content) { + var fontSize = element.attr('size'), + size = 2; + + if (!fontSize) { + fontSize = element.css('fontSize'); + } + + // Most browsers return px value but IE returns 1-7 + if (fontSize.indexOf('px') > -1) { + // convert size to an int + fontSize = fontSize.replace('px', '') - 0; + + if (fontSize < 12) { + size = 1; + } + if (fontSize > 15) { + size = 3; + } + if (fontSize > 17) { + size = 4; + } + if (fontSize > 23) { + size = 5; + } + if (fontSize > 31) { + size = 6; + } + if (fontSize > 47) { + size = 7; + } + } else { + size = fontSize; + } + + return '[size=' + size + ']' + content + '[/size]'; + }, + html: '{!0}' + }, + // END_COMMAND + + // START_COMMAND: Color + color: { + tags: { + font: { + color: null + } + }, + styles: { + color: null + }, + quoteType: BBCodeParser.QuoteType.never, + format: function ($element, content) { + var color; + + if (!$element.is('font') || !(color = $element.attr('color'))) { + color = $element[0].style.color || $element.css('color'); + } + + return '[color=' + _normaliseColour(color) + ']' + + content + '[/color]'; + }, + html: function (token, attrs, content) { + return '' + content + ''; + } + }, + // END_COMMAND + + // START_COMMAND: Lists + ul: { + tags: { + ul: null + }, + breakStart: true, + isInline: false, + skipLastLineBreak: true, + format: '[ul]{0}[/ul]', + html: '
      {0}
    ' + }, + list: { + breakStart: true, + isInline: false, + skipLastLineBreak: true, + html: '
      {0}
    ' + }, + ol: { + tags: { + ol: null + }, + breakStart: true, + isInline: false, + skipLastLineBreak: true, + format: '[ol]{0}[/ol]', + html: '
      {0}
    ' + }, + li: { + tags: { + li: null + }, + isInline: false, + closedBy: ['/ul', '/ol', '/list', '*', 'li'], + format: '[li]{0}[/li]', + html: '
  • {0}
  • ' + }, + '*': { + isInline: false, + closedBy: ['/ul', '/ol', '/list', '*', 'li'], + html: '
  • {0}
  • ' + }, + // END_COMMAND + + // START_COMMAND: Table + table: { + tags: { + table: null + }, + isInline: false, + isHtmlInline: true, + skipLastLineBreak: true, + format: '[table]{0}[/table]', + html: '{0}
    ' + }, + tr: { + tags: { + tr: null + }, + isInline: false, + skipLastLineBreak: true, + format: '[tr]{0}[/tr]', + html: '{0}' + }, + th: { + tags: { + th: null + }, + allowsEmpty: true, + isInline: false, + format: '[th]{0}[/th]', + html: '{0}' + }, + td: { + tags: { + td: null + }, + allowsEmpty: true, + isInline: false, + format: '[td]{0}[/td]', + html: '{0}' + }, + // END_COMMAND + + // START_COMMAND: Emoticons + emoticon: { + allowsEmpty: true, + tags: { + img: { + src: null, + 'data-sceditor-emoticon': null + } + }, + format: function ($elm, content) { + return $elm.data('sceditor-emoticon') + content; + }, + html: '{0}' + }, + // END_COMMAND + + // START_COMMAND: Horizontal Rule + hr: { + tags: { + hr: null + }, + allowsEmpty: true, + isSelfClosing: true, + isInline: false, + format: '[hr]{0}', + html: '
    ' + }, + // END_COMMAND + + // START_COMMAND: Image + img: { + allowsEmpty: true, + tags: { + img: { + src: null + } + }, + allowedChildren: ['#'], + quoteType: BBCodeParser.QuoteType.never, + format: function ($element, content) { + var width, height, + attribs = '', + element = $element[0], + style = function (name) { + return element.style ? element.style[name] : null; + }; + + // check if this is an emoticon image + if ($element.attr('data-sceditor-emoticon')) { + return content; + } + + width = $element.attr('width') || style('width'); + height = $element.attr('height') || style('height'); + + // only add width and height if one is specified + if ((element.complete && (width || height)) || + (width && height)) { + attribs = '=' + $element.width() + 'x' + $element.height(); + } + + return '[img' + attribs + ']' + $element.attr('src') + '[/img]'; + }, + html: function (token, attrs, content) { + var undef, width, height, match, + attribs = ''; + + // handle [img width=340 height=240]url[/img] + width = attrs.width; + height = attrs.height; + + // handle [img=340x240]url[/img] + if (attrs.defaultattr) { + match = attrs.defaultattr.split(/x/i); + + width = match[0]; + height = (match.length === 2 ? match[1] : match[0]); + } + + if (width !== undef) { + attribs += ' width="' + escapeEntities(width, true) + '"'; + } + + if (height !== undef) { + attribs += ' height="' + escapeEntities(height, true) + '"'; + } + + return ''; + } + }, + // END_COMMAND + + // START_COMMAND: URL + url: { + allowsEmpty: true, + tags: { + a: { + href: null + } + }, + quoteType: BBCodeParser.QuoteType.never, + format: function (element, content) { + var url = element.attr('href'); + + // make sure this link is not an e-mail, + // if it is return e-mail BBCode + if (url.substr(0, 7) === 'mailto:') { + return '[email="' + url.substr(7) + '"]' + + content + '[/email]'; + } + + return '[url=' + url + ']' + content + '[/url]'; + }, + html: function (token, attrs, content) { + attrs.defaultattr = + escapeEntities(attrs.defaultattr, true) || content; + + return '' + content + ''; + } + }, + // END_COMMAND + + // START_COMMAND: E-mail + email: { + quoteType: BBCodeParser.QuoteType.never, + html: function (token, attrs, content) { + return '' + content + ''; + } + }, + // END_COMMAND + + // START_COMMAND: Quote + quote: { + tags: { + blockquote: null + }, + isInline: false, + quoteType: BBCodeParser.QuoteType.never, + format: function (element, content) { + var author = ''; + var $elm = $(element); + var $cite = $elm.children('cite').first(); + + if ($cite.length === 1 || $elm.data('author')) { + author = $cite.text() || $elm.data('author'); + + $elm.data('author', author); + $cite.remove(); + + content = this.elementToBbcode($(element)); + author = '=' + author.replace(/(^\s+|\s+$)/g, ''); + + $elm.prepend($cite); + } + + return '[quote' + author + ']' + content + '[/quote]'; + }, + html: function (token, attrs, content) { + if (attrs.defaultattr) { + content = '' + escapeEntities(attrs.defaultattr) + + '' + content; + } + + return '
    ' + content + '
    '; + } + }, + // END_COMMAND + + // START_COMMAND: Code + code: { + tags: { + code: null + }, + isInline: false, + allowedChildren: ['#', '#newline'], + format: '[code]{0}[/code]', + html: '{0}' + }, + // END_COMMAND + + + // START_COMMAND: Left + left: { + styles: { + 'text-align': [ + 'left', + '-webkit-left', + '-moz-left', + '-khtml-left' + ] + }, + isInline: false, + format: '[left]{0}[/left]', + html: '
    {0}
    ' + }, + // END_COMMAND + + // START_COMMAND: Centre + center: { + styles: { + 'text-align': [ + 'center', + '-webkit-center', + '-moz-center', + '-khtml-center' + ] + }, + isInline: false, + format: '[center]{0}[/center]', + html: '
    {0}
    ' + }, + // END_COMMAND + + // START_COMMAND: Right + right: { + styles: { + 'text-align': [ + 'right', + '-webkit-right', + '-moz-right', + '-khtml-right' + ] + }, + isInline: false, + format: '[right]{0}[/right]', + html: '
    {0}
    ' + }, + // END_COMMAND + + // START_COMMAND: Justify + justify: { + styles: { + 'text-align': [ + 'justify', + '-webkit-justify', + '-moz-justify', + '-khtml-justify' + ] + }, + isInline: false, + format: '[justify]{0}[/justify]', + html: '
    {0}
    ' + }, + // END_COMMAND + + // START_COMMAND: YouTube + youtube: { + allowsEmpty: true, + tags: { + iframe: { + 'data-youtube-id': null + } + }, + format: function (element, content) { + element = element.attr('data-youtube-id'); + + return element ? '[youtube]' + element + '[/youtube]' : content; + }, + html: '' + }, + // END_COMMAND + + + // START_COMMAND: Rtl + rtl: { + styles: { + direction: ['rtl'] + }, + format: '[rtl]{0}[/rtl]', + html: '
    {0}
    ' + }, + // END_COMMAND + + // START_COMMAND: Ltr + ltr: { + styles: { + direction: ['ltr'] + }, + format: '[ltr]{0}[/ltr]', + html: '
    {0}
    ' + }, + // 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: {} + }; + + /** + * Static BBCode helper class + * @class command + * @name jQuery.plugins.bbcode.bbcode + */ + sceditorPlugins.bbcode.bbcode = + /** @lends jQuery.plugins.bbcode.bbcode */ + { + /** + * Gets a BBCode + * + * @param {String} name + * @return {Object|null} + * @since v1.3.5 + */ + get: function (name) { + return bbcodes[name] || null; + }, + + /** + *

    Adds a BBCode to the parser or updates an existing + * BBCode if a BBCode with the specified name already exists.

    + * + * @param {String} name + * @param {Object} bbcode + * @return {this|false} Returns false if name or bbcode is false + * @since v1.3.5 + */ + set: function (name, bbcode) { + if (!name || !bbcode) { + return false; + } + + // merge any existing command properties + bbcode = $.extend(bbcodes[name] || {}, bbcode); + + bbcode.remove = function () { + delete bbcodes[name]; + }; + + bbcodes[name] = bbcode; + + return this; + }, + + /** + * Renames a BBCode + * + * This does not change the format or HTML handling, those must be + * changed manually. + * + * @param {String} name [description] + * @param {String} newName [description] + * @return {this|false} + * @since v1.4.0 + */ + rename: function (name, newName) { + if (name in bbcodes) { + bbcodes[newName] = bbcodes[name]; + + delete bbcodes[name]; + } else { + return false; + } + + return this; + }, + + /** + * Removes a BBCode + * + * @param {String} name + * @return {this} + * @since v1.3.5 + */ + remove: function (name) { + if (name in bbcodes) { + delete bbcodes[name]; + } + + return this; + } + }; + + /** + * Deprecated, use plugins: option instead. I.e.: + * + * $('textarea').sceditor({ + * plugins: 'bbcode' + * }); + * + * @deprecated + */ + $.fn.sceditorBBCodePlugin = function (options) { + options = options || {}; + + if ($.isPlainObject(options)) { + options.plugins = (options.plugins || '') + 'bbcode'; + } + + return this.sceditor(options); + }; + + /** + * Converts CSS RGB and hex shorthand into hex + * + * @since v1.4.0 + * @param {String} colorStr + * @return {String} + * @deprecated + */ + sceditorPlugins.bbcode.normaliseColour = _normaliseColour; + sceditorPlugins.bbcode.formatString = _formatString; + sceditorPlugins.bbcode.stripQuotes = _stripQuotes; + sceditorPlugins.bbcode.bbcodes = bbcodes; + SCEditor.BBCodeParser = BBCodeParser; +})(jQuery, window, document); diff --git a/js/sceditor/development/plugins/format.js b/js/sceditor/development/plugins/format.js new file mode 100644 index 00000000..15129957 --- /dev/null +++ b/js/sceditor/development/plugins/format.js @@ -0,0 +1,127 @@ +/** + * SCEditor Paragraph Formatting Plugin + * http://www.sceditor.com/ + * + * Copyright (C) 2011-2013, Sam Clarke (samclarke.com) + * + * SCEditor is licensed under the MIT license: + * http://www.opensource.org/licenses/mit-license.php + * + * @fileoverview SCEditor Paragraph Formatting Plugin + * @author Sam Clarke + * @requires jQuery + */ +(function ($) { + 'use strict'; + + $.sceditor.plugins.format = function () { + var base = this; + + /** + * Default tags + * @type {Object} + * @private + */ + var tags = { + p: 'Paragraph', + h1: 'Heading 1', + h2: 'Heading 2', + h3: 'Heading 3', + h4: 'Heading 4', + h5: 'Heading 5', + h6: 'Heading 6', + address: 'Address', + pre: 'Preformatted Text' + }; + + /** + * Private functions + * @private + */ + var insertTag, + formatCmd; + + + base.init = function () { + var opts = this.opts, + pOpts = opts.paragraphformat; + + // Don't enable if the BBCode plugin is enabled. + if (opts.plugins && opts.plugins.indexOf('bbcode') > -1) { + return; + } + + if (pOpts) { + if (pOpts.tags) { + tags = pOpts.tags; + } + + if (pOpts.excludeTags) { + $.each(pOpts.excludeTags, function (idx, val) { + delete tags[val]; + }); + } + } + + if (!this.commands.format) { + this.commands.format = { + exec: formatCmd, + txtExec: formatCmd, + tooltip: 'Format Paragraph' + }; + } + + if (opts.toolbar === $.sceditor.defaultOptions.toolbar) { + opts.toolbar = opts.toolbar.replace(',color,', + ',color,format,'); + } + }; + + /** + * Inserts the specified tag into the editor + * + * @param {sceditor} editor + * @param {string} tag + * @private + */ + insertTag = function (editor, tag) { + if (editor.sourceMode()) { + editor.insert('<' + tag + '>', ''); + } else { + editor.execCommand('formatblock', '<' + tag + '>'); + } + + }; + + /** + * Function for the exec and txtExec properties + * + * @param {node} caller + * @private + */ + formatCmd = function (caller) { + var editor = this, + $content = $('
    '); + + $.each(tags, function (tag, val) { + $( + '' + + (val.name || val) + '' + ).click(function () { + editor.closeDropDown(true); + + if (val.exec) { + val.exec(editor); + } else { + insertTag(editor, tag); + } + + return false; + }) + .appendTo($content); + }); + + editor.createDropDown(caller, 'format', $content); + }; + }; +})(jQuery); diff --git a/js/sceditor/development/plugins/undo.js b/js/sceditor/development/plugins/undo.js new file mode 100644 index 00000000..7abedbf4 --- /dev/null +++ b/js/sceditor/development/plugins/undo.js @@ -0,0 +1,187 @@ +(function ($) { + 'use strict'; + + $.sceditor.plugins.undo = function () { + var base = this; + var editor; + var charChangedCount = 0; + var previousValue; + + var undoLimit = 50; + var redoStates = []; + var undoStates = []; + var ignoreNextValueChanged = false; + + /** + * Sets the editor to the specified state. + * + * @param {Object} state + * @private + */ + var applyState = function (state) { + ignoreNextValueChanged = true; + + previousValue = state.value; + + editor.sourceMode(state.sourceMode); + editor.val(state.value, false); + editor.focus(); + + if (state.sourceMode) { + editor.sourceEditorCaret(state.caret); + } else { + editor.getRangeHelper().restoreRange(); + } + + ignoreNextValueChanged = false; + }; + + + /** + * Caluclates the number of characters that have changed + * between two strings. + * + * @param {String} strA + * @param {String} strB + * @return {String} + * @private + */ + var simpleDiff = function (strA, strB) { + var start, end, aLenDiff, bLenDiff, + aLength = strA.length, + bLength = strB.length, + length = Math.max(aLength, bLength); + + // Calculate the start + for (start = 0; start < length; start++) { + if (strA.charAt(start) !== strB.charAt(start)) { + break; + } + } + + // Calculate the end + aLenDiff = aLength < bLength ? bLength - aLength : 0; + bLenDiff = bLength < aLength ? aLength - bLength : 0; + + for (end = length - 1; end >= 0; end--) { + if (strA.charAt(end - aLenDiff) !== + strB.charAt(end - bLenDiff)) { + break; + } + } + + return (end - start) + 1; + }; + + base.init = function () { + // The this variable will be set to the instance of the editor + // calling it, hence why the plugins "this" is saved to the base + // variable. + editor = this; + + undoLimit = editor.undoLimit || undoLimit; + + // addShortcut is the easiest way to add handlers to specific + // shortcuts + editor.addShortcut('ctrl+z', base.undo); + editor.addShortcut('ctrl+shift+z', base.redo); + editor.addShortcut('ctrl+y', base.redo); + }; + + base.undo = function () { + var state = undoStates.pop(); + var rawEditorValue = editor.val(null, false); + + if (state && !redoStates.length && rawEditorValue === state.value) { + state = undoStates.pop(); + } + + if (state) { + if (!redoStates.length) { + redoStates.push({ + 'caret': editor.sourceEditorCaret(), + 'sourceMode': editor.sourceMode(), + 'value': rawEditorValue + }); + } + + redoStates.push(state); + applyState(state); + } + + return false; + }; + + base.redo = function () { + var state = redoStates.pop(); + + if (!undoStates.length) { + undoStates.push(state); + state = redoStates.pop(); + } + + if (state) { + undoStates.push(state); + applyState(state); + } + + return false; + }; + + base.signalReady = function () { + var rawValue = editor.val(null, false); + + // Store the initial value as the last value + previousValue = rawValue; + + undoStates.push({ + 'caret': this.sourceEditorCaret(), + 'sourceMode': this.sourceMode(), + 'value': rawValue + }); + }; + + /** + * Handle the valueChanged signal. + * + * e.rawValue will either be the raw HTML from the WYSIWYG editor with + * the rangeHelper range markers inserted, or it will be the raw value + * of the source editor (BBCode or HTML depening on plugins). + * @return {void} + */ + base.signalValuechangedEvent = function (e) { + var rawValue = e.rawValue; + + if (undoLimit > 0 && undoStates.length > undoLimit) { + undoStates.shift(); + } + + // If the editor hasn't fully loaded yet, + // then the previous value won't be set. + if (ignoreNextValueChanged || !previousValue || + previousValue === rawValue) { + return; + } + + // Value has changed so remove all redo states + redoStates.length = 0; + charChangedCount += simpleDiff(previousValue, rawValue); + + if (charChangedCount < 20) { + return; + // ?? + } else if (charChangedCount < 50 && !/\s$/g.test(e.rawValue)) { + return; + } + + undoStates.push({ + 'caret': editor.sourceEditorCaret(), + 'sourceMode': editor.sourceMode(), + 'value': rawValue + }); + + charChangedCount = 0; + previousValue = rawValue; + }; + }; +}(jQuery)); diff --git a/js/sceditor/development/plugins/xhtml.js b/js/sceditor/development/plugins/xhtml.js new file mode 100644 index 00000000..7ba03e5f --- /dev/null +++ b/js/sceditor/development/plugins/xhtml.js @@ -0,0 +1,1319 @@ +/** + * SCEditor XHTML Plugin + * http://www.sceditor.com/ + * + * Copyright (C) 2011-2013, Sam Clarke (samclarke.com) + * + * SCEditor is licensed under the MIT license: + * http://www.opensource.org/licenses/mit-license.php + * + * @author Sam Clarke + * @requires jQuery + */ +/*global prompt: true*/ +(function ($) { + 'use strict'; + + var SCEditor = $.sceditor; + var sceditorPlugins = SCEditor.plugins; + var dom = SCEditor.dom; + + var defaultCommandsOverrides = { + bold: { + txtExec: [ + '', + '' + ] + }, + italic: { + txtExec: [ + '', + '' + ] + }, + underline: { + txtExec: [ + '', + '' + ] + }, + strike: { + txtExec: [ + '', + '' + ] + }, + subscript: { + txtExec: [ + '', + '' + ] + }, + superscript: { + txtExec: [ + '', + '' + ] + }, + left: { + txtExec: [ + '
    ', + '
    ' + ] + }, + center: { + txtExec: [ + '
    ', + '
    ' + ] + }, + right: { + txtExec: [ + '
    ', + '
    ' + ] + }, + justify: { + txtExec: [ + '
    ', + '
    ' + ] + }, + font: { + txtExec: function (caller) { + var editor = this; + + SCEditor.command.get('font')._dropDown( + editor, + caller, + function (fontName) { + editor.insertText('', ''); + } + ); + } + }, + size: { + txtExec: function (caller) { + var editor = this; + + SCEditor.command.get('size')._dropDown( + editor, + caller, + function (fontSize) { + editor.insertText('', ''); + } + ); + } + }, + color: { + txtExec: function (caller) { + var editor = this; + + SCEditor.command.get('color')._dropDown( + editor, + caller, + function (color) { + editor.insertText('', ''); + } + ); + } + }, + bulletlist: { + txtExec: [ + '
    • ', + '
    ' + ] + }, + orderedlist: { + txtExec: [ + '
    1. ', + '
    ' + ] + }, + table: { + txtExec: [ + '
    ', + '
    ' + ] + }, + horizontalrule: { + txtExec: [ + '
    ' + ] + }, + code: { + txtExec: [ + '', + '' + ] + }, + image: { + txtExec: function (caller, selected) { + var url = prompt(this._('Enter the image URL:'), selected); + + if (url) { + this.insertText(''); + } + } + }, + email: { + txtExec: function (caller, sel) { + var email, text, + display = sel && sel.indexOf('@') > -1 ? null : sel; + + email = prompt( + this._('Enter the e-mail address:'), + (display ? '' : sel) + ); + + text = prompt( + this._('Enter the displayed text:'), + display || email + ) || email; + + if (email) { + this.insertText( + '' + text + '' + ); + } + } + }, + link: { + txtExec: function (caller, sel) { + var display = sel && sel.indexOf('http://') > -1 ? null : sel, + url = prompt(this._('Enter URL:'), + (display ? 'http://' : sel)), + text = prompt(this._('Enter the displayed text:'), + display || url) || url; + + if (url) { + this.insertText( + '' + text + '' + ); + } + } + }, + quote: { + txtExec: [ + '
    ', + '
    ' + ] + }, + youtube: { + txtExec: function (caller) { + var editor = this; + + SCEditor.command.get('youtube')._dropDown( + editor, + caller, + function (id) { + editor.insertText( + '' + ); + } + ); + } + }, + rtl: { + txtExec: [ + '
    ', + '
    ' + ] + }, + ltr: { + txtExec: [ + '
    ', + '
    ' + ] + } + }; + + /** + * XHTMLSerializer part of the XHTML plugin. + * + * @class XHTMLSerializer + * @name jQuery.sceditor.XHTMLSerializer + * @since v1.4.1 + */ + SCEditor.XHTMLSerializer = function () { + var base = this; + + var opts = { + indentStr: '\t' + }; + + /** + * Array containing the output, used as it's faster + * than string concatenation in slow browsers. + * @type {Array} + * @private + */ + var outputStringBuilder = []; + + /** + * Current indention level + * @type {Number} + * @private + */ + var currentIndent = 0; + + /** + * @private + */ + var escapeEntites, + trim, + serializeNode, + handleDoc, + handleElement, + handleCdata, + handleComment, + handleText, + output, + canIndent; +// TODO: use escape.entities + /** + * Escapes XHTML entities + * + * @param {String} str + * @return {String} + * @private + */ + escapeEntites = function (str) { + var entites = { + '&': '&', + '<': '<', + '>': '>', + '"': '"' + }; + + return !str ? '' : str.replace(/[&<>"]/g, function (entity) { + return entites[entity] || entity; + }); + }; + + /** + * @param {string} str + * @return {string} + * @private + */ + trim = function (str) { + return str + // New lines will be shown as spaces so just convert to spaces. + .replace(/[\r\n]/, ' ') + .replace(/[^\S|\u00A0]+/g, ' '); + }; + + /** + * Serializes a node to XHTML + * + * @param {Node} node Node to serialize + * @param {Boolean} onlyChildren If to only serialize the nodes + * children and not the node + * itself + * @return {String} The serialized node + * @name serialize + * @memberOf jQuery.sceditor.XHTMLSerializer.prototype + * @since v1.4.1 + */ + base.serialize = function (node, onlyChildren) { + outputStringBuilder = []; + + if (onlyChildren) { + node = node.firstChild; + + while (node) { + serializeNode(node); + node = node.nextSibling; + } + } else { + serializeNode(node); + } + + return outputStringBuilder.join(''); + }; + + /** + * Serializes a node to the outputStringBuilder + * + * @param {Node} node + * @return {Void} + * @private + */ + serializeNode = function (node, parentIsPre) { + switch (node.nodeType) { + case 1: // element + var tagName = node.nodeName.toLowerCase(); + + // IE comment + if (tagName === '!') { + handleComment(node); + } else { + handleElement(node, parentIsPre); + } + break; + + case 3: // text + handleText(node, parentIsPre); + break; + + case 4: // cdata section + handleCdata(node); + break; + + case 8: // comment + handleComment(node); + break; + + case 9: // document + case 11: // document fragment + handleDoc(node); + break; + + // Ignored types + case 2: // attribute + case 5: // entity ref + case 6: // entity + case 7: // processing instruction + case 10: // document type + case 12: // notation + break; + } + }; + + /** + * Handles doc node + * @param {Node} node + * @return {void} + * @private + */ + handleDoc = function (node) { + var child = node.firstChild; + + while (child) { + serializeNode(child); + child = child.nextSibling; + } + }; + + /** + * Handles element nodes + * @param {Node} node + * @return {void} + * @private + */ + handleElement = function (node, parentIsPre) { + var child, attr, attrValue, + tagName = node.nodeName.toLowerCase(), + isIframe = tagName === 'iframe', + attrIdx = node.attributes.length, + firstChild = node.firstChild, + // pre || pre-wrap with any vendor prefix + isPre = parentIsPre || + /pre(?:\-wrap)?$/i.test($(node).css('whiteSpace')), + selfClosing = !node.firstChild && !dom.canHaveChildren(node) && + !isIframe; + + if ($(node).hasClass('sceditor-ignore')) { + return; + } + + output('<' + tagName, !parentIsPre && canIndent(node)); + while (attrIdx--) { + attr = node.attributes[attrIdx]; + + // IE < 8 returns all possible attributes not just specified + // ones. IE < 8 also doesn't say value on input is specified + // so just assume it is. + if (!SCEditor.ie || attr.specified || + (tagName === 'input' && attr.name === 'value')) { + // IE < 8 doesn't return the CSS for the style attribute + if (SCEditor.ie < 8 && /style/i.test(attr.name)) { + attrValue = node.style.cssText; + } else { + attrValue = attr.value; + } + + output(' ' + attr.name.toLowerCase() + '="' + + escapeEntites(attrValue) + '"', false); + } + } + output(selfClosing ? ' />' : '>', false); + + if (!isIframe) { + child = firstChild; + } + + while (child) { + currentIndent++; + + serializeNode(child, isPre); + child = child.nextSibling; + + currentIndent--; + } + + if (!selfClosing) { + output( + '', + !isPre && !isIframe && canIndent(node) && + firstChild && canIndent(firstChild) + ); + } + }; + + /** + * Handles CDATA nodes + * @param {Node} node + * @return {void} + * @private + */ + handleCdata = function (node) { + output(''); + }; + + /** + * Handles comment nodes + * @param {Node} node + * @return {void} + * @private + */ + handleComment = function (node) { + output(''); + }; + + /** + * Handles text nodes + * @param {Node} node + * @return {void} + * @private + */ + handleText = function (node, parentIsPre) { + var text = node.nodeValue; + + if (!parentIsPre) { + text = trim(text); + } + + if (text) { + output(escapeEntites(text), !parentIsPre && canIndent(node)); + } + }; + + /** + * Adds a string to the outputStringBuilder. + * + * The string will be indented unless indent is set to boolean false. + * @param {String} str + * @param {Boolean} indent + * @return {void} + * @private + */ + output = function (str, indent) { + var i = currentIndent; + + if (indent !== false) { + // Don't add a new line if it's the first element + if (outputStringBuilder.length) { + outputStringBuilder.push('\n'); + } + + while (i--) { + outputStringBuilder.push(opts.indentStr); + } + } + + outputStringBuilder.push(str); + }; + + /** + * Checks if should indent the node or not + * @param {Node} node + * @return {boolean} + * @private + */ + canIndent = function (node) { + var prev = node.previousSibling; + + if (node.nodeType !== 1 && prev) { + return !dom.isInline(prev); + } + + // first child of a block element + if (!prev && !dom.isInline(node.parentNode)) { + return true; + } + + return !dom.isInline(node); + }; + }; + + /** + * SCEditor XHTML plugin + * @class xhtml + * @name jQuery.sceditor.plugins.xhtml + * @since v1.4.1 + */ + sceditorPlugins.xhtml = function () { + var base = this; + + /** + * Tag converstions cache + * @type {Object} + * @private + */ + var tagConvertersCache = {}; + + /** + * Attributes filter cache + * @type {Object} + * @private + */ + var attrsCache = {}; + + /** + * Private methods + * @private + */ + var convertTags, + convertNode, + isEmpty, + removeTags, + mergeAttribsFilters, + removeAttribs, + wrapInlines; + + + /** + * Init + * @return {void} + */ + base.init = function () { + if (!$.isEmptyObject(sceditorPlugins.xhtml.converters || {})) { + $.each( + sceditorPlugins.xhtml.converters, + function (idx, converter) { + $.each(converter.tags, function (tagname) { + if (!tagConvertersCache[tagname]) { + tagConvertersCache[tagname] = []; + } + + tagConvertersCache[tagname].push(converter); + }); + } + ); + } + + this.commands = $.extend(true, + {}, defaultCommandsOverrides, this.commands); + }; + + /** + * Converts the WYSIWYG content to XHTML + * @param {String} html + * @param {Node} domBody + * @return {String} + * @memberOf jQuery.sceditor.plugins.xhtml.prototype + */ + base.signalToSource = function (html, domBody) { + domBody = domBody.jquery ? domBody[0] : domBody; + + convertTags(domBody); + removeTags(domBody); + removeAttribs(domBody); + wrapInlines(domBody); + + return (new SCEditor.XHTMLSerializer()).serialize(domBody, true); + }; + + /** + * Converts the XHTML to WYSIWYG content. + * + * This doesn't currently do anything as XHTML + * is valid WYSIWYG content. + * @param {String} text + * @return {String} + * @memberOf jQuery.sceditor.plugins.xhtml.prototype + */ + base.signalToWysiwyg = function (text) { + return text; + }; + + /** + * Deprecated, use dom.convertElement() instead. + * @deprecated + */ + base.convertTagTo = dom.convertElement; + + /** + * Runs all converters for the specified tagName + * against the DOM node. + * @param {String} tagName + * @param {jQuery} $node + * @return {Node} node + * @private + */ + convertNode = function (tagName, $node, node) { + if (!tagConvertersCache[tagName]) { + return; + } + + $.each(tagConvertersCache[tagName], function (idx, converter) { + if (converter.tags[tagName]) { + $.each(converter.tags[tagName], function (attr, values) { + if (!node.getAttributeNode) { + return; + } + + attr = node.getAttributeNode(attr); + + // IE < 8 always returns an attribute regardless of if + // it has been specified so must check it. + if (!attr || (SCEditor.ie < 8 && !attr.specified)) { + return; + } + + if (values && $.inArray(attr.value, values) < 0) { + return; + } + + converter.conv.call(base, node, $node); + }); + } else if (converter.conv) { + converter.conv.call(base, node, $node); + } + }); + }; + + /** + * Converts any tags/attributes to their XHTML equivalents + * @param {Node} node + * @return {Void} + * @private + */ + convertTags = function (node) { + dom.traverse(node, function (node) { + var $node = $(node), + tagName = node.nodeName.toLowerCase(); + + convertNode('*', $node, node); + convertNode(tagName, $node, node); + }, true); + }; + + /** + * Tests if a node is empty and can be removed. + * + * @param {Node} node + * @return {Boolean} + * @private + */ + isEmpty = function (node, excludeBr) { + var childNodes = node.childNodes, + tagName = node.nodeName.toLowerCase(), + nodeValue = node.nodeValue, + childrenLength = childNodes.length; + + if (excludeBr && tagName === 'br') { + return true; + } + + if ($(node).hasClass('sceditor-ignore')) { + return true; + } + + if (!dom.canHaveChildren(node)) { + return false; + } + + // \S|\u00A0 = any non space char + if (nodeValue && /\S|\u00A0/.test(nodeValue)) { + return false; + } + + while (childrenLength--) { + if (!isEmpty(childNodes[childrenLength], + excludeBr && !node.previousSibling && !node.nextSibling)) { + return false; + } + } + + return true; + }; + + /** + * Removes any tags that are not white listed or if no + * tags are white listed it will remove any tags that + * are black listed. + * + * @param {Node} rootNode + * @return {Void} + * @private + */ + removeTags = function (rootNode) { + dom.traverse(rootNode, function (node) { + var remove, + tagName = node.nodeName.toLowerCase(), + parentNode = node.parentNode, + nodeType = node.nodeType, + isBlock = !dom.isInline(node), + previousSibling = node.previousSibling, + nextSibling = node.nextSibling, + isTopLevel = parentNode === rootNode, + noSiblings = !previousSibling && !nextSibling, + empty = tagName !== 'iframe' && isEmpty(node, + isTopLevel && noSiblings && tagName !== 'br'), + document = node.ownerDocument, + allowedtags = sceditorPlugins.xhtml.allowedTags, + disallowedTags = sceditorPlugins.xhtml.disallowedTags; + + // 3 = text node + if (nodeType === 3) { + return; + } + + if (nodeType === 4) { + tagName = '!cdata'; + } else if (tagName === '!' || nodeType === 8) { + tagName = '!comment'; + } + + if (empty) { + remove = true; + // 3 is text node which do not get filtered + } else if (allowedtags && allowedtags.length) { + remove = ($.inArray(tagName, allowedtags) < 0); + } else if (disallowedTags && disallowedTags.length) { + remove = ($.inArray(tagName, disallowedTags) > -1); + } + + if (remove) { + if (!empty) { + if (isBlock && previousSibling && + dom.isInline(previousSibling)) { + parentNode.insertBefore( + document.createTextNode(' '), node); + } + + // Insert all the childen after node + while (node.firstChild) { + parentNode.insertBefore(node.firstChild, + nextSibling); + } + + if (isBlock && nextSibling && + dom.isInline(nextSibling)) { + parentNode.insertBefore( + document.createTextNode(' '), nextSibling); + } + } + + parentNode.removeChild(node); + } + }, true); + }; + + /** + * Merges two sets of attribute filters into one + * + * @param {Object} filtersA + * @param {Object} filtersB + * @return {Object} + * @private + */ + mergeAttribsFilters = function (filtersA, filtersB) { + var ret = {}; + + if (filtersA) { + $.extend(ret, filtersA); + } + + if (!filtersB) { + return ret; + } + + $.each(filtersB, function (attrName, values) { + if ($.isArray(values)) { + ret[attrName] = $.merge(ret[attrName] || [], values); + } else if (!ret[attrName]) { + ret[attrName] = null; + } + }); + + return ret; + }; + + /** + * Wraps adjacent inline child nodes of root + * in paragraphs. + * + * @param {Node} root + * @private + */ + wrapInlines = function (root) { + var adjacentInlines = []; + var wrapAdjacents = function () { + if (adjacentInlines.length) { + $('

    ', root.ownerDocument) + .insertBefore(adjacentInlines[0]) + .append(adjacentInlines); + + adjacentInlines = []; + } + }; + + // Strip empty text nodes so they don't get wrapped. + dom.removeWhiteSpace(root); + + var node = root.firstChild; + while (node) { + if (dom.isInline(node) && !$(node).is('.sceditor-ignore')) { + adjacentInlines.push(node); + } else { + wrapAdjacents(); + } + + node = node.nextSibling; + } + + wrapAdjacents(); + }; + + /** + * Removes any attributes that are not white listed or + * if no attributes are white listed it will remove + * any attributes that are black listed. + * @param {Node} node + * @return {Void} + * @private + */ + removeAttribs = function (node) { + var tagName, attr, attrName, attrsLength, validValues, remove, + allowedAttribs = sceditorPlugins.xhtml.allowedAttribs, + isAllowed = allowedAttribs && + !$.isEmptyObject(allowedAttribs), + disallowedAttribs = sceditorPlugins.xhtml.disallowedAttribs, + isDisallowed = disallowedAttribs && + !$.isEmptyObject(disallowedAttribs); + + attrsCache = {}; + + dom.traverse(node, function (node) { + if (!node.attributes) { + return; + } + + tagName = node.nodeName.toLowerCase(); + attrsLength = node.attributes.length; + + if (attrsLength) { + if (!attrsCache[tagName]) { + if (isAllowed) { + attrsCache[tagName] = mergeAttribsFilters( + allowedAttribs['*'], + allowedAttribs[tagName] + ); + } else { + attrsCache[tagName] = mergeAttribsFilters( + disallowedAttribs['*'], + disallowedAttribs[tagName] + ); + } + } + + while (attrsLength--) { + attr = node.attributes[attrsLength]; + attrName = attr.name; + validValues = attrsCache[tagName][attrName]; + remove = false; + + if (isAllowed) { + remove = validValues !== null && + (!$.isArray(validValues) || + $.inArray(attr.value, validValues) < 0); + } else if (isDisallowed) { + remove = validValues === null || + ($.isArray(validValues) && + $.inArray(attr.value, validValues) > -1); + } + + if (remove) { + node.removeAttribute(attrName); + } + } + } + }); + }; + }; + + /** + * Tag conveters, a converter is applied to all + * tags that match the criteria. + * @type {Array} + * @name jQuery.sceditor.plugins.xhtml.converters + * @since v1.4.1 + */ + sceditorPlugins.xhtml.converters = [ + { + tags: { + '*': { + width: null + } + }, + conv: function (node, $node) { + $node.css('width', $node.attr('width')).removeAttr('width'); + } + }, + { + tags: { + '*': { + height: null + } + }, + conv: function (node, $node) { + $node.css('height', $node.attr('height')).removeAttr('height'); + } + }, + { + tags: { + 'li': { + value: null + } + }, + conv: function (node, $node) { + if (SCEditor.ie < 8) { + node.removeAttribute('value'); + } else { + $node.removeAttr('value'); + } + } + }, + { + tags: { + '*': { + text: null + } + }, + conv: function (node, $node) { + $node.css('color', $node.attr('text')).removeAttr('text'); + } + }, + { + tags: { + '*': { + color: null + } + }, + conv: function (node, $node) { + $node.css('color', $node.attr('color')).removeAttr('color'); + } + }, + { + tags: { + '*': { + face: null + } + }, + conv: function (node, $node) { + $node.css('fontFamily', $node.attr('face')).removeAttr('face'); + } + }, + { + tags: { + '*': { + align: null + } + }, + conv: function (node, $node) { + $node.css('textAlign', $node.attr('align')).removeAttr('align'); + } + }, + { + tags: { + '*': { + border: null + } + }, + conv: function (node, $node) { + $node + .css('borderWidth',$node.attr('border')) + .removeAttr('border'); + } + }, + { + tags: { + applet: { + name: null + }, + img: { + name: null + }, + layer: { + name: null + }, + map: { + name: null + }, + object: { + name: null + }, + param: { + name: null + } + }, + conv: function (node, $node) { + if (!$node.attr('id')) { + $node.attr('id', $node.attr('name')); + } + + $node.removeAttr('name'); + } + }, + { + tags: { + '*': { + vspace: null + } + }, + conv: function (node, $node) { + $node + .css('marginTop', $node.attr('vspace') - 0) + .css('marginBottom', $node.attr('vspace') - 0) + .removeAttr('vspace'); + } + }, + { + tags: { + '*': { + hspace: null + } + }, + conv: function (node, $node) { + $node + .css('marginLeft', $node.attr('hspace') - 0) + .css('marginRight', $node.attr('hspace') - 0) + .removeAttr('hspace'); + } + }, + { + tags: { + 'hr': { + noshade: null + } + }, + conv: function (node, $node) { + $node.css('borderStyle', 'solid').removeAttr('noshade'); + } + }, + { + tags: { + '*': { + nowrap: null + } + }, + conv: function (node, $node) { + $node.css('white-space', 'nowrap').removeAttr('nowrap'); + } + }, + { + tags: { + big: null + }, + conv: function (node) { + $(this.convertTagTo(node, 'span')).css('fontSize', 'larger'); + } + }, + { + tags: { + small: null + }, + conv: function (node) { + $(this.convertTagTo(node, 'span')).css('fontSize', 'smaller'); + } + }, + { + tags: { + b: null + }, + conv: function (node) { + $(this.convertTagTo(node, 'strong')); + } + }, + { + tags: { + u: null + }, + conv: function (node) { + $(this.convertTagTo(node, 'span')) + .css('textDecoration', 'underline'); + } + }, + { + tags: { + i: null + }, + conv: function (node) { + $(this.convertTagTo(node, 'em')); + } + }, + { + tags: { + s: null, + strike: null + }, + conv: function (node) { + $(this.convertTagTo(node, 'span')) + .css('textDecoration', 'line-through'); + } + }, + { + tags: { + dir: null + }, + conv: function (node) { + this.convertTagTo(node, 'ul'); + } + }, + { + tags: { + center: null + }, + conv: function (node) { + $(this.convertTagTo(node, 'div')) + .css('textAlign', 'center'); + } + }, + { + tags: { + font: { + size: null + } + }, + conv: function (node, $node) { + var size = $node.css('fontSize'), + fontSize = size; + + // IE < 8 sets a font tag with no size to +0 so + // should just skip it. + if (fontSize !== '+0') { + // IE 8 and below incorrectly returns the value of the size + // attribute instead of the px value so must convert it + if (SCEditor.ie < 9) { + fontSize = 10; + + if (size > 1) { + fontSize = 13; + } + if (size > 2) { + fontSize = 16; + } + if (size > 3) { + fontSize = 18; + } + if (size > 4) { + fontSize = 24; + } + if (size > 5) { + fontSize = 32; + } + if (size > 6) { + fontSize = 48; + } + } + + $node.css('fontSize', fontSize); + } + + $node.removeAttr('size'); + } + }, + { + tags: { + font: null + }, + conv: function (node) { + // All it's attributes will be converted + // by the attribute converters + this.convertTagTo(node, 'span'); + } + }, + { + tags: { + '*': { + type: ['_moz'] + } + }, + conv: function (node, $node) { + $node.removeAttr('type'); + } + }, + { + tags: { + '*': { + '_moz_dirty': null + } + }, + conv: function (node, $node) { + $node.removeAttr('_moz_dirty'); + } + }, + { + tags: { + '*': { + '_moz_editor_bogus_node': null + } + }, + conv: function (node, $node) { + $node.remove(); + } + } + ]; + + /** + * Allowed attributes map. + * + * To allow an attribute for all tags use * as the tag name. + * + * Leave empty or null to allow all attributes. (the disallow + * list will be used to filter them instead) + * @type {Object} + * @name jQuery.sceditor.plugins.xhtml.allowedAttribs + * @since v1.4.1 + */ + sceditorPlugins.xhtml.allowedAttribs = {}; + + /** + * Attributes that are not allowed. + * + * Only used if allowed attributes is null or empty. + * @type {Object} + * @name jQuery.sceditor.plugins.xhtml.disallowedAttribs + * @since v1.4.1 + */ + sceditorPlugins.xhtml.disallowedAttribs = {}; + + /** + * Array containing all the allowed tags. + * + * If null or empty all tags will be allowed. + * @type {Array} + * @name jQuery.sceditor.plugins.xhtml.allowedTags + * @since v1.4.1 + */ + sceditorPlugins.xhtml.allowedTags = []; + + /** + * Array containing all the disallowed tags. + * + * Only used if allowed tags is null or empty. + * @type {Array} + * @name jQuery.sceditor.plugins.xhtml.disallowedTags + * @since v1.4.1 + */ + sceditorPlugins.xhtml.disallowedTags = []; +}(jQuery)); diff --git a/js/sceditor/development/themes/default.css b/js/sceditor/development/themes/default.css new file mode 100644 index 00000000..b79d655d --- /dev/null +++ b/js/sceditor/development/themes/default.css @@ -0,0 +1,500 @@ +/*! SCEditor | (C) 2011-2013, Sam Clarke | sceditor.com/license */ +/** + * SCEditor + * http://www.ssceditor.com/ + * + * Copyright (C) 2011-12, Sam Clarke (samclarke.com) + * + * SCEditor is licensed under the MIT license: + * http://www.opensource.org/licenses/mit-license.php + */ +div.sceditor-grip, +.sceditor-button div { + background-image: url("famfamfam.png"); + background-repeat: no-repeat; + width: 16px; + height: 16px; +} +.sceditor-button-youtube div { + background-position: 0px 0px; +} +.sceditor-button-link div { + background-position: 0px -16px; +} +.sceditor-button-unlink div { + background-position: 0px -32px; +} +.sceditor-button-underline div { + background-position: 0px -48px; +} +.sceditor-button-time div { + background-position: 0px -64px; +} +.sceditor-button-table div { + background-position: 0px -80px; +} +.sceditor-button-superscript div { + background-position: 0px -96px; +} +.sceditor-button-subscript div { + background-position: 0px -112px; +} +.sceditor-button-strike div { + background-position: 0px -128px; +} +.sceditor-button-source div { + background-position: 0px -144px; +} +.sceditor-button-size div { + background-position: 0px -160px; +} +.sceditor-button-rtl div { + background-position: 0px -176px; +} +.sceditor-button-right div { + background-position: 0px -192px; +} +.sceditor-button-removeformat div { + background-position: 0px -208px; +} +.sceditor-button-quote div { + background-position: 0px -224px; +} +.sceditor-button-print div { + background-position: 0px -240px; +} +.sceditor-button-pastetext div { + background-position: 0px -256px; +} +.sceditor-button-paste div { + background-position: 0px -272px; +} +.sceditor-button-outdent div { + background-position: 0px -288px; +} +.sceditor-button-orderedlist div { + background-position: 0px -304px; +} +.sceditor-button-maximize div { + background-position: 0px -320px; +} +.sceditor-button-ltr div { + background-position: 0px -336px; +} +.sceditor-button-left div { + background-position: 0px -352px; +} +.sceditor-button-justify div { + background-position: 0px -368px; +} +.sceditor-button-italic div { + background-position: 0px -384px; +} +.sceditor-button-indent div { + background-position: 0px -400px; +} +.sceditor-button-image div { + background-position: 0px -416px; +} +.sceditor-button-horizontalrule div { + background-position: 0px -432px; +} +.sceditor-button-format div { + background-position: 0px -448px; +} +.sceditor-button-font div { + background-position: 0px -464px; +} +.sceditor-button-emoticon div { + background-position: 0px -480px; +} +.sceditor-button-email div { + background-position: 0px -496px; +} +.sceditor-button-date div { + background-position: 0px -512px; +} +.sceditor-button-cut div { + background-position: 0px -528px; +} +.sceditor-button-copy div { + background-position: 0px -544px; +} +.sceditor-button-color div { + background-position: 0px -560px; +} +.sceditor-button-code div { + background-position: 0px -576px; +} +.sceditor-button-center div { + background-position: 0px -592px; +} +.sceditor-button-bulletlist div { + background-position: 0px -608px; +} +.sceditor-button-bold div { + background-position: 0px -624px; +} +div.sceditor-grip { + background-position: 0px -640px; + width: 10px; + height: 10px; +} +.rtl div.sceditor-grip { + background-position: 0px -650px; + width: 10px; + height: 10px; +} +/** + * SCEditor + * http://www.ssceditor.com/ + * + * Copyright (C) 2011-12, Sam Clarke (samclarke.com) + * + * SCEditor is licensed under the MIT license: + * http://www.opensource.org/licenses/mit-license.php + */ +/*--------------------------------------------------- + LESS Elements 0.7 + --------------------------------------------------- + A set of useful LESS mixins + More info at: http://lesselements.com + ---------------------------------------------------*/ +.sceditor-container { + position: relative; + background: #fff; + border: 1px solid #d9d9d9; + font-size: 13px; + font-family: Arial, "Helvetica Neue", Helvetica, sans-serif; + color: #222; + line-height: 1; + font-weight: bold; + border-radius: 4px; + background-clip: padding-box; +} +.sceditor-container *, +.sceditor-container *:before, +.sceditor-container *:after { + -webkit-box-sizing: content-box; + -moz-box-sizing: content-box; + box-sizing: content-box; +} +.sceditor-container, +.sceditor-container div, +div.sceditor-dropdown, +div.sceditor-dropdown div { + padding: 0; + margin: 0; + z-index: 3; +} +.sceditor-container iframe, +.sceditor-container textarea { + line-height: 1; + border: 0; + outline: none; + font-family: Verdana, Arial, Helvetica, sans-serif; + font-size: 13px; + color: #111; + padding: 0; + margin: 5px; + resize: none; + background: #fff; + display: block; +} +div.sceditor-resize-cover { + position: absolute; + top: 0; + left: 0; + background: #000; + width: 100%; + height: 100%; + z-index: 10; + opacity: 0.3; +} +.ie6 div.sceditor-resize-cover, +.ie7 div.sceditor-resize-cover, +.ie8 div.sceditor-resize-cover { + background: #efefef; +} +.sceditor-container.ie6 { + overflow: hidden; +} +div.sceditor-grip { + overflow: hidden; + width: 10px; + height: 10px; + cursor: pointer; + position: absolute; + bottom: 0; + right: 0; + z-index: 3; +} +.sceditor-maximize { + position: fixed; + top: 0; + left: 0; + height: 100% !important; + width: 100% !important; + border-radius: 0; + background-clip: padding-box; + z-index: 2000; +} +html.sceditor-maximize, +body.sceditor-maximize { + height: 100%; + width: 100%; + padding: 0; + margin: 0; + overflow: hidden; +} +.ie6.sceditor-maximize { + position: absolute; +} +.sceditor-maximize div.sceditor-grip { + display: none; +} +.sceditor-maximize div.sceditor-toolbar { + border-radius: 0; + background-clip: padding-box; +} +/** + * Dropdown styleing + */ +div.sceditor-dropdown { + position: absolute; + border: 1px solid #ccc; + background: #fff; + color: #333; + z-index: 4000; + padding: 10px; + line-height: 1; + border-radius: 2px; + background-clip: padding-box; + box-shadow: 1px 2px 4px rgba(0, 0, 0, 0.2); +} +div.sceditor-dropdown a, +div.sceditor-dropdown a:link { + color: #333; +} +div.sceditor-dropdown form { + margin: 0; +} +div.sceditor-dropdown label { + display: block; + font-weight: bold; + color: #3c3c3c; + padding: 4px 0; +} +div.sceditor-dropdown input, +div.sceditor-dropdown textarea { + font-family: Arial, "Helvetica Neue", Helvetica, sans-serif; + outline: 0; + padding: 4px; + border: 1px solid #ccc; + border-top-color: #888; + margin: 0 0 .75em; + border-radius: 1px; + background-clip: padding-box; +} +div.sceditor-dropdown textarea { + padding: 6px; +} +div.sceditor-dropdown input:focus, +div.sceditor-dropdown textarea:focus { + border-color: #aaa; + border-top-color: #666; + box-shadow: inset 0 1px 5px rgba(0, 0, 0, 0.1); +} +div.sceditor-dropdown .button { + font-weight: bold; + color: #444; + padding: 6px 12px; + background: #ececec; + border: solid 1px #ccc; + border-radius: 2px; + background-clip: padding-box; + cursor: pointer; + margin: .3em 0 0; +} +div.sceditor-dropdown .button:hover { + background: #f3f3f3; + box-shadow: 0 1px 1px rgba(0, 0, 0, 0.15); +} +div.sceditor-font-picker, +div.sceditor-fontsize-picker, +div.sceditor-format { + padding: 6px 0; +} +div.sceditor-emoticons, +div.sceditor-more-emoticons, +div.sceditor-color-picker { + padding: 0; +} +.sceditor-pastetext textarea { + border: 1px solid #bbb; + width: 20em; +} +.sceditor-emoticons img, +.sceditor-more-emoticons img { + padding: 0; + cursor: pointer; + margin: 2px; +} +.sceditor-more { + border-top: 1px solid #bbb; + display: block; + text-align: center; + cursor: pointer; + font-weight: bold; + padding: 6px 0; +} +.sceditor-dropdown a:hover { + background: #eee; +} +.sceditor-fontsize-option, +.sceditor-font-option, +.sceditor-format a { + display: block; + padding: 7px 10px; + cursor: pointer; + text-decoration: none; + color: #222; +} +.sceditor-fontsize-option { + padding: 7px 13px; +} +.sceditor-color-column { + float: left; +} +.sceditor-color-option { + display: block; + border: 1px solid #fff; + height: 10px; + width: 10px; + overflow: hidden; +} +.sceditor-color-option:hover { + border: 1px solid #333; +} +/** + * Toolbar styleing + */ +div.sceditor-toolbar { + overflow: hidden; + padding: 3px 5px 2px; + background: #f7f7f7; + border-bottom: 1px solid #c0c0c0; + line-height: 0; + text-align: left; + user-select: none; + border-radius: 3px 3px 0 0; + background-clip: padding-box; +} +div.sceditor-group { + display: inline-block; + background: #ddd; + margin: 1px 5px 1px 0; + padding: 1px; + border-bottom: 1px solid #aaa; + border-radius: 3px; + background-clip: padding-box; +} +.ie6 div.sceditor-group, +.ie7 div.sceditor-group { + display: inline; + zoom: 1; +} +.sceditor-button { + float: left; + cursor: pointer; + padding: 3px 5px; + width: 16px; + height: 20px; + border-radius: 3px; + background-clip: padding-box; + /* Needed for Safari 5? */ + text-indent: -9999px; +} +.ie .sceditor-button { + text-indent: 0; +} +.ie6 .sceditor-button, +.ie7 .sceditor-button { + float: none !important; + display: inline; + zoom: 1; +} +.ie6 .sceditor-button { + padding: 0; +} +.ie6 .sceditor-button div { + margin: 5px; +} +.ie7 .sceditor-button div { + margin: 5px 0; +} +.sceditor-button:hover, +.sceditor-button:active, +.sceditor-button.active { + background: #fff; + box-shadow: inset 1px 1px 0 rgba(0,0,0,0.3), inset -1px 0 rgba(0,0,0,0.3), inset 0 -1px 0 rgba(0,0,0,0.2); +} +.sceditor-button:active { + background: #fff; + box-shadow: inset 1px 1px 0 rgba(0,0,0,0.3), inset -1px 0 rgba(0,0,0,0.3), inset 0 -1px 0 rgba(0,0,0,0.2), inset 0 0 8px rgba(0,0,0,0.3); +} +.sceditor-button.disabled:hover { + background: inherit; + cursor: default; + box-shadow: none; +} +.sceditor-button, +.sceditor-button div { + display: block; +} +.sceditor-button div { + margin: 2px 0; + padding: 0; + overflow: hidden; + line-height: 0; + font-size: 0; + color: transparent; +} +.sceditor-button.disabled div { + filter: alpha(opacity=30); + opacity: 0.3; +} +.text .sceditor-button, +.text .sceditor-button div, +.sceditor-button.text, +.sceditor-button.text div, +.text-icon .sceditor-button, +.text-icon .sceditor-button div, +.sceditor-button.text-icon, +.sceditor-button.text-icon div { + width: auto; + overflow: visible; + line-height: 16px; + font-size: 1em; + color: inherit; + text-indent: 0; +} +.text .sceditor-button div, +.sceditor-button.text div { + padding: 0 2px; + background: none; +} +.text-icon .sceditor-button div, +.sceditor-button.text-icon div { + padding: 0 2px 0 20px; +} +.rtl div.sceditor-toolbar { + text-align: right; +} +.rtl .sceditor-button { + float: right; +} +.rtl div.sceditor-grip { + right: auto; + left: 0; +} diff --git a/js/sceditor/development/themes/famfamfam.png b/js/sceditor/development/themes/famfamfam.png new file mode 100644 index 00000000..c95ef4ac Binary files /dev/null and b/js/sceditor/development/themes/famfamfam.png differ diff --git a/js/sceditor/development/themes/modern.css b/js/sceditor/development/themes/modern.css new file mode 100644 index 00000000..00fe57f4 --- /dev/null +++ b/js/sceditor/development/themes/modern.css @@ -0,0 +1,578 @@ +/** + * Modern theme + * + * Copyright (C) 2012, Sam Clarke (samclarke.com) + * + * SCEditor is licensed under the MIT license: + * http://www.opensource.org/licenses/mit-license.php + * + * Icons by Mark James (http://www.famfamfam.com/lab/icons/silk/) + * Licensed under the Creative Commons CC-BY license (http://creativecommons.org/licenses/by/3.0/) + */ +/*! SCEditor | (C) 2011-2013, Sam Clarke | sceditor.com/license */ +/** + * SCEditor + * http://www.ssceditor.com/ + * + * Copyright (C) 2011-12, Sam Clarke (samclarke.com) + * + * SCEditor is licensed under the MIT license: + * http://www.opensource.org/licenses/mit-license.php + */ +div.sceditor-grip, +.sceditor-button div { + background-image: url("famfamfam.png"); + background-repeat: no-repeat; + width: 16px; + height: 16px; +} +.sceditor-button-youtube div { + background-position: 0px 0px; +} +.sceditor-button-link div { + background-position: 0px -16px; +} +.sceditor-button-unlink div { + background-position: 0px -32px; +} +.sceditor-button-underline div { + background-position: 0px -48px; +} +.sceditor-button-time div { + background-position: 0px -64px; +} +.sceditor-button-table div { + background-position: 0px -80px; +} +.sceditor-button-superscript div { + background-position: 0px -96px; +} +.sceditor-button-subscript div { + background-position: 0px -112px; +} +.sceditor-button-strike div { + background-position: 0px -128px; +} +.sceditor-button-source div { + background-position: 0px -144px; +} +.sceditor-button-size div { + background-position: 0px -160px; +} +.sceditor-button-rtl div { + background-position: 0px -176px; +} +.sceditor-button-right div { + background-position: 0px -192px; +} +.sceditor-button-removeformat div { + background-position: 0px -208px; +} +.sceditor-button-quote div { + background-position: 0px -224px; +} +.sceditor-button-print div { + background-position: 0px -240px; +} +.sceditor-button-pastetext div { + background-position: 0px -256px; +} +.sceditor-button-paste div { + background-position: 0px -272px; +} +.sceditor-button-outdent div { + background-position: 0px -288px; +} +.sceditor-button-orderedlist div { + background-position: 0px -304px; +} +.sceditor-button-maximize div { + background-position: 0px -320px; +} +.sceditor-button-ltr div { + background-position: 0px -336px; +} +.sceditor-button-left div { + background-position: 0px -352px; +} +.sceditor-button-justify div { + background-position: 0px -368px; +} +.sceditor-button-italic div { + background-position: 0px -384px; +} +.sceditor-button-indent div { + background-position: 0px -400px; +} +.sceditor-button-image div { + background-position: 0px -416px; +} +.sceditor-button-horizontalrule div { + background-position: 0px -432px; +} +.sceditor-button-format div { + background-position: 0px -448px; +} +.sceditor-button-font div { + background-position: 0px -464px; +} +.sceditor-button-emoticon div { + background-position: 0px -480px; +} +.sceditor-button-email div { + background-position: 0px -496px; +} +.sceditor-button-date div { + background-position: 0px -512px; +} +.sceditor-button-cut div { + background-position: 0px -528px; +} +.sceditor-button-copy div { + background-position: 0px -544px; +} +.sceditor-button-color div { + background-position: 0px -560px; +} +.sceditor-button-code div { + background-position: 0px -576px; +} +.sceditor-button-center div { + background-position: 0px -592px; +} +.sceditor-button-bulletlist div { + background-position: 0px -608px; +} +.sceditor-button-bold div { + background-position: 0px -624px; +} +div.sceditor-grip { + background-position: 0px -640px; + width: 10px; + height: 10px; +} +.rtl div.sceditor-grip { + background-position: 0px -650px; + width: 10px; + height: 10px; +} +/** + * SCEditor + * http://www.ssceditor.com/ + * + * Copyright (C) 2011-12, Sam Clarke (samclarke.com) + * + * SCEditor is licensed under the MIT license: + * http://www.opensource.org/licenses/mit-license.php + */ +/*--------------------------------------------------- + LESS Elements 0.7 + --------------------------------------------------- + A set of useful LESS mixins + More info at: http://lesselements.com + ---------------------------------------------------*/ +.sceditor-container { + position: relative; + background: #fff; + border: 1px solid #d9d9d9; + font-size: 13px; + font-family: Arial, "Helvetica Neue", Helvetica, sans-serif; + color: #222; + line-height: 1; + font-weight: bold; + border-radius: 4px; + background-clip: padding-box; +} +.sceditor-container *, +.sceditor-container *:before, +.sceditor-container *:after { + -webkit-box-sizing: content-box; + -moz-box-sizing: content-box; + box-sizing: content-box; +} +.sceditor-container, +.sceditor-container div, +div.sceditor-dropdown, +div.sceditor-dropdown div { + padding: 0; + margin: 0; + z-index: 3; +} +.sceditor-container iframe, +.sceditor-container textarea { + line-height: 1; + border: 0; + outline: none; + font-family: Verdana, Arial, Helvetica, sans-serif; + font-size: 13px; + color: #111; + padding: 0; + margin: 5px; + resize: none; + background: #fff; + display: block; +} +div.sceditor-resize-cover { + position: absolute; + top: 0; + left: 0; + background: #000; + width: 100%; + height: 100%; + z-index: 10; + opacity: 0.3; +} +.ie6 div.sceditor-resize-cover, +.ie7 div.sceditor-resize-cover, +.ie8 div.sceditor-resize-cover { + background: #efefef; +} +.sceditor-container.ie6 { + overflow: hidden; +} +div.sceditor-grip { + overflow: hidden; + width: 10px; + height: 10px; + cursor: pointer; + position: absolute; + bottom: 0; + right: 0; + z-index: 3; +} +.sceditor-maximize { + position: fixed; + top: 0; + left: 0; + height: 100% !important; + width: 100% !important; + border-radius: 0; + background-clip: padding-box; + z-index: 2000; +} +html.sceditor-maximize, +body.sceditor-maximize { + height: 100%; + width: 100%; + padding: 0; + margin: 0; + overflow: hidden; +} +.ie6.sceditor-maximize { + position: absolute; +} +.sceditor-maximize div.sceditor-grip { + display: none; +} +.sceditor-maximize div.sceditor-toolbar { + border-radius: 0; + background-clip: padding-box; +} +/** + * Dropdown styleing + */ +div.sceditor-dropdown { + position: absolute; + border: 1px solid #ccc; + background: #fff; + color: #333; + z-index: 4000; + padding: 10px; + line-height: 1; + border-radius: 2px; + background-clip: padding-box; + box-shadow: 1px 2px 4px rgba(0, 0, 0, 0.2); +} +div.sceditor-dropdown a, +div.sceditor-dropdown a:link { + color: #333; +} +div.sceditor-dropdown form { + margin: 0; +} +div.sceditor-dropdown label { + display: block; + font-weight: bold; + color: #3c3c3c; + padding: 4px 0; +} +div.sceditor-dropdown input, +div.sceditor-dropdown textarea { + font-family: Arial, "Helvetica Neue", Helvetica, sans-serif; + outline: 0; + padding: 4px; + border: 1px solid #ccc; + border-top-color: #888; + margin: 0 0 .75em; + border-radius: 1px; + background-clip: padding-box; +} +div.sceditor-dropdown textarea { + padding: 6px; +} +div.sceditor-dropdown input:focus, +div.sceditor-dropdown textarea:focus { + border-color: #aaa; + border-top-color: #666; + box-shadow: inset 0 1px 5px rgba(0, 0, 0, 0.1); +} +div.sceditor-dropdown .button { + font-weight: bold; + color: #444; + padding: 6px 12px; + background: #ececec; + border: solid 1px #ccc; + border-radius: 2px; + background-clip: padding-box; + cursor: pointer; + margin: .3em 0 0; +} +div.sceditor-dropdown .button:hover { + background: #f3f3f3; + box-shadow: 0 1px 1px rgba(0, 0, 0, 0.15); +} +div.sceditor-font-picker, +div.sceditor-fontsize-picker, +div.sceditor-format { + padding: 6px 0; +} +div.sceditor-emoticons, +div.sceditor-more-emoticons, +div.sceditor-color-picker { + padding: 0; +} +.sceditor-pastetext textarea { + border: 1px solid #bbb; + width: 20em; +} +.sceditor-emoticons img, +.sceditor-more-emoticons img { + padding: 0; + cursor: pointer; + margin: 2px; +} +.sceditor-more { + border-top: 1px solid #bbb; + display: block; + text-align: center; + cursor: pointer; + font-weight: bold; + padding: 6px 0; +} +.sceditor-dropdown a:hover { + background: #eee; +} +.sceditor-fontsize-option, +.sceditor-font-option, +.sceditor-format a { + display: block; + padding: 7px 10px; + cursor: pointer; + text-decoration: none; + color: #222; +} +.sceditor-fontsize-option { + padding: 7px 13px; +} +.sceditor-color-column { + float: left; +} +.sceditor-color-option { + display: block; + border: 1px solid #fff; + height: 10px; + width: 10px; + overflow: hidden; +} +.sceditor-color-option:hover { + border: 1px solid #333; +} +/** + * Toolbar styleing + */ +div.sceditor-toolbar { + overflow: hidden; + padding: 3px 5px 2px; + background: #f7f7f7; + border-bottom: 1px solid #c0c0c0; + line-height: 0; + text-align: left; + user-select: none; + border-radius: 3px 3px 0 0; + background-clip: padding-box; +} +div.sceditor-group { + display: inline-block; + background: #ddd; + margin: 1px 5px 1px 0; + padding: 1px; + border-bottom: 1px solid #aaa; + border-radius: 3px; + background-clip: padding-box; +} +.ie6 div.sceditor-group, +.ie7 div.sceditor-group { + display: inline; + zoom: 1; +} +.sceditor-button { + float: left; + cursor: pointer; + padding: 3px 5px; + width: 16px; + height: 20px; + border-radius: 3px; + background-clip: padding-box; + /* Needed for Safari 5? */ + text-indent: -9999px; +} +.ie .sceditor-button { + text-indent: 0; +} +.ie6 .sceditor-button, +.ie7 .sceditor-button { + float: none !important; + display: inline; + zoom: 1; +} +.ie6 .sceditor-button { + padding: 0; +} +.ie6 .sceditor-button div { + margin: 5px; +} +.ie7 .sceditor-button div { + margin: 5px 0; +} +.sceditor-button:hover, +.sceditor-button:active, +.sceditor-button.active { + background: #fff; + box-shadow: inset 1px 1px 0 rgba(0,0,0,0.3), inset -1px 0 rgba(0,0,0,0.3), inset 0 -1px 0 rgba(0,0,0,0.2); +} +.sceditor-button:active { + background: #fff; + box-shadow: inset 1px 1px 0 rgba(0,0,0,0.3), inset -1px 0 rgba(0,0,0,0.3), inset 0 -1px 0 rgba(0,0,0,0.2), inset 0 0 8px rgba(0,0,0,0.3); +} +.sceditor-button.disabled:hover { + background: inherit; + cursor: default; + box-shadow: none; +} +.sceditor-button, +.sceditor-button div { + display: block; +} +.sceditor-button div { + margin: 2px 0; + padding: 0; + overflow: hidden; + line-height: 0; + font-size: 0; + color: transparent; +} +.sceditor-button.disabled div { + filter: alpha(opacity=30); + opacity: 0.3; +} +.text .sceditor-button, +.text .sceditor-button div, +.sceditor-button.text, +.sceditor-button.text div, +.text-icon .sceditor-button, +.text-icon .sceditor-button div, +.sceditor-button.text-icon, +.sceditor-button.text-icon div { + width: auto; + overflow: visible; + line-height: 16px; + font-size: 1em; + color: inherit; + text-indent: 0; +} +.text .sceditor-button div, +.sceditor-button.text div { + padding: 0 2px; + background: none; +} +.text-icon .sceditor-button div, +.sceditor-button.text-icon div { + padding: 0 2px 0 20px; +} +.rtl div.sceditor-toolbar { + text-align: right; +} +.rtl .sceditor-button { + float: right; +} +.rtl div.sceditor-grip { + right: auto; + left: 0; +} +.sceditor-container { + border: 1px solid #999; +} +.sceditor-container textarea { + font-family: Consolas, "Bitstream Vera Sans Mono", "Andale Mono", Monaco, "DejaVu Sans Mono", "Lucida Console", monospace; + background: #2e3436; + color: #fff; + margin: 0; + padding: 5px; +} +div.sceditor-toolbar { + background: #ccc; + background: linear-gradient(to bottom, #cccccc 0%, #b2b2b2 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#cccccc', endColorstr='#b2b2b2', GradientType=0); +} +.ie9 div.sceditor-toolbar { + filter: none; + background: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiA/Pgo8c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjEwMCUiIGhlaWdodD0iMTAwJSIgdmlld0JveD0iMCAwIDEgMSIgcHJlc2VydmVBc3BlY3RSYXRpbz0ibm9uZSI+CiAgPGxpbmVhckdyYWRpZW50IGlkPSJncmFkLXVjZ2ctZ2VuZXJhdGVkIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgeDE9IjAlIiB5MT0iMCUiIHgyPSIwJSIgeTI9IjEwMCUiPgogICAgPHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iI2NjY2NjYyIgc3RvcC1vcGFjaXR5PSIxIi8+CiAgICA8c3RvcCBvZmZzZXQ9IjEwMCUiIHN0b3AtY29sb3I9IiNiMmIyYjIiIHN0b3Atb3BhY2l0eT0iMSIvPgogIDwvbGluZWFyR3JhZGllbnQ+CiAgPHJlY3QgeD0iMCIgeT0iMCIgd2lkdGg9IjEiIGhlaWdodD0iMSIgZmlsbD0idXJsKCNncmFkLXVjZ2ctZ2VuZXJhdGVkKSIgLz4KPC9zdmc+); +} +div.sceditor-group { + display: inline; + background: transparent; + margin: 0; + padding: 0; + border: 0; +} +.sceditor-button { + padding: 4px; + margin: 2px 1px 2px 3px; + height: 16px; + border-radius: 12px; + background-clip: padding-box; +} +.sceditor-button:hover, +.sceditor-button.active, +.sceditor-button.active:hover { + box-shadow: none; +} +.sceditor-button:hover { + background: #fff; + background: rgba(255, 255, 255, 0.75); + margin: 1px 0 1px 2px; + border: 1px solid #eee; +} +.sceditor-button.disabled:hover { + margin: 2px 1px 2px 3px; + border: 0; +} +.sceditor-button.active { + background: #b1b1b1; + background: rgba(0, 0, 0, 0.1); + margin: 1px 0 1px 2px; + border: 1px solid #999; +} +.sceditor-button.active:hover { + background: #fff; + background: rgba(255, 255, 255, 0.25); +} +.sceditor-button:active, +.sceditor-button.active:active { + margin: 1px 0 1px 2px; + border: 1px solid #999; + box-shadow: inset 0 0 4px rgba(0, 0, 0, 0.5); +} +.sceditor-button div { + margin: 0; +} diff --git a/js/sceditor/development/themes/monocons.css b/js/sceditor/development/themes/monocons.css new file mode 100644 index 00000000..f955835c --- /dev/null +++ b/js/sceditor/development/themes/monocons.css @@ -0,0 +1,643 @@ +/** + * SCEditor + * http://www.ssceditor.com/ + * + * Copyright (C) 2011-12, Sam Clarke (samclarke.com) + * + * SCEditor is licensed under the MIT license: + * http://www.opensource.org/licenses/mit-license.php + */ +@font-face { + font-family: 'Monocons'; + src: url('monocons//monocons.eot'); + src: url('monocons//monocons.eot?#iefix') format('embedded-opentype'), url('monocons//monocons.ttf') format('truetype'); + font-weight: normal; + font-style: normal; +} +.sceditor-button div:before, +div.sceditor-grip { + font-family: 'Monocons'; + font-size: 16px; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + -webkit-font-smoothing: antialiased; +} +.sceditor-button-youtube div { + *zoom: expression(this.runtimeStyle['zoom'] = '1', this.innerHTML = '\e000'); +} +.sceditor-button-unlink div { + *zoom: expression(this.runtimeStyle['zoom'] = '1', this.innerHTML = '\e001'); +} +.sceditor-button-underline div { + *zoom: expression(this.runtimeStyle['zoom'] = '1', this.innerHTML = '\e002'); +} +.sceditor-button-time div { + *zoom: expression(this.runtimeStyle['zoom'] = '1', this.innerHTML = '\e003'); +} +.sceditor-button-table div { + *zoom: expression(this.runtimeStyle['zoom'] = '1', this.innerHTML = '\e004'); +} +.sceditor-button-superscript div { + *zoom: expression(this.runtimeStyle['zoom'] = '1', this.innerHTML = '\e005'); +} +.sceditor-button-subscript div { + *zoom: expression(this.runtimeStyle['zoom'] = '1', this.innerHTML = '\e006'); +} +.sceditor-button-strike div { + *zoom: expression(this.runtimeStyle['zoom'] = '1', this.innerHTML = '\e007'); +} +.sceditor-button-source div { + *zoom: expression(this.runtimeStyle['zoom'] = '1', this.innerHTML = '\e008'); +} +.sceditor-button-size div { + *zoom: expression(this.runtimeStyle['zoom'] = '1', this.innerHTML = '\e009'); +} +.sceditor-button-rtl div { + *zoom: expression(this.runtimeStyle['zoom'] = '1', this.innerHTML = '\e00a'); +} +.sceditor-button-right div { + *zoom: expression(this.runtimeStyle['zoom'] = '1', this.innerHTML = '\e00b'); +} +.sceditor-button-removeformat div { + *zoom: expression(this.runtimeStyle['zoom'] = '1', this.innerHTML = '\e00c'); +} +.sceditor-button-quote div { + *zoom: expression(this.runtimeStyle['zoom'] = '1', this.innerHTML = '\e00d'); +} +.sceditor-button-print div { + *zoom: expression(this.runtimeStyle['zoom'] = '1', this.innerHTML = '\e00e'); +} +.sceditor-button-pastetext div { + *zoom: expression(this.runtimeStyle['zoom'] = '1', this.innerHTML = '\e00f'); +} +.sceditor-button-paste div { + *zoom: expression(this.runtimeStyle['zoom'] = '1', this.innerHTML = '\e010'); +} +.sceditor-button-orderedlist div { + *zoom: expression(this.runtimeStyle['zoom'] = '1', this.innerHTML = '\e011'); +} +.sceditor-button-maximize div { + *zoom: expression(this.runtimeStyle['zoom'] = '1', this.innerHTML = '\e012'); +} +.sceditor-button-ltr div { + *zoom: expression(this.runtimeStyle['zoom'] = '1', this.innerHTML = '\e013'); +} +.sceditor-button-link div { + *zoom: expression(this.runtimeStyle['zoom'] = '1', this.innerHTML = '\e014'); +} +.sceditor-button-left div { + *zoom: expression(this.runtimeStyle['zoom'] = '1', this.innerHTML = '\e015'); +} +.sceditor-button-justify div { + *zoom: expression(this.runtimeStyle['zoom'] = '1', this.innerHTML = '\e016'); +} +.sceditor-button-italic div { + *zoom: expression(this.runtimeStyle['zoom'] = '1', this.innerHTML = '\e017'); +} +.sceditor-button-image div { + *zoom: expression(this.runtimeStyle['zoom'] = '1', this.innerHTML = '\e018'); +} +.sceditor-button-horizontalrule div { + *zoom: expression(this.runtimeStyle['zoom'] = '1', this.innerHTML = '\e019'); +} +.sceditor-button-format div { + *zoom: expression(this.runtimeStyle['zoom'] = '1', this.innerHTML = '\e01c'); +} +.sceditor-button-font div { + *zoom: expression(this.runtimeStyle['zoom'] = '1', this.innerHTML = '\e01d'); +} +.sceditor-button-emoticon div { + *zoom: expression(this.runtimeStyle['zoom'] = '1', this.innerHTML = '\e01e'); +} +.sceditor-button-email div { + *zoom: expression(this.runtimeStyle['zoom'] = '1', this.innerHTML = '\e01f'); +} +.sceditor-button-bold div { + *zoom: expression(this.runtimeStyle['zoom'] = '1', this.innerHTML = '\e020'); +} +.sceditor-button-date div { + *zoom: expression(this.runtimeStyle['zoom'] = '1', this.innerHTML = '\e021'); +} +.sceditor-button-cut div { + *zoom: expression(this.runtimeStyle['zoom'] = '1', this.innerHTML = '\e022'); +} +.sceditor-button-copy div { + *zoom: expression(this.runtimeStyle['zoom'] = '1', this.innerHTML = '\e023'); +} +.sceditor-button-color div { + *zoom: expression(this.runtimeStyle['zoom'] = '1', this.innerHTML = '\e024'); +} +.sceditor-button-code div { + *zoom: expression(this.runtimeStyle['zoom'] = '1', this.innerHTML = '\e025'); +} +.sceditor-button-center div { + *zoom: expression(this.runtimeStyle['zoom'] = '1', this.innerHTML = '\e026'); +} +.sceditor-button-bulletlist div { + *zoom: expression(this.runtimeStyle['zoom'] = '1', this.innerHTML = '\e027'); +} +div.sceditor-grip { + *zoom: expression(this.runtimeStyle['zoom'] = '1', this.innerHTML = '\e01b'); +} +.rtl div.sceditor-grip { + *zoom: expression(this.runtimeStyle['zoom'] = '1', this.innerHTML = '\e01a'); +} +.sceditor-button-youtube div:before { + content: "\e000"; +} +.sceditor-button-unlink div:before { + content: "\e001"; +} +.sceditor-button-underline div:before { + content: "\e002"; +} +.sceditor-button-time div:before { + content: "\e003"; +} +.sceditor-button-table div:before { + content: "\e004"; +} +.sceditor-button-superscript div:before { + content: "\e005"; +} +.sceditor-button-subscript div:before { + content: "\e006"; +} +.sceditor-button-strike div:before { + content: "\e007"; +} +.sceditor-button-source div:before { + content: "\e008"; +} +.sceditor-button-size div:before { + content: "\e009"; +} +.sceditor-button-rtl div:before { + content: "\e00a"; +} +.sceditor-button-right div:before { + content: "\e00b"; +} +.sceditor-button-removeformat div:before { + content: "\e00c"; +} +.sceditor-button-quote div:before { + content: "\e00d"; +} +.sceditor-button-print div:before { + content: "\e00e"; +} +.sceditor-button-pastetext div:before { + content: "\e00f"; +} +.sceditor-button-paste div:before { + content: "\e010"; +} +.sceditor-button-orderedlist div:before { + content: "\e011"; +} +.sceditor-button-maximize div:before { + content: "\e012"; +} +.sceditor-button-ltr div:before { + content: "\e013"; +} +.sceditor-button-link div:before { + content: "\e014"; +} +.sceditor-button-left div:before { + content: "\e015"; +} +.sceditor-button-justify div:before { + content: "\e016"; +} +.sceditor-button-italic div:before { + content: "\e017"; +} +.sceditor-button-image div:before { + content: "\e018"; +} +.sceditor-button-horizontalrule div:before { + content: "\e019"; +} +.sceditor-button-format div:before { + content: "\e01c"; +} +.sceditor-button-font div:before { + content: "\e01d"; +} +.sceditor-button-emoticon div:before { + content: "\e01e"; +} +.sceditor-button-email div:before { + content: "\e01f"; +} +.sceditor-button-bold div:before { + content: "\e020"; +} +.sceditor-button-date div:before { + content: "\e021"; +} +.sceditor-button-cut div:before { + content: "\e022"; +} +.sceditor-button-copy div:before { + content: "\e023"; +} +.sceditor-button-color div:before { + content: "\e024"; +} +.sceditor-button-code div:before { + content: "\e025"; +} +.sceditor-button-center div:before { + content: "\e026"; +} +.sceditor-button-bulletlist div:before { + content: "\e027"; +} +div.sceditor-grip:before { + content: "\e01b"; +} +.rtl div.sceditor-grip:before { + content: "\e01a"; +} +/** + * SCEditor + * http://www.ssceditor.com/ + * + * Copyright (C) 2011-12, Sam Clarke (samclarke.com) + * + * SCEditor is licensed under the MIT license: + * http://www.opensource.org/licenses/mit-license.php + */ +/*--------------------------------------------------- + LESS Elements 0.7 + --------------------------------------------------- + A set of useful LESS mixins + More info at: http://lesselements.com + ---------------------------------------------------*/ +.sceditor-container { + position: relative; + background: #fff; + border: 1px solid #d9d9d9; + font-size: 13px; + font-family: Arial, "Helvetica Neue", Helvetica, sans-serif; + color: #222; + line-height: 1; + font-weight: bold; + border-radius: 4px; + background-clip: padding-box; +} +.sceditor-container *, +.sceditor-container *:before, +.sceditor-container *:after { + -webkit-box-sizing: content-box; + -moz-box-sizing: content-box; + box-sizing: content-box; +} +.sceditor-container, +.sceditor-container div, +div.sceditor-dropdown, +div.sceditor-dropdown div { + padding: 0; + margin: 0; + z-index: 3; +} +.sceditor-container iframe, +.sceditor-container textarea { + line-height: 1; + border: 0; + outline: none; + font-family: Verdana, Arial, Helvetica, sans-serif; + font-size: 13px; + color: #111; + padding: 0; + margin: 5px; + resize: none; + background: #fff; + display: block; +} +div.sceditor-resize-cover { + position: absolute; + top: 0; + left: 0; + background: #000; + width: 100%; + height: 100%; + z-index: 10; + opacity: 0.3; +} +.ie6 div.sceditor-resize-cover, +.ie7 div.sceditor-resize-cover, +.ie8 div.sceditor-resize-cover { + background: #efefef; +} +.sceditor-container.ie6 { + overflow: hidden; +} +div.sceditor-grip { + overflow: hidden; + width: 10px; + height: 10px; + cursor: pointer; + position: absolute; + bottom: 0; + right: 0; + z-index: 3; +} +.sceditor-maximize { + position: fixed; + top: 0; + left: 0; + height: 100% !important; + width: 100% !important; + border-radius: 0; + background-clip: padding-box; + z-index: 2000; +} +html.sceditor-maximize, +body.sceditor-maximize { + height: 100%; + width: 100%; + padding: 0; + margin: 0; + overflow: hidden; +} +.ie6.sceditor-maximize { + position: absolute; +} +.sceditor-maximize div.sceditor-grip { + display: none; +} +.sceditor-maximize div.sceditor-toolbar { + border-radius: 0; + background-clip: padding-box; +} +/** + * Dropdown styleing + */ +div.sceditor-dropdown { + position: absolute; + border: 1px solid #ccc; + background: #fff; + color: #333; + z-index: 4000; + padding: 10px; + line-height: 1; + border-radius: 2px; + background-clip: padding-box; + box-shadow: 1px 2px 4px rgba(0, 0, 0, 0.2); +} +div.sceditor-dropdown a, +div.sceditor-dropdown a:link { + color: #333; +} +div.sceditor-dropdown form { + margin: 0; +} +div.sceditor-dropdown label { + display: block; + font-weight: bold; + color: #3c3c3c; + padding: 4px 0; +} +div.sceditor-dropdown input, +div.sceditor-dropdown textarea { + font-family: Arial, "Helvetica Neue", Helvetica, sans-serif; + outline: 0; + padding: 4px; + border: 1px solid #ccc; + border-top-color: #888; + margin: 0 0 .75em; + border-radius: 1px; + background-clip: padding-box; +} +div.sceditor-dropdown textarea { + padding: 6px; +} +div.sceditor-dropdown input:focus, +div.sceditor-dropdown textarea:focus { + border-color: #aaa; + border-top-color: #666; + box-shadow: inset 0 1px 5px rgba(0, 0, 0, 0.1); +} +div.sceditor-dropdown .button { + font-weight: bold; + color: #444; + padding: 6px 12px; + background: #ececec; + border: solid 1px #ccc; + border-radius: 2px; + background-clip: padding-box; + cursor: pointer; + margin: .3em 0 0; +} +div.sceditor-dropdown .button:hover { + background: #f3f3f3; + box-shadow: 0 1px 1px rgba(0, 0, 0, 0.15); +} +div.sceditor-font-picker, +div.sceditor-fontsize-picker, +div.sceditor-format { + padding: 6px 0; +} +div.sceditor-emoticons, +div.sceditor-more-emoticons, +div.sceditor-color-picker { + padding: 0; +} +.sceditor-pastetext textarea { + border: 1px solid #bbb; + width: 20em; +} +.sceditor-emoticons img, +.sceditor-more-emoticons img { + padding: 0; + cursor: pointer; + margin: 2px; +} +.sceditor-more { + border-top: 1px solid #bbb; + display: block; + text-align: center; + cursor: pointer; + font-weight: bold; + padding: 6px 0; +} +.sceditor-dropdown a:hover { + background: #eee; +} +.sceditor-fontsize-option, +.sceditor-font-option, +.sceditor-format a { + display: block; + padding: 7px 10px; + cursor: pointer; + text-decoration: none; + color: #222; +} +.sceditor-fontsize-option { + padding: 7px 13px; +} +.sceditor-color-column { + float: left; +} +.sceditor-color-option { + display: block; + border: 1px solid #fff; + height: 10px; + width: 10px; + overflow: hidden; +} +.sceditor-color-option:hover { + border: 1px solid #333; +} +/** + * Toolbar styleing + */ +div.sceditor-toolbar { + overflow: hidden; + padding: 3px 5px 2px; + background: #f7f7f7; + border-bottom: 1px solid #c0c0c0; + line-height: 0; + text-align: left; + user-select: none; + border-radius: 3px 3px 0 0; + background-clip: padding-box; +} +div.sceditor-group { + display: inline-block; + background: #ddd; + margin: 1px 5px 1px 0; + padding: 1px; + border-bottom: 1px solid #aaa; + border-radius: 3px; + background-clip: padding-box; +} +.ie6 div.sceditor-group, +.ie7 div.sceditor-group { + display: inline; + zoom: 1; +} +.sceditor-button { + float: left; + cursor: pointer; + padding: 3px 5px; + width: 16px; + height: 20px; + border-radius: 3px; + background-clip: padding-box; + /* Needed for Safari 5? */ + text-indent: -9999px; +} +.ie .sceditor-button { + text-indent: 0; +} +.ie6 .sceditor-button, +.ie7 .sceditor-button { + float: none !important; + display: inline; + zoom: 1; +} +.ie6 .sceditor-button { + padding: 0; +} +.ie6 .sceditor-button div { + margin: 5px; +} +.ie7 .sceditor-button div { + margin: 5px 0; +} +.sceditor-button:hover, +.sceditor-button:active, +.sceditor-button.active { + background: #fff; + box-shadow: inset 1px 1px 0 rgba(0,0,0,0.3), inset -1px 0 rgba(0,0,0,0.3), inset 0 -1px 0 rgba(0,0,0,0.2); +} +.sceditor-button:active { + background: #fff; + box-shadow: inset 1px 1px 0 rgba(0,0,0,0.3), inset -1px 0 rgba(0,0,0,0.3), inset 0 -1px 0 rgba(0,0,0,0.2), inset 0 0 8px rgba(0,0,0,0.3); +} +.sceditor-button.disabled:hover { + background: inherit; + cursor: default; + box-shadow: none; +} +.sceditor-button, +.sceditor-button div { + display: block; +} +.sceditor-button div { + margin: 2px 0; + padding: 0; + overflow: hidden; + line-height: 0; + font-size: 0; + color: transparent; +} +.sceditor-button.disabled div { + filter: alpha(opacity=30); + opacity: 0.3; +} +.text .sceditor-button, +.text .sceditor-button div, +.sceditor-button.text, +.sceditor-button.text div, +.text-icon .sceditor-button, +.text-icon .sceditor-button div, +.sceditor-button.text-icon, +.sceditor-button.text-icon div { + width: auto; + overflow: visible; + line-height: 16px; + font-size: 1em; + color: inherit; + text-indent: 0; +} +.text .sceditor-button div, +.sceditor-button.text div { + padding: 0 2px; + background: none; +} +.text-icon .sceditor-button div, +.sceditor-button.text-icon div { + padding: 0 2px 0 20px; +} +.rtl div.sceditor-toolbar { + text-align: right; +} +.rtl .sceditor-button { + float: right; +} +.rtl div.sceditor-grip { + right: auto; + left: 0; +} +.ie7 .sceditor-button div, +.ie6 .sceditor-button div { + font-family: 'Monocons'; + overflow: visible; + font-size: 16px; + line-height: 1; + text-indent: 0; +} +div.sceditor-grip { + height: 16px; + width: 16px; +} +.sceditor-button div:before, +div.sceditor-grip:before { + text-indent: 0; + line-height: 17px; + width: 16px; + height: 16px; + display: block; + color: #333; + text-shadow: 0 1px #fff; +} diff --git a/js/sceditor/development/themes/monocons/monocons.eot b/js/sceditor/development/themes/monocons/monocons.eot new file mode 100644 index 00000000..1db65477 Binary files /dev/null and b/js/sceditor/development/themes/monocons/monocons.eot differ diff --git a/js/sceditor/development/themes/monocons/monocons.ttf b/js/sceditor/development/themes/monocons/monocons.ttf new file mode 100644 index 00000000..d100abdf Binary files /dev/null and b/js/sceditor/development/themes/monocons/monocons.ttf differ diff --git a/js/sceditor/development/themes/office-toolbar.css b/js/sceditor/development/themes/office-toolbar.css new file mode 100644 index 00000000..896f96bd --- /dev/null +++ b/js/sceditor/development/themes/office-toolbar.css @@ -0,0 +1,594 @@ +/** + * Copyright (C) 2012, Sam Clarke (samclarke.com) + * + * SCEditor is licensed under the MIT license: + * http://www.opensource.org/licenses/mit-license.php + * + * Icons by Mark James (http://www.famfamfam.com/lab/icons/silk/) + * Licensed under the Creative Commons CC-BY license (http://creativecommons.org/licenses/by/3.0/) + */ +/*! SCEditor | (C) 2011-2013, Sam Clarke | sceditor.com/license */ +/** + * SCEditor + * http://www.ssceditor.com/ + * + * Copyright (C) 2011-12, Sam Clarke (samclarke.com) + * + * SCEditor is licensed under the MIT license: + * http://www.opensource.org/licenses/mit-license.php + */ +div.sceditor-grip, +.sceditor-button div { + background-image: url("famfamfam.png"); + background-repeat: no-repeat; + width: 16px; + height: 16px; +} +.sceditor-button-youtube div { + background-position: 0px 0px; +} +.sceditor-button-link div { + background-position: 0px -16px; +} +.sceditor-button-unlink div { + background-position: 0px -32px; +} +.sceditor-button-underline div { + background-position: 0px -48px; +} +.sceditor-button-time div { + background-position: 0px -64px; +} +.sceditor-button-table div { + background-position: 0px -80px; +} +.sceditor-button-superscript div { + background-position: 0px -96px; +} +.sceditor-button-subscript div { + background-position: 0px -112px; +} +.sceditor-button-strike div { + background-position: 0px -128px; +} +.sceditor-button-source div { + background-position: 0px -144px; +} +.sceditor-button-size div { + background-position: 0px -160px; +} +.sceditor-button-rtl div { + background-position: 0px -176px; +} +.sceditor-button-right div { + background-position: 0px -192px; +} +.sceditor-button-removeformat div { + background-position: 0px -208px; +} +.sceditor-button-quote div { + background-position: 0px -224px; +} +.sceditor-button-print div { + background-position: 0px -240px; +} +.sceditor-button-pastetext div { + background-position: 0px -256px; +} +.sceditor-button-paste div { + background-position: 0px -272px; +} +.sceditor-button-outdent div { + background-position: 0px -288px; +} +.sceditor-button-orderedlist div { + background-position: 0px -304px; +} +.sceditor-button-maximize div { + background-position: 0px -320px; +} +.sceditor-button-ltr div { + background-position: 0px -336px; +} +.sceditor-button-left div { + background-position: 0px -352px; +} +.sceditor-button-justify div { + background-position: 0px -368px; +} +.sceditor-button-italic div { + background-position: 0px -384px; +} +.sceditor-button-indent div { + background-position: 0px -400px; +} +.sceditor-button-image div { + background-position: 0px -416px; +} +.sceditor-button-horizontalrule div { + background-position: 0px -432px; +} +.sceditor-button-format div { + background-position: 0px -448px; +} +.sceditor-button-font div { + background-position: 0px -464px; +} +.sceditor-button-emoticon div { + background-position: 0px -480px; +} +.sceditor-button-email div { + background-position: 0px -496px; +} +.sceditor-button-date div { + background-position: 0px -512px; +} +.sceditor-button-cut div { + background-position: 0px -528px; +} +.sceditor-button-copy div { + background-position: 0px -544px; +} +.sceditor-button-color div { + background-position: 0px -560px; +} +.sceditor-button-code div { + background-position: 0px -576px; +} +.sceditor-button-center div { + background-position: 0px -592px; +} +.sceditor-button-bulletlist div { + background-position: 0px -608px; +} +.sceditor-button-bold div { + background-position: 0px -624px; +} +div.sceditor-grip { + background-position: 0px -640px; + width: 10px; + height: 10px; +} +.rtl div.sceditor-grip { + background-position: 0px -650px; + width: 10px; + height: 10px; +} +/** + * SCEditor + * http://www.ssceditor.com/ + * + * Copyright (C) 2011-12, Sam Clarke (samclarke.com) + * + * SCEditor is licensed under the MIT license: + * http://www.opensource.org/licenses/mit-license.php + */ +/*--------------------------------------------------- + LESS Elements 0.7 + --------------------------------------------------- + A set of useful LESS mixins + More info at: http://lesselements.com + ---------------------------------------------------*/ +.sceditor-container { + position: relative; + background: #fff; + border: 1px solid #d9d9d9; + font-size: 13px; + font-family: Arial, "Helvetica Neue", Helvetica, sans-serif; + color: #222; + line-height: 1; + font-weight: bold; + border-radius: 4px; + background-clip: padding-box; +} +.sceditor-container *, +.sceditor-container *:before, +.sceditor-container *:after { + -webkit-box-sizing: content-box; + -moz-box-sizing: content-box; + box-sizing: content-box; +} +.sceditor-container, +.sceditor-container div, +div.sceditor-dropdown, +div.sceditor-dropdown div { + padding: 0; + margin: 0; + z-index: 3; +} +.sceditor-container iframe, +.sceditor-container textarea { + line-height: 1; + border: 0; + outline: none; + font-family: Verdana, Arial, Helvetica, sans-serif; + font-size: 13px; + color: #111; + padding: 0; + margin: 5px; + resize: none; + background: #fff; + display: block; +} +div.sceditor-resize-cover { + position: absolute; + top: 0; + left: 0; + background: #000; + width: 100%; + height: 100%; + z-index: 10; + opacity: 0.3; +} +.ie6 div.sceditor-resize-cover, +.ie7 div.sceditor-resize-cover, +.ie8 div.sceditor-resize-cover { + background: #efefef; +} +.sceditor-container.ie6 { + overflow: hidden; +} +div.sceditor-grip { + overflow: hidden; + width: 10px; + height: 10px; + cursor: pointer; + position: absolute; + bottom: 0; + right: 0; + z-index: 3; +} +.sceditor-maximize { + position: fixed; + top: 0; + left: 0; + height: 100% !important; + width: 100% !important; + border-radius: 0; + background-clip: padding-box; + z-index: 2000; +} +html.sceditor-maximize, +body.sceditor-maximize { + height: 100%; + width: 100%; + padding: 0; + margin: 0; + overflow: hidden; +} +.ie6.sceditor-maximize { + position: absolute; +} +.sceditor-maximize div.sceditor-grip { + display: none; +} +.sceditor-maximize div.sceditor-toolbar { + border-radius: 0; + background-clip: padding-box; +} +/** + * Dropdown styleing + */ +div.sceditor-dropdown { + position: absolute; + border: 1px solid #ccc; + background: #fff; + color: #333; + z-index: 4000; + padding: 10px; + line-height: 1; + border-radius: 2px; + background-clip: padding-box; + box-shadow: 1px 2px 4px rgba(0, 0, 0, 0.2); +} +div.sceditor-dropdown a, +div.sceditor-dropdown a:link { + color: #333; +} +div.sceditor-dropdown form { + margin: 0; +} +div.sceditor-dropdown label { + display: block; + font-weight: bold; + color: #3c3c3c; + padding: 4px 0; +} +div.sceditor-dropdown input, +div.sceditor-dropdown textarea { + font-family: Arial, "Helvetica Neue", Helvetica, sans-serif; + outline: 0; + padding: 4px; + border: 1px solid #ccc; + border-top-color: #888; + margin: 0 0 .75em; + border-radius: 1px; + background-clip: padding-box; +} +div.sceditor-dropdown textarea { + padding: 6px; +} +div.sceditor-dropdown input:focus, +div.sceditor-dropdown textarea:focus { + border-color: #aaa; + border-top-color: #666; + box-shadow: inset 0 1px 5px rgba(0, 0, 0, 0.1); +} +div.sceditor-dropdown .button { + font-weight: bold; + color: #444; + padding: 6px 12px; + background: #ececec; + border: solid 1px #ccc; + border-radius: 2px; + background-clip: padding-box; + cursor: pointer; + margin: .3em 0 0; +} +div.sceditor-dropdown .button:hover { + background: #f3f3f3; + box-shadow: 0 1px 1px rgba(0, 0, 0, 0.15); +} +div.sceditor-font-picker, +div.sceditor-fontsize-picker, +div.sceditor-format { + padding: 6px 0; +} +div.sceditor-emoticons, +div.sceditor-more-emoticons, +div.sceditor-color-picker { + padding: 0; +} +.sceditor-pastetext textarea { + border: 1px solid #bbb; + width: 20em; +} +.sceditor-emoticons img, +.sceditor-more-emoticons img { + padding: 0; + cursor: pointer; + margin: 2px; +} +.sceditor-more { + border-top: 1px solid #bbb; + display: block; + text-align: center; + cursor: pointer; + font-weight: bold; + padding: 6px 0; +} +.sceditor-dropdown a:hover { + background: #eee; +} +.sceditor-fontsize-option, +.sceditor-font-option, +.sceditor-format a { + display: block; + padding: 7px 10px; + cursor: pointer; + text-decoration: none; + color: #222; +} +.sceditor-fontsize-option { + padding: 7px 13px; +} +.sceditor-color-column { + float: left; +} +.sceditor-color-option { + display: block; + border: 1px solid #fff; + height: 10px; + width: 10px; + overflow: hidden; +} +.sceditor-color-option:hover { + border: 1px solid #333; +} +/** + * Toolbar styleing + */ +div.sceditor-toolbar { + overflow: hidden; + padding: 3px 5px 2px; + background: #f7f7f7; + border-bottom: 1px solid #c0c0c0; + line-height: 0; + text-align: left; + user-select: none; + border-radius: 3px 3px 0 0; + background-clip: padding-box; +} +div.sceditor-group { + display: inline-block; + background: #ddd; + margin: 1px 5px 1px 0; + padding: 1px; + border-bottom: 1px solid #aaa; + border-radius: 3px; + background-clip: padding-box; +} +.ie6 div.sceditor-group, +.ie7 div.sceditor-group { + display: inline; + zoom: 1; +} +.sceditor-button { + float: left; + cursor: pointer; + padding: 3px 5px; + width: 16px; + height: 20px; + border-radius: 3px; + background-clip: padding-box; + /* Needed for Safari 5? */ + text-indent: -9999px; +} +.ie .sceditor-button { + text-indent: 0; +} +.ie6 .sceditor-button, +.ie7 .sceditor-button { + float: none !important; + display: inline; + zoom: 1; +} +.ie6 .sceditor-button { + padding: 0; +} +.ie6 .sceditor-button div { + margin: 5px; +} +.ie7 .sceditor-button div { + margin: 5px 0; +} +.sceditor-button:hover, +.sceditor-button:active, +.sceditor-button.active { + background: #fff; + box-shadow: inset 1px 1px 0 rgba(0,0,0,0.3), inset -1px 0 rgba(0,0,0,0.3), inset 0 -1px 0 rgba(0,0,0,0.2); +} +.sceditor-button:active { + background: #fff; + box-shadow: inset 1px 1px 0 rgba(0,0,0,0.3), inset -1px 0 rgba(0,0,0,0.3), inset 0 -1px 0 rgba(0,0,0,0.2), inset 0 0 8px rgba(0,0,0,0.3); +} +.sceditor-button.disabled:hover { + background: inherit; + cursor: default; + box-shadow: none; +} +.sceditor-button, +.sceditor-button div { + display: block; +} +.sceditor-button div { + margin: 2px 0; + padding: 0; + overflow: hidden; + line-height: 0; + font-size: 0; + color: transparent; +} +.sceditor-button.disabled div { + filter: alpha(opacity=30); + opacity: 0.3; +} +.text .sceditor-button, +.text .sceditor-button div, +.sceditor-button.text, +.sceditor-button.text div, +.text-icon .sceditor-button, +.text-icon .sceditor-button div, +.sceditor-button.text-icon, +.sceditor-button.text-icon div { + width: auto; + overflow: visible; + line-height: 16px; + font-size: 1em; + color: inherit; + text-indent: 0; +} +.text .sceditor-button div, +.sceditor-button.text div { + padding: 0 2px; + background: none; +} +.text-icon .sceditor-button div, +.sceditor-button.text-icon div { + padding: 0 2px 0 20px; +} +.rtl div.sceditor-toolbar { + text-align: right; +} +.rtl .sceditor-button { + float: right; +} +.rtl div.sceditor-grip { + right: auto; + left: 0; +} +.sceditor-container { + border: 1px solid #8db2e3; +} +.sceditor-container textarea { + font-family: Consolas, "Bitstream Vera Sans Mono", "Andale Mono", Monaco, "DejaVu Sans Mono", "Lucida Console", monospace; +} +div.sceditor-toolbar { + border-bottom: 1px solid #95a9c3; + background: #dee8f5; + background: linear-gradient(to bottom, #dee8f5 0%, #c7d8ed 29%, #ccdcee 61%, #c0d8ef 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#dee8f5', endColorstr='#c0d8ef', GradientType=0); +} +.ie9 div.sceditor-toolbar { + filter: none; + background: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiA/Pgo8c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjEwMCUiIGhlaWdodD0iMTAwJSIgdmlld0JveD0iMCAwIDEgMSIgcHJlc2VydmVBc3BlY3RSYXRpbz0ibm9uZSI+CiAgPGxpbmVhckdyYWRpZW50IGlkPSJncmFkLXVjZ2ctZ2VuZXJhdGVkIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgeDE9IjAlIiB5MT0iMCUiIHgyPSIwJSIgeTI9IjEwMCUiPgogICAgPHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iI2RlZThmNSIgc3RvcC1vcGFjaXR5PSIxIi8+CiAgICA8c3RvcCBvZmZzZXQ9IjI5JSIgc3RvcC1jb2xvcj0iI2M3ZDhlZCIgc3RvcC1vcGFjaXR5PSIxIi8+CiAgICA8c3RvcCBvZmZzZXQ9IjYxJSIgc3RvcC1jb2xvcj0iI2NjZGNlZSIgc3RvcC1vcGFjaXR5PSIxIi8+CiAgICA8c3RvcCBvZmZzZXQ9IjEwMCUiIHN0b3AtY29sb3I9IiNjMGQ4ZWYiIHN0b3Atb3BhY2l0eT0iMSIvPgogIDwvbGluZWFyR3JhZGllbnQ+CiAgPHJlY3QgeD0iMCIgeT0iMCIgd2lkdGg9IjEiIGhlaWdodD0iMSIgZmlsbD0idXJsKCNncmFkLXVjZ2ctZ2VuZXJhdGVkKSIgLz4KPC9zdmc+); +} +div.sceditor-group { + border: 1px solid #7596bf; + background: transparent; + padding: 0; + background: #cadcf0; + background: linear-gradient(to bottom, #cadcf0 24%, #bcd0e9 38%, #d0e1f7 99%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#cadcf0', endColorstr='#d0e1f7', GradientType=0); +} +.ie9 div.sceditor-group { + filter: none; + background: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiA/Pgo8c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjEwMCUiIGhlaWdodD0iMTAwJSIgdmlld0JveD0iMCAwIDEgMSIgcHJlc2VydmVBc3BlY3RSYXRpbz0ibm9uZSI+CiAgPGxpbmVhckdyYWRpZW50IGlkPSJncmFkLXVjZ2ctZ2VuZXJhdGVkIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgeDE9IjAlIiB5MT0iMCUiIHgyPSIwJSIgeTI9IjEwMCUiPgogICAgPHN0b3Agb2Zmc2V0PSIyNCUiIHN0b3AtY29sb3I9IiNjYWRjZjAiIHN0b3Atb3BhY2l0eT0iMSIvPgogICAgPHN0b3Agb2Zmc2V0PSIzOCUiIHN0b3AtY29sb3I9IiNiY2QwZTkiIHN0b3Atb3BhY2l0eT0iMSIvPgogICAgPHN0b3Agb2Zmc2V0PSI5OSUiIHN0b3AtY29sb3I9IiNkMGUxZjciIHN0b3Atb3BhY2l0eT0iMSIvPgogIDwvbGluZWFyR3JhZGllbnQ+CiAgPHJlY3QgeD0iMCIgeT0iMCIgd2lkdGg9IjEiIGhlaWdodD0iMSIgZmlsbD0idXJsKCNncmFkLXVjZ2ctZ2VuZXJhdGVkKSIgLz4KPC9zdmc+); +} +.sceditor-button { + height: 16px; + padding: 3px 4px; + border-radius: 0; + background-clip: padding-box; + box-shadow: inset 0 1px #d5e3f1, inset 0 -1px #e3edfb, inset 1px 0 #cddcef, inset -1px 0 #b8ceea; +} +.sceditor-button:first-child { + border-radius: 4px 0 0 4px; + background-clip: padding-box; +} +.sceditor-button:last-child { + border-radius: 0 4px 4px 0; + background-clip: padding-box; +} +.sceditor-button div { + margin: 0; +} +.ie9 .sceditor-button { + filter: none !important; +} +.sceditor-button.active { + background: #fbdbb5; + background: linear-gradient(to bottom, #fbdbb5 11%, #feb456 29%, #fdeb9f 99%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fbdbb5', endColorstr='#fdeb9f', GradientType=0); + box-shadow: inset 0 1px #ebd1b4, inset 0 -1px #ffe47f, inset -1px 0 #b8ceea; +} +.ie9 .sceditor-button.active { + background: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiA/Pgo8c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjEwMCUiIGhlaWdodD0iMTAwJSIgdmlld0JveD0iMCAwIDEgMSIgcHJlc2VydmVBc3BlY3RSYXRpbz0ibm9uZSI+CiAgPGxpbmVhckdyYWRpZW50IGlkPSJncmFkLXVjZ2ctZ2VuZXJhdGVkIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgeDE9IjAlIiB5MT0iMCUiIHgyPSIwJSIgeTI9IjEwMCUiPgogICAgPHN0b3Agb2Zmc2V0PSIxMSUiIHN0b3AtY29sb3I9IiNmYmRiYjUiIHN0b3Atb3BhY2l0eT0iMSIvPgogICAgPHN0b3Agb2Zmc2V0PSIyOSUiIHN0b3AtY29sb3I9IiNmZWI0NTYiIHN0b3Atb3BhY2l0eT0iMSIvPgogICAgPHN0b3Agb2Zmc2V0PSI5OSUiIHN0b3AtY29sb3I9IiNmZGZkOWYiIHN0b3Atb3BhY2l0eT0iMSIvPgogIDwvbGluZWFyR3JhZGllbnQ+CiAgPHJlY3QgeD0iMCIgeT0iMCIgd2lkdGg9IjEiIGhlaWdodD0iMSIgZmlsbD0idXJsKCNncmFkLXVjZ2ctZ2VuZXJhdGVkKSIgLz4KPC9zdmc+); +} +.sceditor-button:hover { + background: #fef7d5; + background: linear-gradient(to bottom, #fef7d5 0%, #fae5a9 42%, #ffd048 42%, #ffe59f 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fef7d5', endColorstr='#ffe59f', GradientType=0); + box-shadow: inset 0 1px #fffbe8, inset -1px 0 #ffefc4, inset 0 -1px #fff9cc; +} +.ie9 .sceditor-button:hover { + background: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiA/Pgo8c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjEwMCUiIGhlaWdodD0iMTAwJSIgdmlld0JveD0iMCAwIDEgMSIgcHJlc2VydmVBc3BlY3RSYXRpbz0ibm9uZSI+CiAgPGxpbmVhckdyYWRpZW50IGlkPSJncmFkLXVjZ2ctZ2VuZXJhdGVkIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgeDE9IjAlIiB5MT0iMCUiIHgyPSIwJSIgeTI9IjEwMCUiPgogICAgPHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iI2ZlZjdkNSIgc3RvcC1vcGFjaXR5PSIxIi8+CiAgICA8c3RvcCBvZmZzZXQ9IjQyJSIgc3RvcC1jb2xvcj0iI2ZhZTVhOSIgc3RvcC1vcGFjaXR5PSIxIi8+CiAgICA8c3RvcCBvZmZzZXQ9IjQyJSIgc3RvcC1jb2xvcj0iI2ZmZDA0OCIgc3RvcC1vcGFjaXR5PSIxIi8+CiAgICA8c3RvcCBvZmZzZXQ9IjEwMCUiIHN0b3AtY29sb3I9IiNmZmU1OWYiIHN0b3Atb3BhY2l0eT0iMSIvPgogIDwvbGluZWFyR3JhZGllbnQ+CiAgPHJlY3QgeD0iMCIgeT0iMCIgd2lkdGg9IjEiIGhlaWdodD0iMSIgZmlsbD0idXJsKCNncmFkLXVjZ2ctZ2VuZXJhdGVkKSIgLz4KPC9zdmc+); +} +.sceditor-button:active { + background: #e7a66d; + background: linear-gradient(to bottom, #e7a66d 0%, #fcb16d 1%, #ff8d05 42%, #ffc450 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#e7a66d', endColorstr='#ffc450', GradientType=0); + box-shadow: inset 0 1px 1px #7b6645, inset 0 -1px #d19c33; +} +.ie9 .sceditor-button:active { + background: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiA/Pgo8c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjEwMCUiIGhlaWdodD0iMTAwJSIgdmlld0JveD0iMCAwIDEgMSIgcHJlc2VydmVBc3BlY3RSYXRpbz0ibm9uZSI+CiAgPGxpbmVhckdyYWRpZW50IGlkPSJncmFkLXVjZ2ctZ2VuZXJhdGVkIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgeDE9IjAlIiB5MT0iMCUiIHgyPSIwJSIgeTI9IjEwMCUiPgogICAgPHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iI2U3YTY2ZCIgc3RvcC1vcGFjaXR5PSIxIi8+CiAgICA8c3RvcCBvZmZzZXQ9IjElIiBzdG9wLWNvbG9yPSIjZmNiMTZkIiBzdG9wLW9wYWNpdHk9IjEiLz4KICAgIDxzdG9wIG9mZnNldD0iNDIlIiBzdG9wLWNvbG9yPSIjZmY4ZDA1IiBzdG9wLW9wYWNpdHk9IjEiLz4KICAgIDxzdG9wIG9mZnNldD0iMTAwJSIgc3RvcC1jb2xvcj0iI2ZmYzQ1MCIgc3RvcC1vcGFjaXR5PSIxIi8+CiAgPC9saW5lYXJHcmFkaWVudD4KICA8cmVjdCB4PSIwIiB5PSIwIiB3aWR0aD0iMSIgaGVpZ2h0PSIxIiBmaWxsPSJ1cmwoI2dyYWQtdWNnZy1nZW5lcmF0ZWQpIiAvPgo8L3N2Zz4=); +} +.sceditor-button.active:hover { + background: #dba368; + background: linear-gradient(to bottom, #dba368 0%, #ffbd79 4%, #fea335 34%, #ffc64c 66%, #fee069 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#dba368', endColorstr='#fee069', GradientType=0); + box-shadow: inset 0 1px 1px #9e8255, inset 0 -1px #fcce6b; +} +.ie9 .sceditor-button.active:hover { + background: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiA/Pgo8c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjEwMCUiIGhlaWdodD0iMTAwJSIgdmlld0JveD0iMCAwIDEgMSIgcHJlc2VydmVBc3BlY3RSYXRpbz0ibm9uZSI+CiAgPGxpbmVhckdyYWRpZW50IGlkPSJncmFkLXVjZ2ctZ2VuZXJhdGVkIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgeDE9IjAlIiB5MT0iMCUiIHgyPSIwJSIgeTI9IjEwMCUiPgogICAgPHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iI2RiYTM2OCIgc3RvcC1vcGFjaXR5PSIxIi8+CiAgICA8c3RvcCBvZmZzZXQ9IjQlIiBzdG9wLWNvbG9yPSIjZmZiZDc5IiBzdG9wLW9wYWNpdHk9IjEiLz4KICAgIDxzdG9wIG9mZnNldD0iMzQlIiBzdG9wLWNvbG9yPSIjZmVhMzM1IiBzdG9wLW9wYWNpdHk9IjEiLz4KICAgIDxzdG9wIG9mZnNldD0iNjYlIiBzdG9wLWNvbG9yPSIjZmZjNjRjIiBzdG9wLW9wYWNpdHk9IjEiLz4KICAgIDxzdG9wIG9mZnNldD0iMTAwJSIgc3RvcC1jb2xvcj0iI2ZlZTA2OSIgc3RvcC1vcGFjaXR5PSIxIi8+CiAgPC9saW5lYXJHcmFkaWVudD4KICA8cmVjdCB4PSIwIiB5PSIwIiB3aWR0aD0iMSIgaGVpZ2h0PSIxIiBmaWxsPSJ1cmwoI2dyYWQtdWNnZy1nZW5lcmF0ZWQpIiAvPgo8L3N2Zz4=); +} diff --git a/js/sceditor/development/themes/office.css b/js/sceditor/development/themes/office.css new file mode 100644 index 00000000..7f2f3281 --- /dev/null +++ b/js/sceditor/development/themes/office.css @@ -0,0 +1,616 @@ +/** + * Copyright (C) 2012, Sam Clarke (samclarke.com) + * + * SCEditor is licensed under the MIT license: + * http://www.opensource.org/licenses/mit-license.php + * + * Icons by Mark James (http://www.famfamfam.com/lab/icons/silk/) + * Licensed under the Creative Commons CC-BY license (http://creativecommons.org/licenses/by/3.0/) + */ +/** + * Copyright (C) 2012, Sam Clarke (samclarke.com) + * + * SCEditor is licensed under the MIT license: + * http://www.opensource.org/licenses/mit-license.php + * + * Icons by Mark James (http://www.famfamfam.com/lab/icons/silk/) + * Licensed under the Creative Commons CC-BY license (http://creativecommons.org/licenses/by/3.0/) + */ +/*! SCEditor | (C) 2011-2013, Sam Clarke | sceditor.com/license */ +/** + * SCEditor + * http://www.ssceditor.com/ + * + * Copyright (C) 2011-12, Sam Clarke (samclarke.com) + * + * SCEditor is licensed under the MIT license: + * http://www.opensource.org/licenses/mit-license.php + */ +div.sceditor-grip, +.sceditor-button div { + background-image: url("famfamfam.png"); + background-repeat: no-repeat; + width: 16px; + height: 16px; +} +.sceditor-button-youtube div { + background-position: 0px 0px; +} +.sceditor-button-link div { + background-position: 0px -16px; +} +.sceditor-button-unlink div { + background-position: 0px -32px; +} +.sceditor-button-underline div { + background-position: 0px -48px; +} +.sceditor-button-time div { + background-position: 0px -64px; +} +.sceditor-button-table div { + background-position: 0px -80px; +} +.sceditor-button-superscript div { + background-position: 0px -96px; +} +.sceditor-button-subscript div { + background-position: 0px -112px; +} +.sceditor-button-strike div { + background-position: 0px -128px; +} +.sceditor-button-source div { + background-position: 0px -144px; +} +.sceditor-button-size div { + background-position: 0px -160px; +} +.sceditor-button-rtl div { + background-position: 0px -176px; +} +.sceditor-button-right div { + background-position: 0px -192px; +} +.sceditor-button-removeformat div { + background-position: 0px -208px; +} +.sceditor-button-quote div { + background-position: 0px -224px; +} +.sceditor-button-print div { + background-position: 0px -240px; +} +.sceditor-button-pastetext div { + background-position: 0px -256px; +} +.sceditor-button-paste div { + background-position: 0px -272px; +} +.sceditor-button-outdent div { + background-position: 0px -288px; +} +.sceditor-button-orderedlist div { + background-position: 0px -304px; +} +.sceditor-button-maximize div { + background-position: 0px -320px; +} +.sceditor-button-ltr div { + background-position: 0px -336px; +} +.sceditor-button-left div { + background-position: 0px -352px; +} +.sceditor-button-justify div { + background-position: 0px -368px; +} +.sceditor-button-italic div { + background-position: 0px -384px; +} +.sceditor-button-indent div { + background-position: 0px -400px; +} +.sceditor-button-image div { + background-position: 0px -416px; +} +.sceditor-button-horizontalrule div { + background-position: 0px -432px; +} +.sceditor-button-format div { + background-position: 0px -448px; +} +.sceditor-button-font div { + background-position: 0px -464px; +} +.sceditor-button-emoticon div { + background-position: 0px -480px; +} +.sceditor-button-email div { + background-position: 0px -496px; +} +.sceditor-button-date div { + background-position: 0px -512px; +} +.sceditor-button-cut div { + background-position: 0px -528px; +} +.sceditor-button-copy div { + background-position: 0px -544px; +} +.sceditor-button-color div { + background-position: 0px -560px; +} +.sceditor-button-code div { + background-position: 0px -576px; +} +.sceditor-button-center div { + background-position: 0px -592px; +} +.sceditor-button-bulletlist div { + background-position: 0px -608px; +} +.sceditor-button-bold div { + background-position: 0px -624px; +} +div.sceditor-grip { + background-position: 0px -640px; + width: 10px; + height: 10px; +} +.rtl div.sceditor-grip { + background-position: 0px -650px; + width: 10px; + height: 10px; +} +/** + * SCEditor + * http://www.ssceditor.com/ + * + * Copyright (C) 2011-12, Sam Clarke (samclarke.com) + * + * SCEditor is licensed under the MIT license: + * http://www.opensource.org/licenses/mit-license.php + */ +/*--------------------------------------------------- + LESS Elements 0.7 + --------------------------------------------------- + A set of useful LESS mixins + More info at: http://lesselements.com + ---------------------------------------------------*/ +.sceditor-container { + position: relative; + background: #fff; + border: 1px solid #d9d9d9; + font-size: 13px; + font-family: Arial, "Helvetica Neue", Helvetica, sans-serif; + color: #222; + line-height: 1; + font-weight: bold; + border-radius: 4px; + background-clip: padding-box; +} +.sceditor-container *, +.sceditor-container *:before, +.sceditor-container *:after { + -webkit-box-sizing: content-box; + -moz-box-sizing: content-box; + box-sizing: content-box; +} +.sceditor-container, +.sceditor-container div, +div.sceditor-dropdown, +div.sceditor-dropdown div { + padding: 0; + margin: 0; + z-index: 3; +} +.sceditor-container iframe, +.sceditor-container textarea { + line-height: 1; + border: 0; + outline: none; + font-family: Verdana, Arial, Helvetica, sans-serif; + font-size: 13px; + color: #111; + padding: 0; + margin: 5px; + resize: none; + background: #fff; + display: block; +} +div.sceditor-resize-cover { + position: absolute; + top: 0; + left: 0; + background: #000; + width: 100%; + height: 100%; + z-index: 10; + opacity: 0.3; +} +.ie6 div.sceditor-resize-cover, +.ie7 div.sceditor-resize-cover, +.ie8 div.sceditor-resize-cover { + background: #efefef; +} +.sceditor-container.ie6 { + overflow: hidden; +} +div.sceditor-grip { + overflow: hidden; + width: 10px; + height: 10px; + cursor: pointer; + position: absolute; + bottom: 0; + right: 0; + z-index: 3; +} +.sceditor-maximize { + position: fixed; + top: 0; + left: 0; + height: 100% !important; + width: 100% !important; + border-radius: 0; + background-clip: padding-box; + z-index: 2000; +} +html.sceditor-maximize, +body.sceditor-maximize { + height: 100%; + width: 100%; + padding: 0; + margin: 0; + overflow: hidden; +} +.ie6.sceditor-maximize { + position: absolute; +} +.sceditor-maximize div.sceditor-grip { + display: none; +} +.sceditor-maximize div.sceditor-toolbar { + border-radius: 0; + background-clip: padding-box; +} +/** + * Dropdown styleing + */ +div.sceditor-dropdown { + position: absolute; + border: 1px solid #ccc; + background: #fff; + color: #333; + z-index: 4000; + padding: 10px; + line-height: 1; + border-radius: 2px; + background-clip: padding-box; + box-shadow: 1px 2px 4px rgba(0, 0, 0, 0.2); +} +div.sceditor-dropdown a, +div.sceditor-dropdown a:link { + color: #333; +} +div.sceditor-dropdown form { + margin: 0; +} +div.sceditor-dropdown label { + display: block; + font-weight: bold; + color: #3c3c3c; + padding: 4px 0; +} +div.sceditor-dropdown input, +div.sceditor-dropdown textarea { + font-family: Arial, "Helvetica Neue", Helvetica, sans-serif; + outline: 0; + padding: 4px; + border: 1px solid #ccc; + border-top-color: #888; + margin: 0 0 .75em; + border-radius: 1px; + background-clip: padding-box; +} +div.sceditor-dropdown textarea { + padding: 6px; +} +div.sceditor-dropdown input:focus, +div.sceditor-dropdown textarea:focus { + border-color: #aaa; + border-top-color: #666; + box-shadow: inset 0 1px 5px rgba(0, 0, 0, 0.1); +} +div.sceditor-dropdown .button { + font-weight: bold; + color: #444; + padding: 6px 12px; + background: #ececec; + border: solid 1px #ccc; + border-radius: 2px; + background-clip: padding-box; + cursor: pointer; + margin: .3em 0 0; +} +div.sceditor-dropdown .button:hover { + background: #f3f3f3; + box-shadow: 0 1px 1px rgba(0, 0, 0, 0.15); +} +div.sceditor-font-picker, +div.sceditor-fontsize-picker, +div.sceditor-format { + padding: 6px 0; +} +div.sceditor-emoticons, +div.sceditor-more-emoticons, +div.sceditor-color-picker { + padding: 0; +} +.sceditor-pastetext textarea { + border: 1px solid #bbb; + width: 20em; +} +.sceditor-emoticons img, +.sceditor-more-emoticons img { + padding: 0; + cursor: pointer; + margin: 2px; +} +.sceditor-more { + border-top: 1px solid #bbb; + display: block; + text-align: center; + cursor: pointer; + font-weight: bold; + padding: 6px 0; +} +.sceditor-dropdown a:hover { + background: #eee; +} +.sceditor-fontsize-option, +.sceditor-font-option, +.sceditor-format a { + display: block; + padding: 7px 10px; + cursor: pointer; + text-decoration: none; + color: #222; +} +.sceditor-fontsize-option { + padding: 7px 13px; +} +.sceditor-color-column { + float: left; +} +.sceditor-color-option { + display: block; + border: 1px solid #fff; + height: 10px; + width: 10px; + overflow: hidden; +} +.sceditor-color-option:hover { + border: 1px solid #333; +} +/** + * Toolbar styleing + */ +div.sceditor-toolbar { + overflow: hidden; + padding: 3px 5px 2px; + background: #f7f7f7; + border-bottom: 1px solid #c0c0c0; + line-height: 0; + text-align: left; + user-select: none; + border-radius: 3px 3px 0 0; + background-clip: padding-box; +} +div.sceditor-group { + display: inline-block; + background: #ddd; + margin: 1px 5px 1px 0; + padding: 1px; + border-bottom: 1px solid #aaa; + border-radius: 3px; + background-clip: padding-box; +} +.ie6 div.sceditor-group, +.ie7 div.sceditor-group { + display: inline; + zoom: 1; +} +.sceditor-button { + float: left; + cursor: pointer; + padding: 3px 5px; + width: 16px; + height: 20px; + border-radius: 3px; + background-clip: padding-box; + /* Needed for Safari 5? */ + text-indent: -9999px; +} +.ie .sceditor-button { + text-indent: 0; +} +.ie6 .sceditor-button, +.ie7 .sceditor-button { + float: none !important; + display: inline; + zoom: 1; +} +.ie6 .sceditor-button { + padding: 0; +} +.ie6 .sceditor-button div { + margin: 5px; +} +.ie7 .sceditor-button div { + margin: 5px 0; +} +.sceditor-button:hover, +.sceditor-button:active, +.sceditor-button.active { + background: #fff; + box-shadow: inset 1px 1px 0 rgba(0,0,0,0.3), inset -1px 0 rgba(0,0,0,0.3), inset 0 -1px 0 rgba(0,0,0,0.2); +} +.sceditor-button:active { + background: #fff; + box-shadow: inset 1px 1px 0 rgba(0,0,0,0.3), inset -1px 0 rgba(0,0,0,0.3), inset 0 -1px 0 rgba(0,0,0,0.2), inset 0 0 8px rgba(0,0,0,0.3); +} +.sceditor-button.disabled:hover { + background: inherit; + cursor: default; + box-shadow: none; +} +.sceditor-button, +.sceditor-button div { + display: block; +} +.sceditor-button div { + margin: 2px 0; + padding: 0; + overflow: hidden; + line-height: 0; + font-size: 0; + color: transparent; +} +.sceditor-button.disabled div { + filter: alpha(opacity=30); + opacity: 0.3; +} +.text .sceditor-button, +.text .sceditor-button div, +.sceditor-button.text, +.sceditor-button.text div, +.text-icon .sceditor-button, +.text-icon .sceditor-button div, +.sceditor-button.text-icon, +.sceditor-button.text-icon div { + width: auto; + overflow: visible; + line-height: 16px; + font-size: 1em; + color: inherit; + text-indent: 0; +} +.text .sceditor-button div, +.sceditor-button.text div { + padding: 0 2px; + background: none; +} +.text-icon .sceditor-button div, +.sceditor-button.text-icon div { + padding: 0 2px 0 20px; +} +.rtl div.sceditor-toolbar { + text-align: right; +} +.rtl .sceditor-button { + float: right; +} +.rtl div.sceditor-grip { + right: auto; + left: 0; +} +.sceditor-container { + border: 1px solid #8db2e3; +} +.sceditor-container textarea { + font-family: Consolas, "Bitstream Vera Sans Mono", "Andale Mono", Monaco, "DejaVu Sans Mono", "Lucida Console", monospace; +} +div.sceditor-toolbar { + border-bottom: 1px solid #95a9c3; + background: #dee8f5; + background: linear-gradient(to bottom, #dee8f5 0%, #c7d8ed 29%, #ccdcee 61%, #c0d8ef 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#dee8f5', endColorstr='#c0d8ef', GradientType=0); +} +.ie9 div.sceditor-toolbar { + filter: none; + background: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiA/Pgo8c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjEwMCUiIGhlaWdodD0iMTAwJSIgdmlld0JveD0iMCAwIDEgMSIgcHJlc2VydmVBc3BlY3RSYXRpbz0ibm9uZSI+CiAgPGxpbmVhckdyYWRpZW50IGlkPSJncmFkLXVjZ2ctZ2VuZXJhdGVkIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgeDE9IjAlIiB5MT0iMCUiIHgyPSIwJSIgeTI9IjEwMCUiPgogICAgPHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iI2RlZThmNSIgc3RvcC1vcGFjaXR5PSIxIi8+CiAgICA8c3RvcCBvZmZzZXQ9IjI5JSIgc3RvcC1jb2xvcj0iI2M3ZDhlZCIgc3RvcC1vcGFjaXR5PSIxIi8+CiAgICA8c3RvcCBvZmZzZXQ9IjYxJSIgc3RvcC1jb2xvcj0iI2NjZGNlZSIgc3RvcC1vcGFjaXR5PSIxIi8+CiAgICA8c3RvcCBvZmZzZXQ9IjEwMCUiIHN0b3AtY29sb3I9IiNjMGQ4ZWYiIHN0b3Atb3BhY2l0eT0iMSIvPgogIDwvbGluZWFyR3JhZGllbnQ+CiAgPHJlY3QgeD0iMCIgeT0iMCIgd2lkdGg9IjEiIGhlaWdodD0iMSIgZmlsbD0idXJsKCNncmFkLXVjZ2ctZ2VuZXJhdGVkKSIgLz4KPC9zdmc+); +} +div.sceditor-group { + border: 1px solid #7596bf; + background: transparent; + padding: 0; + background: #cadcf0; + background: linear-gradient(to bottom, #cadcf0 24%, #bcd0e9 38%, #d0e1f7 99%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#cadcf0', endColorstr='#d0e1f7', GradientType=0); +} +.ie9 div.sceditor-group { + filter: none; + background: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiA/Pgo8c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjEwMCUiIGhlaWdodD0iMTAwJSIgdmlld0JveD0iMCAwIDEgMSIgcHJlc2VydmVBc3BlY3RSYXRpbz0ibm9uZSI+CiAgPGxpbmVhckdyYWRpZW50IGlkPSJncmFkLXVjZ2ctZ2VuZXJhdGVkIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgeDE9IjAlIiB5MT0iMCUiIHgyPSIwJSIgeTI9IjEwMCUiPgogICAgPHN0b3Agb2Zmc2V0PSIyNCUiIHN0b3AtY29sb3I9IiNjYWRjZjAiIHN0b3Atb3BhY2l0eT0iMSIvPgogICAgPHN0b3Agb2Zmc2V0PSIzOCUiIHN0b3AtY29sb3I9IiNiY2QwZTkiIHN0b3Atb3BhY2l0eT0iMSIvPgogICAgPHN0b3Agb2Zmc2V0PSI5OSUiIHN0b3AtY29sb3I9IiNkMGUxZjciIHN0b3Atb3BhY2l0eT0iMSIvPgogIDwvbGluZWFyR3JhZGllbnQ+CiAgPHJlY3QgeD0iMCIgeT0iMCIgd2lkdGg9IjEiIGhlaWdodD0iMSIgZmlsbD0idXJsKCNncmFkLXVjZ2ctZ2VuZXJhdGVkKSIgLz4KPC9zdmc+); +} +.sceditor-button { + height: 16px; + padding: 3px 4px; + border-radius: 0; + background-clip: padding-box; + box-shadow: inset 0 1px #d5e3f1, inset 0 -1px #e3edfb, inset 1px 0 #cddcef, inset -1px 0 #b8ceea; +} +.sceditor-button:first-child { + border-radius: 4px 0 0 4px; + background-clip: padding-box; +} +.sceditor-button:last-child { + border-radius: 0 4px 4px 0; + background-clip: padding-box; +} +.sceditor-button div { + margin: 0; +} +.ie9 .sceditor-button { + filter: none !important; +} +.sceditor-button.active { + background: #fbdbb5; + background: linear-gradient(to bottom, #fbdbb5 11%, #feb456 29%, #fdeb9f 99%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fbdbb5', endColorstr='#fdeb9f', GradientType=0); + box-shadow: inset 0 1px #ebd1b4, inset 0 -1px #ffe47f, inset -1px 0 #b8ceea; +} +.ie9 .sceditor-button.active { + background: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiA/Pgo8c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjEwMCUiIGhlaWdodD0iMTAwJSIgdmlld0JveD0iMCAwIDEgMSIgcHJlc2VydmVBc3BlY3RSYXRpbz0ibm9uZSI+CiAgPGxpbmVhckdyYWRpZW50IGlkPSJncmFkLXVjZ2ctZ2VuZXJhdGVkIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgeDE9IjAlIiB5MT0iMCUiIHgyPSIwJSIgeTI9IjEwMCUiPgogICAgPHN0b3Agb2Zmc2V0PSIxMSUiIHN0b3AtY29sb3I9IiNmYmRiYjUiIHN0b3Atb3BhY2l0eT0iMSIvPgogICAgPHN0b3Agb2Zmc2V0PSIyOSUiIHN0b3AtY29sb3I9IiNmZWI0NTYiIHN0b3Atb3BhY2l0eT0iMSIvPgogICAgPHN0b3Agb2Zmc2V0PSI5OSUiIHN0b3AtY29sb3I9IiNmZGZkOWYiIHN0b3Atb3BhY2l0eT0iMSIvPgogIDwvbGluZWFyR3JhZGllbnQ+CiAgPHJlY3QgeD0iMCIgeT0iMCIgd2lkdGg9IjEiIGhlaWdodD0iMSIgZmlsbD0idXJsKCNncmFkLXVjZ2ctZ2VuZXJhdGVkKSIgLz4KPC9zdmc+); +} +.sceditor-button:hover { + background: #fef7d5; + background: linear-gradient(to bottom, #fef7d5 0%, #fae5a9 42%, #ffd048 42%, #ffe59f 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fef7d5', endColorstr='#ffe59f', GradientType=0); + box-shadow: inset 0 1px #fffbe8, inset -1px 0 #ffefc4, inset 0 -1px #fff9cc; +} +.ie9 .sceditor-button:hover { + background: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiA/Pgo8c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjEwMCUiIGhlaWdodD0iMTAwJSIgdmlld0JveD0iMCAwIDEgMSIgcHJlc2VydmVBc3BlY3RSYXRpbz0ibm9uZSI+CiAgPGxpbmVhckdyYWRpZW50IGlkPSJncmFkLXVjZ2ctZ2VuZXJhdGVkIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgeDE9IjAlIiB5MT0iMCUiIHgyPSIwJSIgeTI9IjEwMCUiPgogICAgPHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iI2ZlZjdkNSIgc3RvcC1vcGFjaXR5PSIxIi8+CiAgICA8c3RvcCBvZmZzZXQ9IjQyJSIgc3RvcC1jb2xvcj0iI2ZhZTVhOSIgc3RvcC1vcGFjaXR5PSIxIi8+CiAgICA8c3RvcCBvZmZzZXQ9IjQyJSIgc3RvcC1jb2xvcj0iI2ZmZDA0OCIgc3RvcC1vcGFjaXR5PSIxIi8+CiAgICA8c3RvcCBvZmZzZXQ9IjEwMCUiIHN0b3AtY29sb3I9IiNmZmU1OWYiIHN0b3Atb3BhY2l0eT0iMSIvPgogIDwvbGluZWFyR3JhZGllbnQ+CiAgPHJlY3QgeD0iMCIgeT0iMCIgd2lkdGg9IjEiIGhlaWdodD0iMSIgZmlsbD0idXJsKCNncmFkLXVjZ2ctZ2VuZXJhdGVkKSIgLz4KPC9zdmc+); +} +.sceditor-button:active { + background: #e7a66d; + background: linear-gradient(to bottom, #e7a66d 0%, #fcb16d 1%, #ff8d05 42%, #ffc450 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#e7a66d', endColorstr='#ffc450', GradientType=0); + box-shadow: inset 0 1px 1px #7b6645, inset 0 -1px #d19c33; +} +.ie9 .sceditor-button:active { + background: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiA/Pgo8c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjEwMCUiIGhlaWdodD0iMTAwJSIgdmlld0JveD0iMCAwIDEgMSIgcHJlc2VydmVBc3BlY3RSYXRpbz0ibm9uZSI+CiAgPGxpbmVhckdyYWRpZW50IGlkPSJncmFkLXVjZ2ctZ2VuZXJhdGVkIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgeDE9IjAlIiB5MT0iMCUiIHgyPSIwJSIgeTI9IjEwMCUiPgogICAgPHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iI2U3YTY2ZCIgc3RvcC1vcGFjaXR5PSIxIi8+CiAgICA8c3RvcCBvZmZzZXQ9IjElIiBzdG9wLWNvbG9yPSIjZmNiMTZkIiBzdG9wLW9wYWNpdHk9IjEiLz4KICAgIDxzdG9wIG9mZnNldD0iNDIlIiBzdG9wLWNvbG9yPSIjZmY4ZDA1IiBzdG9wLW9wYWNpdHk9IjEiLz4KICAgIDxzdG9wIG9mZnNldD0iMTAwJSIgc3RvcC1jb2xvcj0iI2ZmYzQ1MCIgc3RvcC1vcGFjaXR5PSIxIi8+CiAgPC9saW5lYXJHcmFkaWVudD4KICA8cmVjdCB4PSIwIiB5PSIwIiB3aWR0aD0iMSIgaGVpZ2h0PSIxIiBmaWxsPSJ1cmwoI2dyYWQtdWNnZy1nZW5lcmF0ZWQpIiAvPgo8L3N2Zz4=); +} +.sceditor-button.active:hover { + background: #dba368; + background: linear-gradient(to bottom, #dba368 0%, #ffbd79 4%, #fea335 34%, #ffc64c 66%, #fee069 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#dba368', endColorstr='#fee069', GradientType=0); + box-shadow: inset 0 1px 1px #9e8255, inset 0 -1px #fcce6b; +} +.ie9 .sceditor-button.active:hover { + background: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiA/Pgo8c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjEwMCUiIGhlaWdodD0iMTAwJSIgdmlld0JveD0iMCAwIDEgMSIgcHJlc2VydmVBc3BlY3RSYXRpbz0ibm9uZSI+CiAgPGxpbmVhckdyYWRpZW50IGlkPSJncmFkLXVjZ2ctZ2VuZXJhdGVkIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgeDE9IjAlIiB5MT0iMCUiIHgyPSIwJSIgeTI9IjEwMCUiPgogICAgPHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iI2RiYTM2OCIgc3RvcC1vcGFjaXR5PSIxIi8+CiAgICA8c3RvcCBvZmZzZXQ9IjQlIiBzdG9wLWNvbG9yPSIjZmZiZDc5IiBzdG9wLW9wYWNpdHk9IjEiLz4KICAgIDxzdG9wIG9mZnNldD0iMzQlIiBzdG9wLWNvbG9yPSIjZmVhMzM1IiBzdG9wLW9wYWNpdHk9IjEiLz4KICAgIDxzdG9wIG9mZnNldD0iNjYlIiBzdG9wLWNvbG9yPSIjZmZjNjRjIiBzdG9wLW9wYWNpdHk9IjEiLz4KICAgIDxzdG9wIG9mZnNldD0iMTAwJSIgc3RvcC1jb2xvcj0iI2ZlZTA2OSIgc3RvcC1vcGFjaXR5PSIxIi8+CiAgPC9saW5lYXJHcmFkaWVudD4KICA8cmVjdCB4PSIwIiB5PSIwIiB3aWR0aD0iMSIgaGVpZ2h0PSIxIiBmaWxsPSJ1cmwoI2dyYWQtdWNnZy1nZW5lcmF0ZWQpIiAvPgo8L3N2Zz4=); +} +.sceditor-container { + background: #a3c2ea; + background: linear-gradient(to bottom, #a3c2ea 0%, #6d92c1 39%, #577fb3 64%, #6591cc 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#a3c2ea', endColorstr='#6591cc', GradientType=0); +} +.sceditor-container iframe, +.sceditor-container textarea { + border: 1px solid #646464; + background: #fff; + margin: 7px 40px; + padding: 20px; + box-shadow: 1px 1px 5px #293a52; +} diff --git a/js/sceditor/development/themes/square.css b/js/sceditor/development/themes/square.css new file mode 100644 index 00000000..3c644de4 --- /dev/null +++ b/js/sceditor/development/themes/square.css @@ -0,0 +1,589 @@ +/** + * Square theme + * + * This theme is best suited to short toolbars that + * don't span multiple lines. + * + * Copyright (C) 2012, Sam Clarke (samclarke.com) + * + * SCEditor is licensed under the MIT license: + * http://www.opensource.org/licenses/mit-license.php + * + * Icons by Mark James (http://www.famfamfam.com/lab/icons/silk/) + * Licensed under the Creative Commons CC-BY license (http://creativecommons.org/licenses/by/3.0/) + */ +/*! SCEditor | (C) 2011-2013, Sam Clarke | sceditor.com/license */ +/** + * SCEditor + * http://www.ssceditor.com/ + * + * Copyright (C) 2011-12, Sam Clarke (samclarke.com) + * + * SCEditor is licensed under the MIT license: + * http://www.opensource.org/licenses/mit-license.php + */ +div.sceditor-grip, +.sceditor-button div { + background-image: url("famfamfam.png"); + background-repeat: no-repeat; + width: 16px; + height: 16px; +} +.sceditor-button-youtube div { + background-position: 0px 0px; +} +.sceditor-button-link div { + background-position: 0px -16px; +} +.sceditor-button-unlink div { + background-position: 0px -32px; +} +.sceditor-button-underline div { + background-position: 0px -48px; +} +.sceditor-button-time div { + background-position: 0px -64px; +} +.sceditor-button-table div { + background-position: 0px -80px; +} +.sceditor-button-superscript div { + background-position: 0px -96px; +} +.sceditor-button-subscript div { + background-position: 0px -112px; +} +.sceditor-button-strike div { + background-position: 0px -128px; +} +.sceditor-button-source div { + background-position: 0px -144px; +} +.sceditor-button-size div { + background-position: 0px -160px; +} +.sceditor-button-rtl div { + background-position: 0px -176px; +} +.sceditor-button-right div { + background-position: 0px -192px; +} +.sceditor-button-removeformat div { + background-position: 0px -208px; +} +.sceditor-button-quote div { + background-position: 0px -224px; +} +.sceditor-button-print div { + background-position: 0px -240px; +} +.sceditor-button-pastetext div { + background-position: 0px -256px; +} +.sceditor-button-paste div { + background-position: 0px -272px; +} +.sceditor-button-outdent div { + background-position: 0px -288px; +} +.sceditor-button-orderedlist div { + background-position: 0px -304px; +} +.sceditor-button-maximize div { + background-position: 0px -320px; +} +.sceditor-button-ltr div { + background-position: 0px -336px; +} +.sceditor-button-left div { + background-position: 0px -352px; +} +.sceditor-button-justify div { + background-position: 0px -368px; +} +.sceditor-button-italic div { + background-position: 0px -384px; +} +.sceditor-button-indent div { + background-position: 0px -400px; +} +.sceditor-button-image div { + background-position: 0px -416px; +} +.sceditor-button-horizontalrule div { + background-position: 0px -432px; +} +.sceditor-button-format div { + background-position: 0px -448px; +} +.sceditor-button-font div { + background-position: 0px -464px; +} +.sceditor-button-emoticon div { + background-position: 0px -480px; +} +.sceditor-button-email div { + background-position: 0px -496px; +} +.sceditor-button-date div { + background-position: 0px -512px; +} +.sceditor-button-cut div { + background-position: 0px -528px; +} +.sceditor-button-copy div { + background-position: 0px -544px; +} +.sceditor-button-color div { + background-position: 0px -560px; +} +.sceditor-button-code div { + background-position: 0px -576px; +} +.sceditor-button-center div { + background-position: 0px -592px; +} +.sceditor-button-bulletlist div { + background-position: 0px -608px; +} +.sceditor-button-bold div { + background-position: 0px -624px; +} +div.sceditor-grip { + background-position: 0px -640px; + width: 10px; + height: 10px; +} +.rtl div.sceditor-grip { + background-position: 0px -650px; + width: 10px; + height: 10px; +} +/** + * SCEditor + * http://www.ssceditor.com/ + * + * Copyright (C) 2011-12, Sam Clarke (samclarke.com) + * + * SCEditor is licensed under the MIT license: + * http://www.opensource.org/licenses/mit-license.php + */ +/*--------------------------------------------------- + LESS Elements 0.7 + --------------------------------------------------- + A set of useful LESS mixins + More info at: http://lesselements.com + ---------------------------------------------------*/ +.sceditor-container { + position: relative; + background: #fff; + border: 1px solid #d9d9d9; + font-size: 13px; + font-family: Arial, "Helvetica Neue", Helvetica, sans-serif; + color: #222; + line-height: 1; + font-weight: bold; + border-radius: 4px; + background-clip: padding-box; +} +.sceditor-container *, +.sceditor-container *:before, +.sceditor-container *:after { + -webkit-box-sizing: content-box; + -moz-box-sizing: content-box; + box-sizing: content-box; +} +.sceditor-container, +.sceditor-container div, +div.sceditor-dropdown, +div.sceditor-dropdown div { + padding: 0; + margin: 0; + z-index: 3; +} +.sceditor-container iframe, +.sceditor-container textarea { + line-height: 1; + border: 0; + outline: none; + font-family: Verdana, Arial, Helvetica, sans-serif; + font-size: 13px; + color: #111; + padding: 0; + margin: 5px; + resize: none; + background: #fff; + display: block; +} +div.sceditor-resize-cover { + position: absolute; + top: 0; + left: 0; + background: #000; + width: 100%; + height: 100%; + z-index: 10; + opacity: 0.3; +} +.ie6 div.sceditor-resize-cover, +.ie7 div.sceditor-resize-cover, +.ie8 div.sceditor-resize-cover { + background: #efefef; +} +.sceditor-container.ie6 { + overflow: hidden; +} +div.sceditor-grip { + overflow: hidden; + width: 10px; + height: 10px; + cursor: pointer; + position: absolute; + bottom: 0; + right: 0; + z-index: 3; +} +.sceditor-maximize { + position: fixed; + top: 0; + left: 0; + height: 100% !important; + width: 100% !important; + border-radius: 0; + background-clip: padding-box; + z-index: 2000; +} +html.sceditor-maximize, +body.sceditor-maximize { + height: 100%; + width: 100%; + padding: 0; + margin: 0; + overflow: hidden; +} +.ie6.sceditor-maximize { + position: absolute; +} +.sceditor-maximize div.sceditor-grip { + display: none; +} +.sceditor-maximize div.sceditor-toolbar { + border-radius: 0; + background-clip: padding-box; +} +/** + * Dropdown styleing + */ +div.sceditor-dropdown { + position: absolute; + border: 1px solid #ccc; + background: #fff; + color: #333; + z-index: 4000; + padding: 10px; + line-height: 1; + border-radius: 2px; + background-clip: padding-box; + box-shadow: 1px 2px 4px rgba(0, 0, 0, 0.2); +} +div.sceditor-dropdown a, +div.sceditor-dropdown a:link { + color: #333; +} +div.sceditor-dropdown form { + margin: 0; +} +div.sceditor-dropdown label { + display: block; + font-weight: bold; + color: #3c3c3c; + padding: 4px 0; +} +div.sceditor-dropdown input, +div.sceditor-dropdown textarea { + font-family: Arial, "Helvetica Neue", Helvetica, sans-serif; + outline: 0; + padding: 4px; + border: 1px solid #ccc; + border-top-color: #888; + margin: 0 0 .75em; + border-radius: 1px; + background-clip: padding-box; +} +div.sceditor-dropdown textarea { + padding: 6px; +} +div.sceditor-dropdown input:focus, +div.sceditor-dropdown textarea:focus { + border-color: #aaa; + border-top-color: #666; + box-shadow: inset 0 1px 5px rgba(0, 0, 0, 0.1); +} +div.sceditor-dropdown .button { + font-weight: bold; + color: #444; + padding: 6px 12px; + background: #ececec; + border: solid 1px #ccc; + border-radius: 2px; + background-clip: padding-box; + cursor: pointer; + margin: .3em 0 0; +} +div.sceditor-dropdown .button:hover { + background: #f3f3f3; + box-shadow: 0 1px 1px rgba(0, 0, 0, 0.15); +} +div.sceditor-font-picker, +div.sceditor-fontsize-picker, +div.sceditor-format { + padding: 6px 0; +} +div.sceditor-emoticons, +div.sceditor-more-emoticons, +div.sceditor-color-picker { + padding: 0; +} +.sceditor-pastetext textarea { + border: 1px solid #bbb; + width: 20em; +} +.sceditor-emoticons img, +.sceditor-more-emoticons img { + padding: 0; + cursor: pointer; + margin: 2px; +} +.sceditor-more { + border-top: 1px solid #bbb; + display: block; + text-align: center; + cursor: pointer; + font-weight: bold; + padding: 6px 0; +} +.sceditor-dropdown a:hover { + background: #eee; +} +.sceditor-fontsize-option, +.sceditor-font-option, +.sceditor-format a { + display: block; + padding: 7px 10px; + cursor: pointer; + text-decoration: none; + color: #222; +} +.sceditor-fontsize-option { + padding: 7px 13px; +} +.sceditor-color-column { + float: left; +} +.sceditor-color-option { + display: block; + border: 1px solid #fff; + height: 10px; + width: 10px; + overflow: hidden; +} +.sceditor-color-option:hover { + border: 1px solid #333; +} +/** + * Toolbar styleing + */ +div.sceditor-toolbar { + overflow: hidden; + padding: 3px 5px 2px; + background: #f7f7f7; + border-bottom: 1px solid #c0c0c0; + line-height: 0; + text-align: left; + user-select: none; + border-radius: 3px 3px 0 0; + background-clip: padding-box; +} +div.sceditor-group { + display: inline-block; + background: #ddd; + margin: 1px 5px 1px 0; + padding: 1px; + border-bottom: 1px solid #aaa; + border-radius: 3px; + background-clip: padding-box; +} +.ie6 div.sceditor-group, +.ie7 div.sceditor-group { + display: inline; + zoom: 1; +} +.sceditor-button { + float: left; + cursor: pointer; + padding: 3px 5px; + width: 16px; + height: 20px; + border-radius: 3px; + background-clip: padding-box; + /* Needed for Safari 5? */ + text-indent: -9999px; +} +.ie .sceditor-button { + text-indent: 0; +} +.ie6 .sceditor-button, +.ie7 .sceditor-button { + float: none !important; + display: inline; + zoom: 1; +} +.ie6 .sceditor-button { + padding: 0; +} +.ie6 .sceditor-button div { + margin: 5px; +} +.ie7 .sceditor-button div { + margin: 5px 0; +} +.sceditor-button:hover, +.sceditor-button:active, +.sceditor-button.active { + background: #fff; + box-shadow: inset 1px 1px 0 rgba(0,0,0,0.3), inset -1px 0 rgba(0,0,0,0.3), inset 0 -1px 0 rgba(0,0,0,0.2); +} +.sceditor-button:active { + background: #fff; + box-shadow: inset 1px 1px 0 rgba(0,0,0,0.3), inset -1px 0 rgba(0,0,0,0.3), inset 0 -1px 0 rgba(0,0,0,0.2), inset 0 0 8px rgba(0,0,0,0.3); +} +.sceditor-button.disabled:hover { + background: inherit; + cursor: default; + box-shadow: none; +} +.sceditor-button, +.sceditor-button div { + display: block; +} +.sceditor-button div { + margin: 2px 0; + padding: 0; + overflow: hidden; + line-height: 0; + font-size: 0; + color: transparent; +} +.sceditor-button.disabled div { + filter: alpha(opacity=30); + opacity: 0.3; +} +.text .sceditor-button, +.text .sceditor-button div, +.sceditor-button.text, +.sceditor-button.text div, +.text-icon .sceditor-button, +.text-icon .sceditor-button div, +.sceditor-button.text-icon, +.sceditor-button.text-icon div { + width: auto; + overflow: visible; + line-height: 16px; + font-size: 1em; + color: inherit; + text-indent: 0; +} +.text .sceditor-button div, +.sceditor-button.text div { + padding: 0 2px; + background: none; +} +.text-icon .sceditor-button div, +.sceditor-button.text-icon div { + padding: 0 2px 0 20px; +} +.rtl div.sceditor-toolbar { + text-align: right; +} +.rtl .sceditor-button { + float: right; +} +.rtl div.sceditor-grip { + right: auto; + left: 0; +} +.sceditor-container { + border: 1px solid #d6d6d6; + border-radius: 0; + background-clip: padding-box; +} +.sceditor-container textarea { + font-family: Consolas, "Bitstream Vera Sans Mono", "Andale Mono", Monaco, "DejaVu Sans Mono", "Lucida Console", monospace; + background: #2e3436; + color: #fff; + margin: 0; + padding: 5px; +} +div.sceditor-toolbar, +div.sceditor-group { + background: #f2f2f2; + background: linear-gradient(to bottom, #f2f2f2 0%, #dddddd 89%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#f2f2f2', endColorstr='#dddddd', GradientType=0); +} +div.sceditor-toolbar { + padding: 0; + border-bottom: 1px solid #bbb; + background-size: 100% 32px; +} +div.sceditor-group { + margin: 0; + padding: 2px 4px; + border: 0; + border-right: 1px solid #ccc; + border-left: 1px solid #eaeaea; + border-radius: 0; + background-clip: padding-box; +} +div.sceditor-group:last-child { + border-right: 0; +} +div.sceditor-group:first-child { + border-left: 0; +} +.sceditor-button { + height: 16px; + padding: 5px; + margin: 1px; + border-radius: 0; + background-clip: padding-box; +} +.sceditor-button div { + margin: 0; +} +.sceditor-button.active, +.sceditor-button:hover, +.sceditor-button:active, +.sceditor-button.active:hover { + margin: 0; + box-shadow: none; +} +.sceditor-button.active { + background: #f4f4f4; + border: 1px solid #ccc; +} +.sceditor-button:hover { + background: #fefefe; + border: 1px solid #ddd; +} +.sceditor-button.disabled:hover { + margin: 1px; + border: 0; +} +.sceditor-button:active { + background: #eee; + border: 1px solid #ccc; +} +.sceditor-button.active:hover { + background: #f8f8f8; + border: 1px solid #ddd; +} -- cgit v1.3