or
or
+ * if it has a class, style attribute or data.
+ *
+ * @param {HTMLElement} elm
+ * @return {boolean}
+ * @since 1.4.4
+ */
+ function hasStyling(node) {
+ return node && (!is(node, 'p,div') || node.className ||
+ attr(node, 'style') || !isEmptyObject(data(node)));
+ }
+
+ /**
+ * Converts an element from one type to another.
+ *
+ * For example it can convert the element to
+ *
+ * @param {HTMLElement} element
+ * @param {string} toTagName
+ * @return {HTMLElement}
+ * @since 1.4.4
+ */
+ function convertElement(element, toTagName) {
+ var newElement = createElement(toTagName, {}, element.ownerDocument);
+
+ each(element.attributes, function (_, attribute) {
+ // Some browsers parse invalid attributes names like
+ // 'size"2' which throw an exception when set, just
+ // ignore these.
+ try {
+ attr(newElement, attribute.name, attribute.value);
+ } catch (ex) {}
+ });
+
+ while (element.firstChild) {
+ appendChild(newElement, element.firstChild);
+ }
+
+ element.parentNode.replaceChild(newElement, element);
+
+ return newElement;
+ }
+
+ /**
+ * List of block level elements separated by bars (|)
+ *
+ * @type {string}
+ */
+ var blockLevelList = '|body|hr|p|div|h1|h2|h3|h4|h5|h6|address|pre|' +
+ 'form|table|tbody|thead|tfoot|th|tr|td|li|ol|ul|blockquote|center|';
+
+ /**
+ * List of elements that do not allow children separated by bars (|)
+ *
+ * @param {Node} node
+ * @return {boolean}
+ * @since 1.4.5
+ */
+ function canHaveChildren(node) {
+ // 1 = Element
+ // 9 = Document
+ // 11 = Document Fragment
+ if (!/11?|9/.test(node.nodeType)) {
+ return false;
+ }
+
+ // List of empty HTML tags separated by bar (|) character.
+ // Source: http://www.w3.org/TR/html4/index/elements.html
+ // Source: http://www.w3.org/TR/html5/syntax.html#void-elements
+ return ('|iframe|area|base|basefont|br|col|frame|hr|img|input|wbr' +
+ '|isindex|link|meta|param|command|embed|keygen|source|track|' +
+ 'object|').indexOf('|' + node.nodeName.toLowerCase() + '|') < 0;
+ }
+
+ /**
+ * Checks if an element is inline
+ *
+ * @param {HTMLElement} elm
+ * @param {boolean} [includeCodeAsBlock=false]
+ * @return {boolean}
+ */
+ function isInline(elm, includeCodeAsBlock) {
+ var tagName,
+ nodeType = (elm || {}).nodeType || TEXT_NODE;
+
+ if (nodeType !== ELEMENT_NODE) {
+ return nodeType === TEXT_NODE;
+ }
+
+ tagName = elm.tagName.toLowerCase();
+
+ if (tagName === 'code') {
+ return !includeCodeAsBlock;
+ }
+
+ return blockLevelList.indexOf('|' + tagName + '|') < 0;
+ }
+
+ /**
+ * Copy the CSS from 1 node to another.
+ *
+ * Only copies CSS defined on the element e.g. style attr.
+ *
+ * @param {HTMLElement} from
+ * @param {HTMLElement} to
+ */
+ function copyCSS(from, to) {
+ to.style.cssText = from.style.cssText + to.style.cssText;
+ }
+
+ /**
+ * Fixes block level elements inside in inline elements.
+ *
+ * Also fixes invalid list nesting by placing nested lists
+ * inside the previous li tag or wrapping them in an li tag.
+ *
+ * @param {HTMLElement} node
+ */
+ function fixNesting(node) {
+ var getLastInlineParent = function (node) {
+ while (isInline(node.parentNode, true)) {
+ node = node.parentNode;
+ }
+
+ return node;
+ };
+
+ traverse(node, function (node) {
+ var list = 'ul,ol',
+ isBlock = !isInline(node, true);
+
+ // Any blocklevel element inside an inline element needs fixing.
+ if (isBlock && isInline(node.parentNode, true)) {
+ var parent = getLastInlineParent(node),
+ before = extractContents(parent, node),
+ middle = node;
+
+ // copy current styling so when moved out of the parent
+ // it still has the same styling
+ copyCSS(parent, middle);
+
+ insertBefore(before, parent);
+ insertBefore(middle, parent);
+ }
+
+ // Fix invalid nested lists which should be wrapped in an li tag
+ if (isBlock && is(node, list) && is(node.parentNode, list)) {
+ var li = previousElementSibling(node, 'li');
+
+ if (!li) {
+ li = createElement('li');
+ insertBefore(li, node);
+ }
+
+ appendChild(li, node);
+ }
+ });
+ }
+
+ /**
+ * Finds the common parent of two nodes
+ *
+ * @param {!HTMLElement} node1
+ * @param {!HTMLElement} node2
+ * @return {?HTMLElement}
+ */
+ function findCommonAncestor(node1, node2) {
+ while ((node1 = node1.parentNode)) {
+ if (contains(node1, node2)) {
+ return node1;
+ }
+ }
+ }
+
+ /**
+ * @param {?Node}
+ * @param {boolean} [previous=false]
+ * @returns {?Node}
+ */
+ function getSibling(node, previous) {
+ if (!node) {
+ return null;
+ }
+
+ return (previous ? node.previousSibling : node.nextSibling) ||
+ getSibling(node.parentNode, previous);
+ }
+
+ /**
+ * Removes unused whitespace from the root and all it's children.
+ *
+ * @param {!HTMLElement} root
+ * @since 1.4.3
+ */
+ function removeWhiteSpace(root) {
+ var nodeValue, nodeType, next, previous, previousSibling,
+ nextNode, trimStart,
+ cssWhiteSpace = css(root, 'whiteSpace'),
+ // Preserve newlines if is pre-line
+ preserveNewLines = /line$/i.test(cssWhiteSpace),
+ node = root.firstChild;
+
+ // Skip pre & pre-wrap with any vendor prefix
+ if (/pre(\-wrap)?$/i.test(cssWhiteSpace)) {
+ return;
+ }
+
+ while (node) {
+ nextNode = node.nextSibling;
+ nodeValue = node.nodeValue;
+ nodeType = node.nodeType;
+
+ if (nodeType === ELEMENT_NODE && node.firstChild) {
+ removeWhiteSpace(node);
+ }
+
+ if (nodeType === TEXT_NODE) {
+ next = getSibling(node);
+ previous = getSibling(node, true);
+ trimStart = false;
+
+ while (hasClass(previous, 'sceditor-ignore')) {
+ previous = getSibling(previous, true);
+ }
+
+ // If previous sibling isn't inline or is a textnode that
+ // ends in whitespace, time the start whitespace
+ if (isInline(node) && previous) {
+ previousSibling = previous;
+
+ while (previousSibling.lastChild) {
+ previousSibling = previousSibling.lastChild;
+
+ // eslint-disable-next-line max-depth
+ while (hasClass(previousSibling, 'sceditor-ignore')) {
+ previousSibling = getSibling(previousSibling, true);
+ }
+ }
+
+ trimStart = previousSibling.nodeType === TEXT_NODE ?
+ /[\t\n\r ]$/.test(previousSibling.nodeValue) :
+ !isInline(previousSibling);
+ }
+
+ // Clear zero width spaces
+ nodeValue = nodeValue.replace(/\u200B/g, '');
+
+ // Strip leading whitespace
+ if (!previous || !isInline(previous) || trimStart) {
+ nodeValue = nodeValue.replace(
+ preserveNewLines ? /^[\t ]+/ : /^[\t\n\r ]+/,
+ ''
+ );
+ }
+
+ // Strip trailing whitespace
+ if (!next || !isInline(next)) {
+ nodeValue = nodeValue.replace(
+ preserveNewLines ? /[\t ]+$/ : /[\t\n\r ]+$/,
+ ''
+ );
+ }
+
+ // Remove empty text nodes
+ if (!nodeValue.length) {
+ remove(node);
+ } else {
+ node.nodeValue = nodeValue.replace(
+ preserveNewLines ? /[\t ]+/g : /[\t\n\r ]+/g,
+ ' '
+ );
+ }
+ }
+
+ node = nextNode;
+ }
+ }
+
+ /**
+ * Extracts all the nodes between the start and end nodes
+ *
+ * @param {HTMLElement} startNode The node to start extracting at
+ * @param {HTMLElement} endNode The node to stop extracting at
+ * @return {DocumentFragment}
+ */
+ function extractContents(startNode, endNode) {
+ var range = startNode.ownerDocument.createRange();
+
+ range.setStartBefore(startNode);
+ range.setEndAfter(endNode);
+
+ return range.extractContents();
+ }
+
+ /**
+ * Gets the offset position of an element
+ *
+ * @param {HTMLElement} node
+ * @return {Object} An object with left and top properties
+ */
+ function getOffset(node) {
+ var left = 0,
+ top = 0;
+
+ while (node) {
+ left += node.offsetLeft;
+ top += node.offsetTop;
+ node = node.offsetParent;
+ }
+
+ return {
+ left: left,
+ top: top
+ };
+ }
+
+ /**
+ * Gets the value of a CSS property from the elements style attribute
+ *
+ * @param {HTMLElement} elm
+ * @param {string} property
+ * @return {string}
+ */
+ function getStyle(elm, property) {
+ var direction, styleValue,
+ elmStyle = elm.style;
+
+ if (!cssPropertyNameCache[property]) {
+ cssPropertyNameCache[property] = camelCase(property);
+ }
+
+ property = cssPropertyNameCache[property];
+ styleValue = elmStyle[property];
+
+ // Add an exception for text-align
+ if ('textAlign' === property) {
+ direction = elmStyle.direction;
+ styleValue = styleValue || css(elm, property);
+
+ if (css(elm.parentNode, property) === styleValue ||
+ css(elm, 'display') !== 'block' || is(elm, 'hr,th')) {
+ return '';
+ }
+
+ // IE changes text-align to the same as the current direction
+ // so skip unless its not the same
+ if ((/right/i.test(styleValue) && direction === 'rtl') ||
+ (/left/i.test(styleValue) && direction === 'ltr')) {
+ return '';
+ }
+ }
+
+ return styleValue;
+ }
+
+ /**
+ * Tests if an element has a style.
+ *
+ * If values are specified it will check that the styles value
+ * matches one of the values
+ *
+ * @param {HTMLElement} elm
+ * @param {string} property
+ * @param {string|array} [values]
+ * @return {boolean}
+ */
+ function hasStyle(elm, property, values) {
+ var styleValue = getStyle(elm, property);
+
+ if (!styleValue) {
+ return false;
+ }
+
+ return !values || styleValue === values ||
+ (Array.isArray(values) && values.indexOf(styleValue) > -1);
+ }
+
+ /**
+ * Default options for SCEditor
+ * @type {Object}
+ */
+ var defaultOptions = {
+ /** @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,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: '#000000,#44B8FF,#1E92F7,#0074D9,#005DC2,#00369B,#b3d5f4|' +
+ '#444444,#C3FFFF,#9DF9FF,#7FDBFF,#68C4E8,#419DC1,#d9f4ff|' +
+ '#666666,#72FF84,#4CEA5E,#2ECC40,#17B529,#008E02,#c0f0c6|' +
+ '#888888,#FFFF44,#FFFA1E,#FFDC00,#E8C500,#C19E00,#fff5b3|' +
+ '#aaaaaa,#FFC95F,#FFA339,#FF851B,#E86E04,#C14700,#ffdbbb|' +
+ '#cccccc,#FF857A,#FF5F54,#FF4136,#E82A1F,#C10300,#ffc6c3|' +
+ '#eeeeee,#FF56FF,#FF30DC,#F012BE,#D900A7,#B20080,#fbb8ec|' +
+ '#ffffff,#F551FF,#CF2BE7,#B10DC9,#9A00B2,#9A00B2,#e8b6ef',
+
+ /**
+ * The locale to use.
+ * @type {string}
+ */
+ locale: attr(document.documentElement, '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 {?number}
+ */
+ width: null,
+
+ /**
+ * Height of the editor including toolbar. Set to null for automatic
+ * height
+ *
+ * @type {?number}
+ */
+ 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 {?number}
+ */
+ resizeMinWidth: null,
+ /**
+ * Min resize to height, set to null for half textarea height or -1 for
+ * unlimited
+ *
+ * @type {?number}
+ */
+ resizeMinHeight: null,
+ /**
+ * Max resize to height, set to null for double textarea height or -1
+ * for unlimited
+ *
+ * @type {?number}
+ */
+ resizeMaxHeight: null,
+ /**
+ * Max resize to width, set to null for double textarea width or -1 for
+ * unlimited
+ *
+ * @type {?number}
+ */
+ 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 {?number}
+ */
+ 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: { }
+ };
+
+ var USER_AGENT = navigator.userAgent;
+
+ /**
+ * Detects the version of IE is being used if any.
+ *
+ * Will be the IE version number or undefined if the
+ * browser is not IE.
+ *
+ * Source: https://gist.github.com/527683 with extra code
+ * for IE 10 & 11 detection.
+ *
+ * @function
+ * @name ie
+ * @type {number}
+ */
+ var ie = (function () {
+ var undef,
+ v = 3,
+ doc = document,
+ div = doc.createElement('div'),
+ all = div.getElementsByTagName('i');
+
+ do {
+ div.innerHTML = '';
+ } while (all[0]);
+
+ // Detect IE 10 as it doesn't support conditional comments.
+ if ((doc.documentMode && doc.all && window.atob)) {
+ v = 10;
+ }
+
+ // Detect IE 11
+ if (v === 4 && doc.documentMode) {
+ v = 11;
+ }
+
+ return v > 4 ? v : undef;
+ }());
+
+ var edge = '-ms-ime-align' in document.documentElement.style;
+
+ /**
+ * Detects if the browser is iOS
+ *
+ * Needed to fix iOS specific bugs
+ *
+ * @function
+ * @name ios
+ * @memberOf jQuery.sceditor
+ * @type {boolean}
+ */
+ var ios = /iPhone|iPod|iPad| wosbrowser\//i.test(USER_AGENT);
+
+ /**
+ * If the browser supports WYSIWYG editing (e.g. older mobile browsers).
+ *
+ * @function
+ * @name isWysiwygSupported
+ * @return {boolean}
+ */
+ var isWysiwygSupported = (function () {
+ var match, isUnsupported;
+
+ var div = document.createElement('div');
+ div.contentEditable = true ;
+
+ // Check if the contentEditable attribute is supported
+ if (!('contentEditable' in document.documentElement) ||
+ div.contentEditable !== 'true') {
+ return false;
+ }
+
+ // I think blackberry supports contentEditable or will at least
+ // give a valid value for the contentEditable detection above
+ // so it isn't included in the below tests.
+
+ // I hate having to do UA sniffing but some mobile browsers say they
+ // support contentediable when it isn't usable, i.e. you can't enter
+ // text.
+ // This is the only way I can think of to detect them which is also how
+ // every other editor I've seen deals with this issue.
+
+ // Exclude Opera mobile and mini
+ isUnsupported = /Opera Mobi|Opera Mini/i.test(USER_AGENT);
+
+ if (/Android/i.test(USER_AGENT)) {
+ isUnsupported = true;
+
+ if (/Safari/.test(USER_AGENT)) {
+ // Android browser 534+ supports content editable
+ // This also matches Chrome which supports content editable too
+ match = /Safari\/(\d+)/.exec(USER_AGENT);
+ isUnsupported = (!match || !match[1] ? true : match[1] < 534);
+ }
+ }
+
+ // The current version of Amazon Silk supports it, older versions didn't
+ // As it uses webkit like Android, assume it's the same and started
+ // working at versions >= 534
+ if (/ Silk\//i.test(USER_AGENT)) {
+ match = /AppleWebKit\/(\d+)/.exec(USER_AGENT);
+ isUnsupported = (!match || !match[1] ? true : match[1] < 534);
+ }
+
+ // iOS 5+ supports content editable
+ if (ios) {
+ // Block any version <= 4_x(_x)
+ isUnsupported = /OS [0-4](_\d)+ like Mac/i.test(USER_AGENT);
+ }
+
+ // Firefox does support WYSIWYG on mobiles so override
+ // any previous value if using FF
+ if (/Firefox/i.test(USER_AGENT)) {
+ isUnsupported = false;
+ }
+
+ if (/OneBrowser/i.test(USER_AGENT)) {
+ isUnsupported = false;
+ }
+
+ // UCBrowser works but doesn't give a unique user agent
+ if (navigator.vendor === 'UCWEB') {
+ isUnsupported = false;
+ }
+
+ // IE <= 9 is not supported any more
+ if (ie <= 9) {
+ isUnsupported = true;
+ }
+
+ return !isUnsupported;
+ }());
+
+ // Must start with a valid scheme
+ // ^
+ // Schemes that are considered safe
+ // (https?|s?ftp|mailto|spotify|skype|ssh|teamspeak|tel):|
+ // Relative schemes (//:) are considered safe
+ // (\\/\\/)|
+ // Image data URI's are considered safe
+ // data:image\\/(png|bmp|gif|p?jpe?g);
+ var VALID_SCHEME_REGEX =
+ /^(https?|s?ftp|mailto|spotify|skype|ssh|teamspeak|tel):|(\/\/)|data:image\/(png|bmp|gif|p?jpe?g);/i;
+
+ /**
+ * Escapes a string so it's safe to use in regex
+ *
+ * @param {string} str
+ * @return {string}
+ */
+ function regex(str) {
+ return str.replace(/([\-.*+?^=!:${}()|\[\]\/\\])/g, '\\$1');
+ }
+
+ /**
+ * Escapes all HTML entities in a string
+ *
+ * If noQuotes is set to false, all single and double
+ * quotes will also be escaped
+ *
+ * @param {string} str
+ * @param {boolean} [noQuotes=true]
+ * @return {string}
+ * @since 1.4.1
+ */
+ function entities(str, noQuotes) {
+ if (!str) {
+ return str;
+ }
+
+ var replacements = {
+ '&': '&',
+ '<': '<',
+ '>': '>',
+ ' ': ' ',
+ '\r\n': ' ',
+ '\r': ' ',
+ '\n': ' '
+ };
+
+ if (noQuotes !== false) {
+ replacements['"'] = '"';
+ replacements['\''] = ''';
+ replacements['`'] = '`';
+ }
+
+ str = str.replace(/ {2}|\r\n|[&<>\r\n'"`]/g, function (match) {
+ return replacements[match] || match;
+ });
+
+ return str;
+ }
+
+ /**
+ * Escape URI scheme.
+ *
+ * Appends the current URL to a url if it has a scheme that is not:
+ *
+ * http
+ * https
+ * sftp
+ * ftp
+ * mailto
+ * spotify
+ * skype
+ * ssh
+ * teamspeak
+ * tel
+ * //
+ * data:image/(png|jpeg|jpg|pjpeg|bmp|gif);
+ *
+ * **IMPORTANT**: This does not escape any HTML in a url, for
+ * that use the escape.entities() method.
+ *
+ * @param {string} url
+ * @return {string}
+ * @since 1.4.5
+ */
+ function uriScheme(url) {
+ var path,
+ // If there is a : before a / then it has a scheme
+ hasScheme = /^[^\/]*:/i,
+ location = window.location;
+
+ // Has no scheme or a valid scheme
+ if ((!url || !hasScheme.test(url)) || VALID_SCHEME_REGEX.test(url)) {
+ return url;
+ }
+
+ path = location.pathname.split('/');
+ path.pop();
+
+ return location.protocol + '//' +
+ location.host +
+ path.join('/') + '/' +
+ url;
+ }
+
+ /**
+ * HTML templates used by the editor and default commands
+ * @type {Object}
+ * @private
+ */
+ var _templates = {
+ html:
+ '' +
+ '' +
+ '' +
+ '' +
+ ' ' +
+ ' ' +
+ '' +
+ '
' +
+ '',
+
+ toolbarButton: '' +
+ '{dispName}
',
+
+ emoticon: ' ',
+
+ fontOpt: '{font} ',
+
+ sizeOpt: '{size} ',
+
+ pastetext:
+ '{label} ' +
+ '
' +
+ ' ' +
+ '
',
+
+ table:
+ '{rows}
' +
+ '{cols}
' +
+ '
',
+
+ image:
+ '{url} ' +
+ '
' +
+ '{width} ' +
+ '
' +
+ '{height} ' +
+ '
' +
+ ' ' +
+ '
',
+
+ email:
+ '{label} ' +
+ '
' +
+ '{desc} ' +
+ '
' +
+ ' ' +
+ '
',
+
+ link:
+ '{url} ' +
+ '
' +
+ '{desc} ' +
+ '
' +
+ '
',
+
+ youtubeMenu:
+ '{label} ' +
+ '
' +
+ ' ' +
+ '
',
+
+ youtube:
+ ''
+ };
+
+ /**
+ * Replaces any params in a template with the passed params.
+ *
+ * If createHtml is passed it will return a DocumentFragment
+ * containing the parsed template.
+ *
+ * @param {string} name
+ * @param {Object} [params]
+ * @param {boolean} [createHtml]
+ * @returns {string|DocumentFragment}
+ * @private
+ */
+ function _tmpl (name, params, createHtml) {
+ var template = _templates[name];
+
+ Object.keys(params).forEach(function (name) {
+ template = template.replace(
+ new RegExp(regex('{' + name + '}'), 'g'), params[name]
+ );
+ });
+
+ if (createHtml) {
+ template = parseHTML(template);
+ }
+
+ return template;
+ }
+
+ // 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 && ie < 11;
+
+ /**
+ * Fixes a bug in FF where it sometimes wraps
+ * new lines in their own list item.
+ * See issue #359
+ */
+ function fixFirefoxListBug(editor) {
+ // Only apply to Firefox as will break other browsers.
+ if ('mozHidden' in document) {
+ var node = editor.getBody();
+ var next;
+
+ while (node) {
+ next = node;
+
+ if (next.firstChild) {
+ next = next.firstChild;
+ } else {
+
+ while (next && !next.nextSibling) {
+ next = next.parentNode;
+ }
+
+ if (next) {
+ next = next.nextSibling;
+ }
+ }
+
+ if (node.nodeType === 3 && /[\n\r\t]+/.test(node.nodeValue)) {
+ // Only remove if newlines are collapsed
+ if (!/^pre/.test(css(node.parentNode, 'whiteSpace'))) {
+ remove(node);
+ }
+ }
+
+ node = next;
+ }
+ }
+ }
+
+
+ /**
+ * Map of all the commands for SCEditor
+ * @type {Object}
+ * @name commands
+ * @memberOf jQuery.sceditor
+ */
+ var defaultCmds = {
+ // 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: {
+ state: function (node) {
+ if (node && node.nodeType === 3) {
+ node = node.parentNode;
+ }
+
+ if (node) {
+ var isLtr = css(node, 'direction') === 'ltr';
+ var align = css(node, 'textAlign');
+
+ return align === 'left' || align === (isLtr ? 'start' : 'end');
+ }
+ },
+ exec: 'justifyleft',
+ tooltip: 'Align left'
+ },
+ // END_COMMAND
+ // START_COMMAND: Centre
+ center: {
+ exec: 'justifycenter',
+ tooltip: 'Center'
+ },
+ // END_COMMAND
+ // START_COMMAND: Right
+ right: {
+ state: function (node) {
+ if (node && node.nodeType === 3) {
+ node = node.parentNode;
+ }
+
+ if (node) {
+ var isLtr = css(node, 'direction') === 'ltr';
+ var align = css(node, 'textAlign');
+
+ return align === 'right' || align === (isLtr ? 'end' : 'start');
+ }
+ },
+ 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 content = createElement('div');
+
+ on(content, 'click', 'a', function (e) {
+ callback(data(this, 'font'));
+ editor.closeDropDown(true);
+ e.preventDefault();
+ });
+
+ editor.opts.fonts.split(',').forEach(function (font) {
+ appendChild(content, _tmpl('fontOpt', {
+ font: font
+ }, true));
+ });
+
+ editor.createDropDown(caller, 'font-picker', content);
+ },
+ exec: function (caller) {
+ var editor = this;
+
+ defaultCmds.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 = createElement('div');
+
+ on(content, 'click', 'a', function (e) {
+ callback(data(this, 'size'));
+ editor.closeDropDown(true);
+ e.preventDefault();
+ });
+
+ for (var i = 1; i <= 7; i++) {
+ appendChild(content, _tmpl('sizeOpt', {
+ size: i
+ }, true));
+ }
+
+ editor.createDropDown(caller, 'fontsize-picker', content);
+ },
+ exec: function (caller) {
+ var editor = this;
+
+ defaultCmds.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 content = createElement('div'),
+ html = '',
+ cmd = defaultCmds.color;
+
+ if (!cmd._htmlCache) {
+ editor.opts.colors.split('|').forEach(function (column) {
+ html += '';
+
+ column.split(',').forEach(function (color) {
+ html +=
+ '
';
+ });
+
+ html += '
';
+ });
+
+ cmd._htmlCache = html;
+ }
+
+ appendChild(content, parseHTML(cmd._htmlCache));
+
+ on(content, 'click', 'a', function (e) {
+ callback(data(this, 'color'));
+ editor.closeDropDown(true);
+ e.preventDefault();
+ });
+
+ editor.createDropDown(caller, 'color-picker', content);
+ },
+ exec: function (caller) {
+ var editor = this;
+
+ defaultCmds.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 = createElement('div'),
+ editor = this;
+
+ appendChild(content, _tmpl('pastetext', {
+ label: editor._(
+ 'Paste your text inside the following box:'
+ ),
+ insert: editor._('Insert')
+ }, true));
+
+ on(content, 'click', '.button', function (e) {
+ val = find(content, '#txt')[0].value;
+
+ 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: function () {
+ fixFirefoxListBug(this);
+ this.execCommand('insertunorderedlist');
+ },
+ tooltip: 'Bullet list'
+ },
+ // END_COMMAND
+ // START_COMMAND: Ordered List
+ orderedlist: {
+ exec: function () {
+ fixFirefoxListBug(this);
+ this.execCommand('insertorderedlist');
+ },
+ tooltip: 'Numbered list'
+ },
+ // END_COMMAND
+ // START_COMMAND: Indent
+ indent: {
+ state: function (parent$$1, firstBlock) {
+ // Only works with lists, for now
+ var range, startParent, endParent;
+
+ if (is(firstBlock, 'li')) {
+ return 0;
+ }
+
+ if (is(firstBlock, '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();
+
+ 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
+ (is(endParent, 'li') && endParent !==
+ endParent.parentNode.lastElementChild)) {
+ return 0;
+ }
+ }
+
+ return -1;
+ },
+ exec: function () {
+ var editor = this,
+ block = 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 (closest(block, 'ul,ol,menu')) {
+ editor.execCommand('indent');
+ }
+ },
+ tooltip: 'Add indent'
+ },
+ // END_COMMAND
+ // START_COMMAND: Outdent
+ outdent: {
+ state: function (parents$$1, firstBlock) {
+ return closest(firstBlock, 'ul,ol,menu') ? 0 : -1;
+ },
+ exec: function () {
+ var block = this.getRangeHelper().getFirstBlockParent();
+ if (closest(block, 'ul,ol,menu')) {
+ this.execCommand('outdent');
+ }
+ },
+ tooltip: 'Remove one indent'
+ },
+ // END_COMMAND
+
+ // START_COMMAND: Table
+ table: {
+ exec: function (caller) {
+ var editor = this,
+ content = createElement('div');
+
+ appendChild(content, _tmpl('table', {
+ rows: editor._('Rows:'),
+ cols: editor._('Cols:'),
+ insert: editor._('Insert')
+ }, true));
+
+ on(content, 'click', '.button', function (e) {
+ var rows = Number(find(content, '#rows')[0].value),
+ cols = Number(find(content, '#cols')[0].value),
+ html = '';
+
+ if (rows > 0 && cols > 0) {
+ html += Array(rows + 1).join(
+ '' +
+ Array(cols + 1).join(
+ '' + (IE_BR_FIX ? '' : ' ') + ' '
+ ) +
+ ' '
+ );
+
+ html += '
';
+
+ editor.wysiwygEditorInsertHtml(html);
+ editor.closeDropDown(true);
+ e.preventDefault();
+ }
+ });
+
+ editor.createDropDown(caller, 'inserttable', content);
+ },
+ tooltip: 'Insert a table'
+ },
+ // END_COMMAND
+
+ // START_COMMAND: Horizontal Rule
+ horizontalrule: {
+ exec: 'inserthorizontalrule',
+ tooltip: 'Insert a horizontal rule'
+ },
+ // END_COMMAND
+
+ // START_COMMAND: Code
+ code: {
+ exec: function () {
+ this.wysiwygEditorInsertHtml(
+ '',
+ (IE_BR_FIX ? '' : ' ') + '
'
+ );
+ },
+ tooltip: 'Code'
+ },
+ // END_COMMAND
+
+ // START_COMMAND: Image
+ image: {
+ _dropDown: function (editor, caller, selected, cb) {
+ var content = createElement('div');
+
+ appendChild(content, _tmpl('image', {
+ url: editor._('URL:'),
+ width: editor._('Width (optional):'),
+ height: editor._('Height (optional):'),
+ insert: editor._('Insert')
+ }, true));
+
+
+ var urlInput = find(content, '#image')[0];
+
+ urlInput.value = selected;
+
+ on(content, 'click', '.button', function (e) {
+ if (urlInput.value) {
+ cb(
+ urlInput.value,
+ find(content, '#width')[0].value,
+ find(content, '#height')[0].value
+ );
+ }
+
+ editor.closeDropDown(true);
+ e.preventDefault();
+ });
+
+ editor.createDropDown(caller, 'insertimage', content);
+ },
+ exec: function (caller) {
+ var editor = this;
+
+ defaultCmds.image._dropDown(
+ editor,
+ caller,
+ '',
+ function (url, width$$1, height$$1) {
+ var attrs = '';
+
+ if (width$$1) {
+ attrs += ' width="' + width$$1 + '"';
+ }
+
+ if (height$$1) {
+ attrs += ' height="' + height$$1 + '"';
+ }
+
+ editor.wysiwygEditorInsertHtml(
+ ' '
+ );
+ }
+ );
+ },
+ tooltip: 'Insert an image'
+ },
+ // END_COMMAND
+
+ // START_COMMAND: E-mail
+ email: {
+ _dropDown: function (editor, caller, cb) {
+ var content = createElement('div');
+
+ appendChild(content, _tmpl('email', {
+ label: editor._('E-mail:'),
+ desc: editor._('Description (optional):'),
+ insert: editor._('Insert')
+ }, true));
+
+ on(content, 'click', '.button', function (e) {
+ var email = find(content, '#email')[0].value;
+
+ if (email) {
+ cb(email, find(content, '#des')[0].value);
+ }
+
+ editor.closeDropDown(true);
+ e.preventDefault();
+ });
+
+ editor.createDropDown(caller, 'insertemail', content);
+ },
+ exec: function (caller) {
+ var editor = this;
+
+ defaultCmds.email._dropDown(
+ editor,
+ caller,
+ function (email, text) {
+ // needed for IE to reset the last range
+ editor.focus();
+
+ if (!editor.getRangeHelper().selectedHtml() || text) {
+ editor.wysiwygEditorInsertHtml(
+ '' +
+ (text || email) +
+ ' '
+ );
+ } else {
+ editor.execCommand('createlink', 'mailto:' + email);
+ }
+ }
+ );
+ },
+ tooltip: 'Insert an email'
+ },
+ // END_COMMAND
+
+ // START_COMMAND: Link
+ link: {
+ _dropDown: function (editor, caller, cb) {
+ var content = createElement('div');
+
+ appendChild(content, _tmpl('link', {
+ url: editor._('URL:'),
+ desc: editor._('Description (optional):'),
+ ins: editor._('Insert')
+ }, true));
+
+ var linkInput = find(content, '#link')[0];
+
+ function insertUrl(e) {
+ if (linkInput.value) {
+ cb(linkInput.value, find(content, '#des')[0].value);
+ }
+
+ editor.closeDropDown(true);
+ e.preventDefault();
+ }
+
+ on(content, 'click', '.button', insertUrl);
+ on(content, 'keypress', function (e) {
+ // 13 = enter key
+ if (e.which === 13 && linkInput.value) {
+ insertUrl(e);
+ }
+ }, EVENT_CAPTURE);
+
+ editor.createDropDown(caller, 'insertlink', content);
+ },
+ exec: function (caller) {
+ var editor = this;
+
+ defaultCmds.link._dropDown(editor, caller, function (url, text) {
+ // 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 (text || !editor.getRangeHelper().selectedHtml()) {
+ text = text || url;
+
+ editor.wysiwygEditorInsertHtml(
+ '' + text + ' '
+ );
+ } else {
+ editor.execCommand('createlink', url);
+ }
+ });
+ },
+ tooltip: 'Insert a link'
+ },
+ // END_COMMAND
+
+ // START_COMMAND: Unlink
+ unlink: {
+ state: function () {
+ return closest(this.currentNode(), 'a') ? 0 : -1;
+ },
+ exec: function () {
+ var anchor = closest(this.currentNode(), 'a');
+
+ if (anchor) {
+ while (anchor.firstChild) {
+ insertBefore(anchor.firstChild, anchor);
+ }
+
+ remove(anchor);
+ }
+ },
+ tooltip: 'Unlink'
+ },
+ // END_COMMAND
+
+
+ // START_COMMAND: Quote
+ quote: {
+ 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,
+ opts = editor.opts,
+ emoticonsRoot = opts.emoticonsRoot || '',
+ emoticonsCompat = opts.emoticonsCompat,
+ rangeHelper = editor.getRangeHelper(),
+ startSpace = emoticonsCompat &&
+ rangeHelper.getOuterText(true, 1) !== ' ' ? ' ' : '',
+ endSpace = emoticonsCompat &&
+ rangeHelper.getOuterText(false, 1) !== ' ' ? ' ' : '',
+ content = createElement('div'),
+ line = createElement('div'),
+ perLine = 0,
+ emoticons = extend(
+ {},
+ opts.emoticons.dropdown,
+ includeMore ? opts.emoticons.more : {}
+ );
+
+ appendChild(content, line);
+
+ perLine = Math.sqrt(Object.keys(emoticons).length);
+
+ on(content, 'click', 'img', function (e) {
+ editor.insert(startSpace + attr(this, 'alt') + endSpace,
+ null, false).closeDropDown(true);
+
+ e.preventDefault();
+ });
+
+ each(emoticons, function (code, emoticon) {
+ appendChild(line, createElement('img', {
+ src: emoticonsRoot + (emoticon.url || emoticon),
+ alt: code,
+ title: emoticon.tooltip || code
+ }));
+
+ if (line.children.length >= perLine) {
+ line = createElement('div');
+ appendChild(content, line);
+ }
+ });
+
+ if (!includeMore && opts.emoticons.more) {
+ moreLink = createElement('a', {
+ className: 'sceditor-more'
+ });
+
+ appendChild(moreLink,
+ document.createTextNode(editor._('More')));
+
+ on(moreLink, 'click', function (e) {
+ editor.createDropDown(
+ caller, 'more-emoticons', createContent(true)
+ );
+
+ e.preventDefault();
+ });
+
+ appendChild(content, moreLink);
+ }
+
+ return content;
+ };
+
+ editor.createDropDown(caller, 'emoticons', createContent(false));
+ },
+ txtExec: function (caller) {
+ defaultCmds.emoticon.exec.call(this, caller);
+ },
+ tooltip: 'Insert an emoticon'
+ },
+ // END_COMMAND
+
+ // START_COMMAND: YouTube
+ youtube: {
+ _dropDown: function (editor, caller, callback) {
+ var content = createElement('div');
+
+ appendChild(content, _tmpl('youtubeMenu', {
+ label: editor._('Video URL:'),
+ insert: editor._('Insert')
+ }, true));
+
+ on(content, 'click', '.button', function (e) {
+ var val = find(content, '#link')[0].value;
+ var idMatch = val.match(/(?:v=|v\/|embed\/|youtu.be\/)(.{11})/);
+ var timeMatch = val.match(/[&|?](?:star)?t=((\d+[hms]?){1,3})/);
+ var time = 0;
+
+ if (timeMatch) {
+ each(timeMatch[1].split(/[hms]/), function (i, val) {
+ if (val !== '') {
+ time = (time * 60) + Number(val);
+ }
+ });
+ }
+
+ if (idMatch && /^[a-zA-Z0-9_\-]{11}$/.test(idMatch[1])) {
+ callback(idMatch[1], time);
+ }
+
+ editor.closeDropDown(true);
+ e.preventDefault();
+ });
+
+ editor.createDropDown(caller, 'insertlink', content);
+ },
+ exec: function (btn) {
+ var editor = this;
+
+ defaultCmds.youtube._dropDown(editor, btn, function (id, time) {
+ editor.wysiwygEditorInsertHtml(_tmpl('youtube', {
+ id: id,
+ time: time
+ }));
+ });
+ },
+ 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(defaultCmds.date._date(this));
+ },
+ txtExec: function () {
+ this.insertText(defaultCmds.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(defaultCmds.time._time());
+ },
+ txtExec: function () {
+ this.insertText(defaultCmds.time._time());
+ },
+ tooltip: 'Insert current time'
+ },
+ // END_COMMAND
+
+
+ // START_COMMAND: Ltr
+ ltr: {
+ state: function (parents$$1, firstBlock) {
+ return firstBlock && firstBlock.style.direction === 'ltr';
+ },
+ exec: function () {
+ var editor = this,
+ rangeHelper = editor.getRangeHelper(),
+ node = rangeHelper.getFirstBlockParent();
+
+ editor.focus();
+
+ if (!node || is(node, 'body')) {
+ editor.execCommand('formatBlock', 'p');
+
+ node = rangeHelper.getFirstBlockParent();
+
+ if (!node || is(node, 'body')) {
+ return;
+ }
+ }
+
+ var toggleValue = css(node, 'direction') === 'ltr' ? '' : 'ltr';
+ css(node, 'direction', toggleValue);
+ },
+ tooltip: 'Left-to-Right'
+ },
+ // END_COMMAND
+
+ // START_COMMAND: Rtl
+ rtl: {
+ state: function (parents$$1, firstBlock) {
+ return firstBlock && firstBlock.style.direction === 'rtl';
+ },
+ exec: function () {
+ var editor = this,
+ rangeHelper = editor.getRangeHelper(),
+ node = rangeHelper.getFirstBlockParent();
+
+ editor.focus();
+
+ if (!node || is(node, 'body')) {
+ editor.execCommand('formatBlock', 'p');
+
+ node = rangeHelper.getFirstBlockParent();
+
+ if (!node || is(node, 'body')) {
+ return;
+ }
+ }
+
+ var toggleValue = css(node, 'direction') === 'rtl' ? '' : 'rtl';
+ css(node, 'direction', toggleValue);
+ },
+ 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: {}
+ };
+
+ var plugins = {};
+
+ /**
+ * Plugin Manager class
+ * @class PluginManager
+ * @name PluginManager
+ */
+ function PluginManager(thisObj) {
+ /**
+ * Alias of this
+ *
+ * @private
+ * @type {Object}
+ */
+ var base = this;
+
+ /**
+ * Array of all currently registered plugins
+ *
+ * @type {Array}
+ * @private
+ */
+ var registeredPlugins = [];
+
+
+ /**
+ * Changes a signals name from "name" into "signalName".
+ *
+ * @param {string} signal
+ * @return {string}
+ * @private
+ */
+ var formatSignalName = function (signal) {
+ return 'signal' + signal.charAt(0).toUpperCase() + signal.slice(1);
+ };
+
+ /**
+ * Calls handlers for a signal
+ *
+ * @see call()
+ * @see callOnlyFirst()
+ * @param {Array} args
+ * @param {boolean} returnAtFirst
+ * @return {*}
+ * @private
+ */
+ var callHandlers = function (args, returnAtFirst) {
+ args = [].slice.call(args);
+
+ var idx, ret,
+ signal = formatSignalName(args.shift());
+
+ for (idx = 0; idx < registeredPlugins.length; idx++) {
+ if (signal in registeredPlugins[idx]) {
+ ret = registeredPlugins[idx][signal].apply(thisObj, args);
+
+ if (returnAtFirst) {
+ return ret;
+ }
+ }
+ }
+ };
+
+ /**
+ * Calls all handlers for the passed signal
+ *
+ * @param {string} signal
+ * @param {...string} args
+ * @function
+ * @name call
+ * @memberOf PluginManager.prototype
+ */
+ base.call = function () {
+ callHandlers(arguments, false);
+ };
+
+ /**
+ * Calls the first handler for a signal, and returns the
+ *
+ * @param {string} signal
+ * @param {...string} args
+ * @return {*} The result of calling the handler
+ * @function
+ * @name callOnlyFirst
+ * @memberOf PluginManager.prototype
+ */
+ base.callOnlyFirst = function () {
+ return callHandlers(arguments, true);
+ };
+
+ /**
+ * Checks if a signal has a handler
+ *
+ * @param {string} signal
+ * @return {boolean}
+ * @function
+ * @name hasHandler
+ * @memberOf PluginManager.prototype
+ */
+ base.hasHandler = function (signal) {
+ var i = registeredPlugins.length;
+ signal = formatSignalName(signal);
+
+ while (i--) {
+ if (signal in registeredPlugins[i]) {
+ return true;
+ }
+ }
+
+ return false;
+ };
+
+ /**
+ * Checks if the plugin exists in plugins
+ *
+ * @param {string} plugin
+ * @return {boolean}
+ * @function
+ * @name exists
+ * @memberOf PluginManager.prototype
+ */
+ base.exists = function (plugin) {
+ if (plugin in plugins) {
+ plugin = plugins[plugin];
+
+ return typeof plugin === 'function' &&
+ typeof plugin.prototype === 'object';
+ }
+
+ return false;
+ };
+
+ /**
+ * Checks if the passed plugin is currently registered.
+ *
+ * @param {string} plugin
+ * @return {boolean}
+ * @function
+ * @name isRegistered
+ * @memberOf PluginManager.prototype
+ */
+ base.isRegistered = function (plugin) {
+ if (base.exists(plugin)) {
+ var idx = registeredPlugins.length;
+
+ while (idx--) {
+ if (registeredPlugins[idx] instanceof plugins[plugin]) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ };
+
+ /**
+ * Registers a plugin to receive signals
+ *
+ * @param {string} plugin
+ * @return {boolean}
+ * @function
+ * @name register
+ * @memberOf PluginManager.prototype
+ */
+ base.register = function (plugin) {
+ if (!base.exists(plugin) || base.isRegistered(plugin)) {
+ return false;
+ }
+
+ plugin = new plugins[plugin]();
+ registeredPlugins.push(plugin);
+
+ if ('init' in plugin) {
+ plugin.init.call(thisObj);
+ }
+
+ return true;
+ };
+
+ /**
+ * Deregisters a plugin.
+ *
+ * @param {string} plugin
+ * @return {boolean}
+ * @function
+ * @name deregister
+ * @memberOf PluginManager.prototype
+ */
+ base.deregister = function (plugin) {
+ var removedPlugin,
+ pluginIdx = registeredPlugins.length,
+ removed = false;
+
+ if (!base.isRegistered(plugin)) {
+ return removed;
+ }
+
+ while (pluginIdx--) {
+ if (registeredPlugins[pluginIdx] instanceof plugins[plugin]) {
+ removedPlugin = registeredPlugins.splice(pluginIdx, 1)[0];
+ removed = true;
+
+ if ('destroy' in removedPlugin) {
+ removedPlugin.destroy.call(thisObj);
+ }
+ }
+ }
+
+ return removed;
+ };
+
+ /**
+ * Clears all plugins and removes the owner reference.
+ *
+ * Calling any functions on this object after calling
+ * destroy will cause a JS error.
+ *
+ * @name destroy
+ * @memberOf PluginManager.prototype
+ */
+ base.destroy = function () {
+ var i = registeredPlugins.length;
+
+ while (i--) {
+ if ('destroy' in registeredPlugins[i]) {
+ registeredPlugins[i].destroy.call(thisObj);
+ }
+ }
+
+ registeredPlugins = [];
+ thisObj = null;
+ };
+ }
+
+ PluginManager.plugins = plugins;
+
+ // 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$1 = ie && ie < 11;
+
+
+ /**
+ * Gets the text, start/end node and offset for
+ * length chars left or right of the passed node
+ * at the specified offset.
+ *
+ * @param {Node} node
+ * @param {number} offset
+ * @param {boolean} isLeft
+ * @param {number} length
+ * @return {Object}
+ * @private
+ */
+ var outerText = function (range, isLeft, length) {
+ var nodeValue, remaining, start, end, node,
+ text = '',
+ next = range.startContainer,
+ offset = range.startOffset;
+
+ // Handle cases where node is a paragraph and offset
+ // refers to the index of a text node.
+ // 3 = text node
+ if (next && next.nodeType !== 3) {
+ next = next.childNodes[offset];
+ offset = 0;
+ }
+
+ start = end = offset;
+
+ while (length > text.length && next && next.nodeType === 3) {
+ nodeValue = next.nodeValue;
+ remaining = length - text.length;
+
+ // If not the first node, start and end should be at their
+ // max values as will be updated when getting the text
+ if (node) {
+ end = nodeValue.length;
+ start = 0;
+ }
+
+ node = next;
+
+ if (isLeft) {
+ start = Math.max(end - remaining, 0);
+ offset = start;
+
+ text = nodeValue.substr(start, end - start) + text;
+ next = node.previousSibling;
+ } else {
+ end = Math.min(remaining, nodeValue.length);
+ offset = start + end;
+
+ text += nodeValue.substr(start, end);
+ next = node.nextSibling;
+ }
+ }
+
+ return {
+ node: node || next,
+ offset: offset,
+ text: text
+ };
+ };
+
+ /**
+ * Range helper
+ *
+ * @class RangeHelper
+ * @name RangeHelper
+ */
+ function RangeHelper(win, d) {
+ var _createMarker, _prepareInput,
+ doc = d || win.contentDocument || win.document,
+ startMarker = 'sceditor-start-marker',
+ endMarker = 'sceditor-end-marker',
+ base = this;
+
+ /**
+ * Inserts HTML into the current range replacing any selected
+ * text.
+ *
+ * If endHTML is specified the selected contents will be put between
+ * html and endHTML. If there is nothing selected html and endHTML are
+ * just concatenate together.
+ *
+ * @param {string} html
+ * @param {string} [endHTML]
+ * @return False on fail
+ * @function
+ * @name insertHTML
+ * @memberOf RangeHelper.prototype
+ */
+ base.insertHTML = function (html, endHTML) {
+ var node, div,
+ range = base.selectedRange();
+
+ if (!range) {
+ return false;
+ }
+
+ if (endHTML) {
+ html += base.selectedHtml() + endHTML;
+ }
+
+ div = createElement('p', {}, doc);
+ node = doc.createDocumentFragment();
+ div.innerHTML = html;
+
+ while (div.firstChild) {
+ appendChild(node, div.firstChild);
+ }
+
+ base.insertNode(node);
+ };
+
+ /**
+ * Prepares HTML to be inserted by adding a zero width space
+ * if the last child is empty and adding the range start/end
+ * markers to the last child.
+ *
+ * @param {Node|string} node
+ * @param {Node|string} [endNode]
+ * @param {boolean} [returnHtml]
+ * @return {Node|string}
+ * @private
+ */
+ _prepareInput = function (node, endNode, returnHtml) {
+ var lastChild,
+ frag = doc.createDocumentFragment();
+
+ if (typeof node === 'string') {
+ if (endNode) {
+ node += base.selectedHtml() + endNode;
+ }
+
+ frag = parseHTML(node);
+ } else {
+ appendChild(frag, node);
+
+ if (endNode) {
+ appendChild(frag, base.selectedRange().extractContents());
+ appendChild(frag, endNode);
+ }
+ }
+
+ if (!(lastChild = frag.lastChild)) {
+ return;
+ }
+
+ while (!isInline(lastChild.lastChild, true)) {
+ lastChild = lastChild.lastChild;
+ }
+
+ if (canHaveChildren(lastChild)) {
+ // Webkit won't allow the cursor to be placed inside an
+ // empty tag, so add a zero width space to it.
+ if (!lastChild.lastChild) {
+ appendChild(lastChild, document.createTextNode('\u200B'));
+ }
+ } else {
+ lastChild = frag;
+ }
+
+ base.removeMarkers();
+
+ // Append marks to last child so when restored cursor will be in
+ // the right place
+ appendChild(lastChild, _createMarker(startMarker));
+ appendChild(lastChild, _createMarker(endMarker));
+
+ if (returnHtml) {
+ var div = createElement('div');
+ appendChild(div, frag);
+
+ return div.innerHTML;
+ }
+
+ return frag;
+ };
+
+ /**
+ * The same as insertHTML except with DOM nodes instead
+ *
+ * Warning: the nodes must belong to the
+ * document they are being inserted into. Some browsers
+ * will throw exceptions if they don't.
+ *
+ * Returns boolean false on fail
+ *
+ * @param {Node} node
+ * @param {Node} endNode
+ * @return {false|undefined}
+ * @function
+ * @name insertNode
+ * @memberOf RangeHelper.prototype
+ */
+ base.insertNode = function (node, endNode) {
+ var input = _prepareInput(node, endNode),
+ range = base.selectedRange(),
+ parent$$1 = range.commonAncestorContainer;
+
+ if (!input) {
+ return false;
+ }
+
+ range.deleteContents();
+
+ // FF allows to be selected but inserting a node
+ // into will cause it not to be displayed so must
+ // insert before the in FF.
+ // 3 = TextNode
+ if (parent$$1 && parent$$1.nodeType !== 3 && !canHaveChildren(parent$$1)) {
+ insertBefore(input, parent$$1);
+ } else {
+ range.insertNode(input);
+ }
+
+ base.restoreRange();
+ };
+
+ /**
+ * Clones the selected Range
+ *
+ * @return {Range}
+ * @function
+ * @name cloneSelected
+ * @memberOf RangeHelper.prototype
+ */
+ base.cloneSelected = function () {
+ var range = base.selectedRange();
+
+ if (range) {
+ return range.cloneRange();
+ }
+ };
+
+ /**
+ * Gets the selected Range
+ *
+ * @return {Range}
+ * @function
+ * @name selectedRange
+ * @memberOf RangeHelper.prototype
+ */
+ base.selectedRange = function () {
+ var range, firstChild,
+ sel = win.getSelection();
+
+ if (!sel) {
+ return;
+ }
+
+ // When creating a new range, set the start to the first child
+ // element of the body element to avoid errors in FF.
+ if (sel.rangeCount <= 0) {
+ firstChild = doc.body;
+ while (firstChild.firstChild) {
+ firstChild = firstChild.firstChild;
+ }
+
+ range = doc.createRange();
+ // Must be setStartBefore otherwise it can cause infinite
+ // loops with lists in WebKit. See issue 442
+ range.setStartBefore(firstChild);
+
+ sel.addRange(range);
+ }
+
+ if (sel.rangeCount > 0) {
+ range = sel.getRangeAt(0);
+ }
+
+ return range;
+ };
+
+ /**
+ * Gets if there is currently a selection
+ *
+ * @return {boolean}
+ * @function
+ * @name hasSelection
+ * @since 1.4.4
+ * @memberOf RangeHelper.prototype
+ */
+ base.hasSelection = function () {
+ var sel = win.getSelection();
+
+ return sel && sel.rangeCount > 0;
+ };
+
+ /**
+ * Gets the currently selected HTML
+ *
+ * @return {string}
+ * @function
+ * @name selectedHtml
+ * @memberOf RangeHelper.prototype
+ */
+ base.selectedHtml = function () {
+ var div,
+ range = base.selectedRange();
+
+ if (range) {
+ div = createElement('p', {}, doc);
+ appendChild(div, range.cloneContents());
+
+ return div.innerHTML;
+ }
+
+ return '';
+ };
+
+ /**
+ * Gets the parent node of the selected contents in the range
+ *
+ * @return {HTMLElement}
+ * @function
+ * @name parentNode
+ * @memberOf RangeHelper.prototype
+ */
+ base.parentNode = function () {
+ var range = base.selectedRange();
+
+ if (range) {
+ return range.commonAncestorContainer;
+ }
+ };
+
+ /**
+ * Gets the first block level parent of the selected
+ * contents of the range.
+ *
+ * @return {HTMLElement}
+ * @function
+ * @name getFirstBlockParent
+ * @memberOf RangeHelper.prototype
+ */
+ /**
+ * Gets the first block level parent of the selected
+ * contents of the range.
+ *
+ * @param {Node} [n] The element to get the first block level parent from
+ * @return {HTMLElement}
+ * @function
+ * @name getFirstBlockParent^2
+ * @since 1.4.1
+ * @memberOf RangeHelper.prototype
+ */
+ base.getFirstBlockParent = function (node) {
+ var func = function (elm) {
+ if (!isInline(elm, true)) {
+ return elm;
+ }
+
+ elm = elm ? elm.parentNode : null;
+
+ return elm ? func(elm) : elm;
+ };
+
+ return func(node || base.parentNode());
+ };
+
+ /**
+ * Inserts a node at either the start or end of the current selection
+ *
+ * @param {Bool} start
+ * @param {Node} node
+ * @function
+ * @name insertNodeAt
+ * @memberOf RangeHelper.prototype
+ */
+ base.insertNodeAt = function (start, node) {
+ var currentRange = base.selectedRange(),
+ range = base.cloneSelected();
+
+ if (!range) {
+ return false;
+ }
+
+ range.collapse(start);
+ range.insertNode(node);
+
+ // Reselect the current range.
+ // Fixes issue with Chrome losing the selection. Issue#82
+ base.selectRange(currentRange);
+ };
+
+ /**
+ * Creates a marker node
+ *
+ * @param {string} id
+ * @return {HTMLSpanElement}
+ * @private
+ */
+ _createMarker = function (id) {
+ base.removeMarker(id);
+
+ var marker = createElement('span', {
+ id: id,
+ className: 'sceditor-selection sceditor-ignore',
+ style: 'display:none;line-height:0'
+ }, doc);
+
+ marker.innerHTML = ' ';
+
+ return marker;
+ };
+
+ /**
+ * Inserts start/end markers for the current selection
+ * which can be used by restoreRange to re-select the
+ * range.
+ *
+ * @memberOf RangeHelper.prototype
+ * @function
+ * @name insertMarkers
+ */
+ base.insertMarkers = function () {
+ var currentRange = base.selectedRange();
+ var startNode = _createMarker(startMarker);
+
+ base.removeMarkers();
+ base.insertNodeAt(true, startNode);
+
+ // Fixes issue with end marker sometimes being placed before
+ // the start marker when the range is collapsed.
+ if (currentRange && currentRange.collapsed) {
+ startNode.parentNode.insertBefore(
+ _createMarker(endMarker), startNode.nextSibling);
+ } else {
+ base.insertNodeAt(false, _createMarker(endMarker));
+ }
+ };
+
+ /**
+ * Gets the marker with the specified ID
+ *
+ * @param {string} id
+ * @return {Node}
+ * @function
+ * @name getMarker
+ * @memberOf RangeHelper.prototype
+ */
+ base.getMarker = function (id) {
+ return doc.getElementById(id);
+ };
+
+ /**
+ * Removes the marker with the specified ID
+ *
+ * @param {string} id
+ * @function
+ * @name removeMarker
+ * @memberOf RangeHelper.prototype
+ */
+ base.removeMarker = function (id) {
+ var marker = base.getMarker(id);
+
+ if (marker) {
+ remove(marker);
+ }
+ };
+
+ /**
+ * Removes the start/end markers
+ *
+ * @function
+ * @name removeMarkers
+ * @memberOf RangeHelper.prototype
+ */
+ base.removeMarkers = function () {
+ base.removeMarker(startMarker);
+ base.removeMarker(endMarker);
+ };
+
+ /**
+ * Saves the current range location. Alias of insertMarkers()
+ *
+ * @function
+ * @name saveRage
+ * @memberOf RangeHelper.prototype
+ */
+ base.saveRange = function () {
+ base.insertMarkers();
+ };
+
+ /**
+ * Select the specified range
+ *
+ * @param {Range} range
+ * @function
+ * @name selectRange
+ * @memberOf RangeHelper.prototype
+ */
+ base.selectRange = function (range) {
+ var lastChild;
+ var sel = win.getSelection();
+ var container = range.endContainer;
+
+ // Check if cursor is set after a BR when the BR is the only
+ // child of the parent. In Firefox this causes a line break
+ // to occur when something is typed. See issue #321
+ if (!IE_BR_FIX$1 && range.collapsed && container &&
+ !isInline(container, true)) {
+
+ lastChild = container.lastChild;
+ while (lastChild && is(lastChild, '.sceditor-ignore')) {
+ lastChild = lastChild.previousSibling;
+ }
+
+ if (is(lastChild, 'br')) {
+ var rng = doc.createRange();
+ rng.setEndAfter(lastChild);
+ rng.collapse(false);
+
+ if (base.compare(range, rng)) {
+ range.setStartBefore(lastChild);
+ range.collapse(true);
+ }
+ }
+ }
+
+ if (sel) {
+ base.clear();
+ sel.addRange(range);
+ }
+ };
+
+ /**
+ * Restores the last range saved by saveRange() or insertMarkers()
+ *
+ * @function
+ * @name restoreRange
+ * @memberOf RangeHelper.prototype
+ */
+ base.restoreRange = function () {
+ var isCollapsed,
+ range = base.selectedRange(),
+ start = base.getMarker(startMarker),
+ end = base.getMarker(endMarker);
+
+ if (!start || !end || !range) {
+ return false;
+ }
+
+ isCollapsed = start.nextSibling === end;
+
+ range = doc.createRange();
+ range.setStartBefore(start);
+ range.setEndAfter(end);
+
+ if (isCollapsed) {
+ range.collapse(true);
+ }
+
+ base.selectRange(range);
+ base.removeMarkers();
+ };
+
+ /**
+ * Selects the text left and right of the current selection
+ *
+ * @param {number} left
+ * @param {number} right
+ * @since 1.4.3
+ * @function
+ * @name selectOuterText
+ * @memberOf RangeHelper.prototype
+ */
+ base.selectOuterText = function (left, right) {
+ var start, end,
+ range = base.cloneSelected();
+
+ if (!range) {
+ return false;
+ }
+
+ range.collapse(false);
+
+ start = outerText(range, true, left);
+ end = outerText(range, false, right);
+
+ range.setStart(start.node, start.offset);
+ range.setEnd(end.node, end.offset);
+
+ base.selectRange(range);
+ };
+
+ /**
+ * Gets the text left or right of the current selection
+ *
+ * @param {boolean} before
+ * @param {number} length
+ * @return {string}
+ * @since 1.4.3
+ * @function
+ * @name selectOuterText
+ * @memberOf RangeHelper.prototype
+ */
+ base.getOuterText = function (before, length) {
+ var range = base.cloneSelected();
+
+ if (!range) {
+ return '';
+ }
+
+ range.collapse(!before);
+
+ return outerText(range, before, length).text;
+ };
+
+ /**
+ * Replaces keywords with values based on the current caret position
+ *
+ * @param {Array} keywords
+ * @param {boolean} includeAfter If to include the text after the
+ * current caret position or just
+ * text before
+ * @param {boolean} keywordsSorted If the keywords array is pre
+ * sorted shortest to longest
+ * @param {number} longestKeyword Length of the longest keyword
+ * @param {boolean} requireWhitespace If the key must be surrounded
+ * by whitespace
+ * @param {string} keypressChar If this is being called from
+ * a keypress event, this should be
+ * set to the pressed character
+ * @return {boolean}
+ * @function
+ * @name replaceKeyword
+ * @memberOf RangeHelper.prototype
+ */
+ // eslint-disable-next-line max-params
+ base.replaceKeyword = function (
+ keywords,
+ includeAfter,
+ keywordsSorted,
+ longestKeyword,
+ requireWhitespace,
+ keypressChar
+ ) {
+ if (!keywordsSorted) {
+ keywords.sort(function (a, b) {
+ return a[0].length - b[0].length;
+ });
+ }
+
+ var outerText, match, matchPos, startIndex,
+ leftLen, charsLeft, keyword, keywordLen,
+ whitespaceRegex = '(^|[\\s\xA0\u2002\u2003\u2009])',
+ keywordIdx = keywords.length,
+ whitespaceLen = requireWhitespace ? 1 : 0,
+ maxKeyLen = longestKeyword ||
+ keywords[keywordIdx - 1][0].length;
+
+ if (requireWhitespace) {
+ maxKeyLen++;
+ }
+
+ keypressChar = keypressChar || '';
+ outerText = base.getOuterText(true, maxKeyLen);
+ leftLen = outerText.length;
+ outerText += keypressChar;
+
+ if (includeAfter) {
+ outerText += base.getOuterText(false, maxKeyLen);
+ }
+
+ while (keywordIdx--) {
+ keyword = keywords[keywordIdx][0];
+ keywordLen = keyword.length;
+ startIndex = Math.max(0, leftLen - keywordLen - whitespaceLen);
+ matchPos = -1;
+
+ if (requireWhitespace) {
+ match = outerText
+ .substr(startIndex)
+ .match(new RegExp(whitespaceRegex +
+ regex(keyword) + whitespaceRegex));
+
+ if (match) {
+ // Add the length of the text that was removed by
+ // substr() and also add 1 for the whitespace
+ matchPos = match.index + startIndex + match[1].length;
+ }
+ } else {
+ matchPos = outerText.indexOf(keyword, startIndex);
+ }
+
+ if (matchPos > -1) {
+ // Make sure the match is between before and
+ // after, not just entirely in one side or the other
+ if (matchPos <= leftLen &&
+ matchPos + keywordLen + whitespaceLen >= leftLen) {
+ charsLeft = leftLen - matchPos;
+
+ // If the keypress char is white space then it should
+ // not be replaced, only chars that are part of the
+ // key should be replaced.
+ base.selectOuterText(
+ charsLeft,
+ keywordLen - charsLeft -
+ (/^\S/.test(keypressChar) ? 1 : 0)
+ );
+
+ base.insertHTML(keywords[keywordIdx][1]);
+ return true;
+ }
+ }
+ }
+
+ return false;
+ };
+
+ /**
+ * Compares two ranges.
+ *
+ * If rangeB is undefined it will be set to
+ * the current selected range
+ *
+ * @param {Range} rngA
+ * @param {Range} [rngB]
+ * @return {boolean}
+ * @function
+ * @name compare
+ * @memberOf RangeHelper.prototype
+ */
+ base.compare = function (rngA, rngB) {
+ if (!rngB) {
+ rngB = base.selectedRange();
+ }
+
+ if (!rngA || !rngB) {
+ return !rngA && !rngB;
+ }
+
+ return rngA.compareBoundaryPoints(Range.END_TO_END, rngB) === 0 &&
+ rngA.compareBoundaryPoints(Range.START_TO_START, rngB) === 0;
+ };
+
+ /**
+ * Removes any current selection
+ *
+ * @since 1.4.6
+ * @function
+ * @name clear
+ * @memberOf RangeHelper.prototype
+ */
+ base.clear = function () {
+ var sel = win.getSelection();
+
+ if (sel) {
+ if (sel.removeAllRanges) {
+ sel.removeAllRanges();
+ } else if (sel.empty) {
+ sel.empty();
+ }
+ }
+ };
+ }
+
+ /**
+ * Checks all emoticons are surrounded by whitespace and
+ * replaces any that aren't with with their emoticon code.
+ *
+ * @param {HTMLElement} node
+ * @param {rangeHelper} rangeHelper
+ * @return {void}
+ */
+ function checkWhitespace(node, rangeHelper) {
+ var noneWsRegex = /[^\s\xA0\u2002\u2003\u2009\u00a0]+/;
+ var emoticons = node && find(node, 'img[data-sceditor-emoticon]');
+
+ if (!node || !emoticons.length) {
+ return;
+ }
+
+ for (var i = 0; i < emoticons.length; i++) {
+ var emoticon = emoticons[i];
+ var parent$$1 = emoticon.parentNode;
+ var prev = emoticon.previousSibling;
+ var next = emoticon.nextSibling;
+
+ if ((!prev || !noneWsRegex.test(prev.nodeValue.slice(-1))) &&
+ (!next || !noneWsRegex.test((next.nodeValue || '')[0]))) {
+ continue;
+ }
+
+ var range = rangeHelper.cloneSelected();
+ var rangeStart = -1;
+ var rangeStartContainer = range.startContainer;
+ var previousText = prev.nodeValue;
+
+ // For IE's HTMLPhraseElement
+ if (previousText === null) {
+ previousText = prev.innerText || '';
+ }
+
+ previousText += data(emoticon, 'sceditor-emoticon');
+
+ // If the cursor is after the removed emoticon, add
+ // the length of the newly added text to it
+ if (rangeStartContainer === next) {
+ rangeStart = previousText.length + range.startOffset;
+ }
+
+ // If the cursor is set before the next node, set it to
+ // the end of the new text node
+ if (rangeStartContainer === node &&
+ node.childNodes[range.startOffset] === next) {
+ rangeStart = previousText.length;
+ }
+
+ // If the cursor is set before the removed emoticon,
+ // just keep it at that position
+ if (rangeStartContainer === prev) {
+ rangeStart = range.startOffset;
+ }
+
+ if (!next || next.nodeType !== TEXT_NODE) {
+ next = parent$$1.insertBefore(
+ parent$$1.ownerDocument.createTextNode(''), next
+ );
+ }
+
+ next.insertData(0, previousText);
+ remove(prev);
+ remove(emoticon);
+
+ // Need to update the range starting position if it's been modified
+ if (rangeStart > -1) {
+ range.setStart(next, rangeStart);
+ range.collapse(true);
+ rangeHelper.selectRange(range);
+ }
+ }
+ }
+
+ /**
+ * Replaces any emoticons inside the root node with images.
+ *
+ * emoticons should be an object where the key is the emoticon
+ * code and the value is the HTML to replace it with.
+ *
+ * @param {HTMLElement} root
+ * @param {Object} emoticons
+ * @param {boolean} emoticonsCompat
+ * @return {void}
+ */
+ function replace(root, emoticons, emoticonsCompat) {
+ var doc = root.ownerDocument;
+ var space = '(^|\\s|\xA0|\u2002|\u2003|\u2009|$)';
+ var emoticonCodes = [];
+ var emoticonRegex = {};
+
+ // TODO: Make this tag configurable.
+ if (parent(root, 'code')) {
+ return;
+ }
+
+ each(emoticons, function (key) {
+ emoticonRegex[key] = new RegExp(space + regex(key) + space);
+ emoticonCodes.push(key);
+ });
+
+ // Sort keys longest to shortest so that longer keys
+ // take precedence (avoids bugs with shorter keys partially
+ // matching longer ones)
+ emoticonCodes.sort(function (a, b) {
+ return b.length - a.length;
+ });
+
+ (function convert(node) {
+ node = node.firstChild;
+
+ while (node) {
+ // TODO: Make this tag configurable.
+ if (node.nodeType === ELEMENT_NODE && !is(node, 'code')) {
+ convert(node);
+ }
+
+ if (node.nodeType === TEXT_NODE) {
+ for (var i = 0; i < emoticonCodes.length; i++) {
+ var text = node.nodeValue;
+ var key = emoticonCodes[i];
+ var index = emoticonsCompat ?
+ text.search(emoticonRegex[key]) :
+ text.indexOf(key);
+
+ if (index > -1) {
+ // When emoticonsCompat is enabled this will be the
+ // position after any white space
+ var startIndex = text.indexOf(key, index);
+ var fragment = parseHTML(emoticons[key], doc);
+ var after = text.substr(startIndex + key.length);
+
+ fragment.appendChild(doc.createTextNode(after));
+
+ node.nodeValue = text.substr(0, startIndex);
+ node.parentNode
+ .insertBefore(fragment, node.nextSibling);
+ }
+ }
+ }
+
+ node = node.nextSibling;
+ }
+ }(root));
+ }
+
+ var globalWin = window;
+ var globalDoc = document;
+
+ var IE_VER = 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$2 = IE_VER && IE_VER < 11;
+
+ var IMAGE_MIME_REGEX = /^image\/(p?jpe?g|gif|png|bmp)$/i;
+
+ /**
+ * Wrap inlines that are in the root in paragraphs.
+ *
+ * @param {HTMLBodyElement} body
+ * @param {Document} doc
+ * @private
+ */
+ function wrapInlines(body, doc) {
+ var wrapper;
+
+ traverse(body, function (node) {
+ if (isInline(node, true)) {
+ if (!wrapper) {
+ wrapper = createElement('p', {}, doc);
+ insertBefore(wrapper, node);
+ }
+
+ if (node.nodeType !== TEXT_NODE || node.nodeValue !== '') {
+ appendChild(wrapper, node);
+ }
+ } else {
+ wrapper = null;
+ }
+ }, false, true);
+ }
+
+ /**
+ * SCEditor - A lightweight WYSIWYG editor
+ *
+ * @param {HTMLTextAreaElement} original The textarea to be converted
+ * @param {Object} userOptions
+ * @class SCEditor
+ * @name SCEditor
+ */
+ function SCEditor(original, userOptions) {
+ /**
+ * Alias of this
+ *
+ * @private
+ */
+ var base = this;
+
+ /**
+ * Editor format like BBCode or HTML
+ */
+ var format;
+
+ /**
+ * The div which contains the editor and toolbar
+ *
+ * @type {HTMLDivElement}
+ * @private
+ */
+ var editorContainer;
+
+ /**
+ * Map of events handlers bound to this instance.
+ *
+ * @type {Object}
+ * @private
+ */
+ var eventHandlers = {};
+
+ /**
+ * The editors toolbar
+ *
+ * @type {HTMLDivElement}
+ * @private
+ */
+ var toolbar;
+
+ /**
+ * The editors iframe which should be in design mode
+ *
+ * @type {HTMLIFrameElement}
+ * @private
+ */
+ var wysiwygEditor;
+
+ /**
+ * The editors window
+ *
+ * @type {Window}
+ * @private
+ */
+ var wysiwygWindow;
+
+ /**
+ * The WYSIWYG editors body element
+ *
+ * @type {HTMLBodyElement}
+ * @private
+ */
+ var wysiwygBody;
+
+ /**
+ * The WYSIWYG editors document
+ *
+ * @type {Document}
+ * @private
+ */
+ var wysiwygDocument;
+
+ /**
+ * The editors textarea for viewing source
+ *
+ * @type {HTMLTextAreaElement}
+ * @private
+ */
+ var sourceEditor;
+
+ /**
+ * The current dropdown
+ *
+ * @type {HTMLDivElement}
+ * @private
+ */
+ var dropdown;
+
+ /**
+ * Store the last cursor position. Needed for IE because it forgets
+ *
+ * @type {Range}
+ * @private
+ */
+ var lastRange;
+
+ /**
+ * If the user is currently composing text via IME
+ * @type {boolean}
+ */
+ var isComposing;
+
+ /**
+ * Timer for valueChanged key handler
+ * @type {number}
+ */
+ var valueChangedKeyUpTimer;
+
+ /**
+ * The editors locale
+ *
+ * @private
+ */
+ var locale;
+
+ /**
+ * Stores a cache of preloaded images
+ *
+ * @private
+ * @type {Array.}
+ */
+ var preLoadCache = [];
+
+ /**
+ * The editors rangeHelper instance
+ *
+ * @type {RangeHelper}
+ * @private
+ */
+ var rangeHelper;
+
+ /**
+ * An array of button state handlers
+ *
+ * @type {Array.}
+ * @private
+ */
+ var btnStateHandlers = [];
+
+ /**
+ * Plugin manager instance
+ *
+ * @type {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 {HTMLStyleElement}
+ * @private
+ */
+ var inlineCss;
+
+ /**
+ * Object containing a list of shortcut handlers
+ *
+ * @type {Object}
+ * @private
+ */
+ var shortcutHandlers = {};
+
+ /**
+ * The min and max heights that autoExpand should stay within
+ *
+ * @type {Object}
+ * @private
+ */
+ var autoExpandBounds;
+
+ /**
+ * Timeout for the autoExpand function to throttle calls
+ *
+ * @private
+ */
+ var autoExpandThrottle;
+
+ /**
+ * Cache of the current toolbar buttons
+ *
+ * @type {Object}
+ * @private
+ */
+ var toolbarButtons = {};
+
+ /**
+ * Last scroll position before maximizing so
+ * it can be restored when finished.
+ *
+ * @type {number}
+ * @private
+ */
+ var maximizeScrollPosition;
+
+ /**
+ * Stores the contents while a paste is taking place.
+ *
+ * Needed to support browsers that lack clipboard API support.
+ *
+ * @type {?DocumentFragment}
+ * @private
+ */
+ var pasteContentFragment;
+
+ /**
+ * All the emoticons from dropdown, more and hidden combined
+ * and with the emoticons root set
+ *
+ * @type {!Object}
+ * @private
+ */
+ var allEmoticons = {};
+
+ /**
+ * Current icon set if any
+ *
+ * @type {?Object}
+ * @private
+ */
+ var icons;
+
+ /**
+ * Private functions
+ * @private
+ */
+ var init,
+ replaceEmoticons,
+ handleCommand,
+ saveRange,
+ initEditor,
+ initPlugins,
+ initLocale,
+ initToolBar,
+ initOptions,
+ initEvents,
+ initResize,
+ initEmoticons,
+ handlePasteEvt,
+ handlePasteData,
+ handleKeyDown,
+ handleBackSpace,
+ handleKeyPress,
+ handleFormReset,
+ handleMouseDown,
+ handleComposition,
+ handleEvent,
+ handleDocumentClick,
+ updateToolBar,
+ updateActiveButtons,
+ sourceEditorSelectedText,
+ appendNewLine,
+ checkSelectionChanged,
+ checkNodeChanged,
+ autofocus,
+ emoticonsKeyPress,
+ emoticonsCheckWhitespace,
+ currentStyledBlockNode,
+ triggerValueChanged,
+ valueChangedBlur,
+ valueChangedKeyUp,
+ autoUpdate,
+ autoExpand;
+
+ /**
+ * All the commands supported by the editor
+ * @name commands
+ * @memberOf SCEditor.prototype
+ */
+ base.commands = extend(true, {}, (userOptions.commands || defaultCmds));
+
+ /**
+ * Options for this editor instance
+ * @name opts
+ * @memberOf SCEditor.prototype
+ */
+ var options = base.opts = extend(
+ true, {}, defaultOptions, userOptions
+ );
+
+ // Don't deep extend emoticons (fixes #565)
+ base.opts.emoticons = userOptions.emoticons || defaultOptions.emoticons;
+
+ /**
+ * Creates the editor iframe and textarea
+ * @private
+ */
+ init = function () {
+ original._sceditor = base;
+
+ // Load locale
+ if (options.locale && options.locale !== 'en') {
+ initLocale();
+ }
+
+ editorContainer = createElement('div', {
+ className: 'sceditor-container'
+ });
+
+ insertBefore(editorContainer, original);
+ css(editorContainer, '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) {
+ addClass(editorContainer, 'ie ie' + IE_VER);
+ }
+
+ isRequired = original.required;
+ original.required = false;
+
+ var FormatCtor = SCEditor.formats[options.format];
+ format = FormatCtor ? new FormatCtor() : {};
+ if ('init' in format) {
+ format.init.call(base);
+ }
+
+ // create the editor
+ initPlugins();
+ initEmoticons();
+ initToolBar();
+ initEditor();
+ initOptions();
+ initEvents();
+
+ // force into source mode if is a browser that can't handle
+ // full editing
+ if (!isWysiwygSupported) {
+ base.toggleSourceMode();
+ }
+
+ updateActiveButtons();
+
+ var loaded = function () {
+ off(globalWin, 'load', loaded);
+
+ if (options.autofocus) {
+ autofocus();
+ }
+
+ autoExpand();
+ appendNewLine();
+ // TODO: use editor doc and window?
+ pluginManager.call('ready');
+ if ('onReady' in format) {
+ format.onReady.call(base);
+ }
+ };
+ on(globalWin, 'load', loaded);
+ if (globalDoc.readyState === 'complete') {
+ loaded();
+ }
+ };
+
+ initPlugins = function () {
+ var plugins = options.plugins;
+
+ plugins = plugins ? plugins.toString().split(',') : [];
+ pluginManager = new PluginManager(base);
+
+ plugins.forEach(function (plugin) {
+ pluginManager.register(plugin.trim());
+ });
+ };
+
+ /**
+ * 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
+ * @private
+ */
+ initEditor = function () {
+ sourceEditor = createElement('textarea');
+ wysiwygEditor = createElement('iframe', {
+ frameborder: 0,
+ allowfullscreen: true
+ });
+
+ /* 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 (options.startInSourceMode) {
+ addClass(editorContainer, 'sourceMode');
+ hide(wysiwygEditor);
+ } else {
+ addClass(editorContainer, 'wysiwygMode');
+ hide(sourceEditor);
+ }
+
+ if (!options.spellcheck) {
+ attr(editorContainer, 'spellcheck', 'false');
+ }
+
+ if (globalWin.location.protocol === 'https:') {
+ // eslint-disable-next-line no-script-url
+ attr(wysiwygEditor, 'src', 'javascript:false');
+ }
+
+ // Add the editor to the container
+ appendChild(editorContainer, wysiwygEditor);
+ appendChild(editorContainer, sourceEditor);
+
+ // TODO: make this optional somehow
+ base.dimensions(
+ options.width || width(original),
+ options.height || height(original)
+ );
+
+ // Add IE version class to the HTML element so can apply
+ // conditional styling without CSS hacks
+ var className = IE_VER ? 'ie ie' + IE_VER : '';
+ // Add ios to HTML so can apply CSS fix to only it
+ className += ios ? ' ios' : '';
+
+ wysiwygDocument = wysiwygEditor.contentDocument;
+ wysiwygDocument.open();
+ wysiwygDocument.write(_tmpl('html', {
+ attrs: ' class="' + className + '"',
+ spellcheck: options.spellcheck ? '' : 'spellcheck="false"',
+ charset: options.charset,
+ style: options.style
+ }));
+ wysiwygDocument.close();
+
+ wysiwygBody = wysiwygDocument.body;
+ wysiwygWindow = wysiwygEditor.contentWindow;
+
+ base.readOnly(!!options.readOnly);
+
+ // iframe overflow fix for iOS, also fixes an IE issue with the
+ // editor not getting focus when clicking inside
+ if (ios || edge || IE_VER) {
+ height(wysiwygBody, '100%');
+
+ if (!IE_VER) {
+ on(wysiwygBody, 'touchend', base.focus);
+ }
+ }
+
+ var tabIndex = attr(original, 'tabindex');
+ attr(sourceEditor, 'tabindex', tabIndex);
+ attr(wysiwygEditor, 'tabindex', tabIndex);
+
+ rangeHelper = new RangeHelper(wysiwygWindow);
+
+ // load any textarea value into the editor
+ hide(original);
+ base.val(original.value);
+
+ var placeholder = options.placeholder ||
+ attr(original, 'placeholder');
+
+ if (placeholder) {
+ sourceEditor.placeholder = placeholder;
+ attr(wysiwygBody, 'placeholder', placeholder);
+ }
+ };
+
+ /**
+ * Initialises options
+ * @private
+ */
+ initOptions = function () {
+ // auto-update original textbox on blur if option set to true
+ if (options.autoUpdate) {
+ on(wysiwygBody, 'blur', autoUpdate);
+ on(sourceEditor, 'blur', autoUpdate);
+ }
+
+ if (options.rtl === null) {
+ options.rtl = css(sourceEditor, 'direction') === 'rtl';
+ }
+
+ base.rtl(!!options.rtl);
+
+ if (options.autoExpand) {
+ // Need to update when images (or anything else) loads
+ on(wysiwygBody, 'load', autoExpand, EVENT_CAPTURE);
+ on(wysiwygBody, 'input keyup', autoExpand);
+ }
+
+ if (options.resizeEnabled) {
+ initResize();
+ }
+
+ attr(editorContainer, 'id', options.id);
+ base.emoticons(options.emoticonsEnabled);
+ };
+
+ /**
+ * Initialises events
+ * @private
+ */
+ initEvents = function () {
+ var form = original.form;
+ var compositionEvents = 'compositionstart compositionend';
+ var eventsToForward = 'keydown keyup keypress focus blur contextmenu';
+ var checkSelectionEvents = 'onselectionchange' in wysiwygDocument ?
+ 'selectionchange' :
+ 'keyup focus blur contextmenu mouseup touchend click';
+
+ on(globalDoc, 'click', handleDocumentClick);
+
+ if (form) {
+ on(form, 'reset', handleFormReset);
+ on(form, 'submit', base.updateOriginal, EVENT_CAPTURE);
+ }
+
+ on(wysiwygBody, 'keypress', handleKeyPress);
+ on(wysiwygBody, 'keydown', handleKeyDown);
+ on(wysiwygBody, 'keydown', handleBackSpace);
+ on(wysiwygBody, 'keyup', appendNewLine);
+ on(wysiwygBody, 'blur', valueChangedBlur);
+ on(wysiwygBody, 'keyup', valueChangedKeyUp);
+ on(wysiwygBody, 'paste', handlePasteEvt);
+ on(wysiwygBody, compositionEvents, handleComposition);
+ on(wysiwygBody, checkSelectionEvents, checkSelectionChanged);
+ on(wysiwygBody, eventsToForward, handleEvent);
+
+ if (options.emoticonsCompat && globalWin.getSelection) {
+ on(wysiwygBody, 'keyup', emoticonsCheckWhitespace);
+ }
+
+ on(wysiwygBody, 'blur', function () {
+ if (!base.val()) {
+ addClass(wysiwygBody, 'placeholder');
+ }
+ });
+
+ on(wysiwygBody, 'focus', function () {
+ removeClass(wysiwygBody, 'placeholder');
+ });
+
+ on(sourceEditor, 'blur', valueChangedBlur);
+ on(sourceEditor, 'keyup', valueChangedKeyUp);
+ on(sourceEditor, 'keydown', handleKeyDown);
+ on(sourceEditor, compositionEvents, handleComposition);
+ on(sourceEditor, eventsToForward, handleEvent);
+
+ on(wysiwygDocument, 'mousedown', handleMouseDown);
+ on(wysiwygDocument, checkSelectionEvents, checkSelectionChanged);
+ on(wysiwygDocument, 'beforedeactivate keyup mouseup', saveRange);
+ on(wysiwygDocument, 'keyup', appendNewLine);
+ on(wysiwygDocument, 'focus', function () {
+ lastRange = null;
+ });
+
+ on(editorContainer, 'selectionchanged', checkNodeChanged);
+ on(editorContainer, 'selectionchanged', updateActiveButtons);
+ // Custom events to forward
+ on(
+ editorContainer,
+ 'selectionchanged valuechanged nodechanged pasteraw paste',
+ 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 = createElement('div', {
+ className: 'sceditor-toolbar',
+ unselectable: 'on'
+ });
+
+ if (options.icons in SCEditor.icons) {
+ icons = new SCEditor.icons[options.icons]();
+ }
+
+ each(groups, function (_, menuItems) {
+ group = createElement('div', {
+ className: 'sceditor-group'
+ });
+
+ each(menuItems.split(','), function (_, commandName) {
+ var button, shortcut,
+ command = commands[commandName];
+
+ // The commandName must be a valid command and not excluded
+ if (!command || exclude.indexOf(commandName) > -1) {
+ return;
+ }
+
+ shortcut = command.shortcut;
+ button = _tmpl('toolbarButton', {
+ name: commandName,
+ dispName: base._(command.name ||
+ command.tooltip || commandName)
+ }, true).firstChild;
+
+ if (icons && icons.create) {
+ var icon = icons.create(commandName);
+ if (icon) {
+ insertBefore(icons.create(commandName),
+ button.firstChild);
+ addClass(button, 'has-icon');
+ }
+ }
+
+ button._sceTxtMode = !!command.txtExec;
+ button._sceWysiwygMode = !!command.exec;
+ toggleClass(button, 'disabled', !command.exec);
+ on(button, 'click', function (e) {
+ if (!hasClass(button, 'disabled')) {
+ handleCommand(button, command);
+ }
+
+ updateActiveButtons();
+ e.preventDefault();
+ });
+ // Prevent editor losing focus when button clicked
+ on(button, 'mousedown', function (e) {
+ base.closeDropDown();
+ e.preventDefault();
+ });
+
+ if (command.tooltip) {
+ attr(button, '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 (isString(command.exec)) {
+ btnStateHandlers.push({
+ name: commandName,
+ state: command.exec
+ });
+ }
+
+ appendChild(group, button);
+ toolbarButtons[commandName] = button;
+ });
+
+ // Exclude empty groups
+ if (group.firstChild) {
+ appendChild(toolbar, group);
+ }
+ });
+
+ // Append the toolbar to the toolbarContainer option if given
+ appendChild(options.toolbarContainer || editorContainer, toolbar);
+ };
+
+ /**
+ * Creates the resizer.
+ * @private
+ */
+ initResize = function () {
+ var minHeight, maxHeight, minWidth, maxWidth,
+ mouseMoveFunc, mouseUpFunc,
+ grip = createElement('div', {
+ className: 'sceditor-grip'
+ }),
+ // Cover is used to cover the editor iframe so document
+ // still gets mouse move events
+ cover = createElement('div', {
+ className: 'sceditor-resize-cover'
+ }),
+ moveEvents = 'touchmove mousemove',
+ endEvents = 'touchcancel touchend mouseup',
+ startX = 0,
+ startY = 0,
+ newX = 0,
+ newY = 0,
+ startWidth = 0,
+ startHeight = 0,
+ origWidth = width(editorContainer),
+ origHeight = height(editorContainer),
+ 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);
+ }
+
+ e.preventDefault();
+ };
+
+ mouseUpFunc = function (e) {
+ if (!isDragging) {
+ return;
+ }
+
+ isDragging = false;
+
+ hide(cover);
+ removeClass(editorContainer, 'resizing');
+ off(globalDoc, moveEvents, mouseMoveFunc);
+ off(globalDoc, endEvents, mouseUpFunc);
+
+ e.preventDefault();
+ };
+
+ if (icons && icons.create) {
+ var icon = icons.create('grip');
+ if (icon) {
+ appendChild(grip, icon);
+ addClass(grip, 'has-icon');
+ }
+ }
+
+ appendChild(editorContainer, grip);
+ appendChild(editorContainer, cover);
+ hide(cover);
+
+ on(grip, '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 = width(editorContainer);
+ startHeight = height(editorContainer);
+ isDragging = true;
+
+ addClass(editorContainer, 'resizing');
+ show(cover);
+ on(globalDoc, moveEvents, mouseMoveFunc);
+ on(globalDoc, endEvents, mouseUpFunc);
+
+ e.preventDefault();
+ });
+ };
+
+ /**
+ * Prefixes and preloads the emoticon images
+ * @private
+ */
+ initEmoticons = function () {
+ var emoticons = options.emoticons;
+ var root = options.emoticonsRoot || '';
+
+ if (emoticons) {
+ allEmoticons = extend(
+ {}, emoticons.more, emoticons.dropdown, emoticons.hidden
+ );
+ }
+
+ each(allEmoticons, function (key, url) {
+ allEmoticons[key] = _tmpl('emoticon', {
+ key: key,
+ // Prefix emoticon root to emoticon urls
+ url: root + (url.url || url),
+ tooltip: url.tooltip || key
+ });
+
+ // Preload the emoticon
+ if (options.emoticonsEnabled) {
+ preLoadCache.push(createElement('img', {
+ src: root + (url.url || url)
+ }));
+ }
+ });
+ };
+
+ /**
+ * Autofocus the editor
+ * @private
+ */
+ autofocus = function () {
+ var range, txtPos,
+ node = wysiwygBody.firstChild,
+ focusEnd = !!options.autofocusEnd;
+
+ // Can't focus invisible elements
+ if (!isVisible(editorContainer)) {
+ return;
+ }
+
+ if (base.sourceMode()) {
+ txtPos = focusEnd ? sourceEditor.value.length : 0;
+
+ sourceEditor.setSelectionRange(txtPos, txtPos);
+
+ return;
+ }
+
+ removeWhiteSpace(wysiwygBody);
+
+ if (focusEnd) {
+ if (!(node = wysiwygBody.lastChild)) {
+ node = createElement('p', {}, wysiwygDocument);
+ appendChild(wysiwygBody, 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$2 && is(node, 'br') && node.previousSibling) {
+ node = node.previousSibling;
+ }
+ }
+ }
+
+ range = wysiwygDocument.createRange();
+
+ if (!canHaveChildren(node)) {
+ range.setStartBefore(node);
+
+ if (focusEnd) {
+ range.setStartAfter(node);
+ }
+ } else {
+ range.selectNodeContents(node);
+ }
+
+ range.collapse(!focusEnd);
+ rangeHelper.selectRange(range);
+ currentSelection = range;
+
+ if (focusEnd) {
+ wysiwygBody.scrollTop = wysiwygBody.scrollHeight;
+ }
+
+ base.focus();
+ };
+
+ /**
+ * Gets if the editor is read only
+ *
+ * @since 1.3.5
+ * @function
+ * @memberOf SCEditor.prototype
+ * @name readOnly
+ * @return {boolean}
+ */
+ /**
+ * Sets if the editor is read only
+ *
+ * @param {boolean} readOnly
+ * @since 1.3.5
+ * @function
+ * @memberOf SCEditor.prototype
+ * @name readOnly^2
+ * @return {this}
+ */
+ base.readOnly = function (readOnly) {
+ if (typeof readOnly !== 'boolean') {
+ return !sourceEditor.readonly;
+ }
+
+ wysiwygBody.contentEditable = !readOnly;
+ sourceEditor.readonly = !readOnly;
+
+ updateToolBar(readOnly);
+
+ return base;
+ };
+
+ /**
+ * Gets if the editor is in RTL mode
+ *
+ * @since 1.4.1
+ * @function
+ * @memberOf SCEditor.prototype
+ * @name rtl
+ * @return {boolean}
+ */
+ /**
+ * Sets if the editor is in RTL mode
+ *
+ * @param {boolean} rtl
+ * @since 1.4.1
+ * @function
+ * @memberOf SCEditor.prototype
+ * @name rtl^2
+ * @return {this}
+ */
+ base.rtl = function (rtl) {
+ var dir = rtl ? 'rtl' : 'ltr';
+
+ if (typeof rtl !== 'boolean') {
+ return attr(sourceEditor, 'dir') === 'rtl';
+ }
+
+ attr(wysiwygBody, 'dir', dir);
+ attr(sourceEditor, 'dir', dir);
+
+ removeClass(editorContainer, 'rtl');
+ removeClass(editorContainer, 'ltr');
+ addClass(editorContainer, dir);
+
+ if (icons && icons.rtl) {
+ icons.rtl(rtl);
+ }
+
+ return base;
+ };
+
+ /**
+ * Updates the toolbar to disable/enable the appropriate buttons
+ * @private
+ */
+ updateToolBar = function (disable) {
+ var mode = base.inSourceMode() ? '_sceTxtMode' : '_sceWysiwygMode';
+
+ each(toolbarButtons, function (_, button) {
+ toggleClass(button, 'disabled', disable || !button[mode]);
+ });
+ };
+
+ /**
+ * Gets the width of the editor in pixels
+ *
+ * @since 1.3.5
+ * @function
+ * @memberOf SCEditor.prototype
+ * @name width
+ * @return {number}
+ */
+ /**
+ * Sets the width of the editor
+ *
+ * @param {number} width Width in pixels
+ * @since 1.3.5
+ * @function
+ * @memberOf 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 {number} width Width in pixels
+ * @param {boolean} [saveWidth=true] If to store the width
+ * @since 1.4.1
+ * @function
+ * @memberOf SCEditor.prototype
+ * @name width^3
+ * @return {this}
+ */
+ base.width = function (width$$1, saveWidth) {
+ if (!width$$1 && width$$1 !== 0) {
+ return width(editorContainer);
+ }
+
+ base.dimensions(width$$1, 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 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 {number} width Width in px
+ * @param {number} height Height in px
+ * @since 1.4.1
+ * @function
+ * @memberOf 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 {number} width Width in px
+ * @param {number} height Height in px
+ * @param {boolean} [save=true] If to store the new sizes
+ * @since 1.4.1
+ * @function
+ * @memberOf SCEditor.prototype
+ * @name dimensions^3
+ * @return {this}
+ */
+ base.dimensions = function (width$$1, height$$1, save) {
+ // set undefined width/height to boolean false
+ width$$1 = (!width$$1 && width$$1 !== 0) ? false : width$$1;
+ height$$1 = (!height$$1 && height$$1 !== 0) ? false : height$$1;
+
+ if (width$$1 === false && height$$1 === false) {
+ return { width: base.width(), height: base.height() };
+ }
+
+ if (width$$1 !== false) {
+ if (save !== false) {
+ options.width = width$$1;
+ }
+
+ width(editorContainer, width$$1);
+ }
+
+ if (height$$1 !== false) {
+ if (save !== false) {
+ options.height = height$$1;
+ }
+
+ height(editorContainer, height$$1);
+ }
+
+ return base;
+ };
+
+ /**
+ * Gets the height of the editor in px
+ *
+ * @since 1.3.5
+ * @function
+ * @memberOf SCEditor.prototype
+ * @name height
+ * @return {number}
+ */
+ /**
+ * Sets the height of the editor
+ *
+ * @param {number} height Height in px
+ * @since 1.3.5
+ * @function
+ * @memberOf 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 {number} height Height in px
+ * @param {boolean} [saveHeight=true] If to store the height
+ * @since 1.4.1
+ * @function
+ * @memberOf SCEditor.prototype
+ * @name height^3
+ * @return {this}
+ */
+ base.height = function (height$$1, saveHeight) {
+ if (!height$$1 && height$$1 !== 0) {
+ return height(editorContainer);
+ }
+
+ base.dimensions(null, height$$1, saveHeight);
+
+ return base;
+ };
+
+ /**
+ * Gets if the editor is maximised or not
+ *
+ * @since 1.4.1
+ * @function
+ * @memberOf 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 SCEditor.prototype
+ * @name maximize^2
+ * @return {this}
+ */
+ base.maximize = function (maximize) {
+ var maximizeSize = 'sceditor-maximize';
+
+ if (isUndefined(maximize)) {
+ return hasClass(editorContainer, maximizeSize);
+ }
+
+ maximize = !!maximize;
+
+ if (maximize) {
+ maximizeScrollPosition = globalWin.pageYOffset;
+ }
+
+ toggleClass(globalDoc.documentElement, maximizeSize, maximize);
+ toggleClass(globalDoc.body, maximizeSize, maximize);
+ toggleClass(editorContainer, maximizeSize, maximize);
+ base.width(maximize ? '100%' : options.width, false);
+ base.height(maximize ? '100%' : options.height, false);
+
+ if (!maximize) {
+ globalWin.scrollTo(0, maximizeScrollPosition);
+ }
+
+ autoExpand();
+
+ return base;
+ };
+
+ autoExpand = function () {
+ if (options.autoExpand && !autoExpandThrottle) {
+ autoExpandThrottle = setTimeout(base.expandToContent, 200);
+ }
+ };
+
+ /**
+ * Expands or shrinks 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 SCEditor.prototype
+ * @see #resizeToContent
+ */
+ base.expandToContent = function (ignoreMaxHeight) {
+ if (base.maximize()) {
+ return;
+ }
+
+ clearTimeout(autoExpandThrottle);
+ autoExpandThrottle = false;
+
+ if (!autoExpandBounds) {
+ var height$$1 = options.resizeMinHeight || options.height ||
+ height(original);
+
+ autoExpandBounds = {
+ min: height$$1,
+ max: options.resizeMaxHeight || (height$$1 * 2)
+ };
+ }
+
+ var range = globalDoc.createRange();
+ range.selectNodeContents(wysiwygBody);
+
+ var rect = range.getBoundingClientRect();
+ var current = wysiwygDocument.documentElement.clientHeight - 1;
+ var spaceNeeded = rect.bottom - rect.top;
+ var newHeight = base.height() + 1 + (spaceNeeded - current);
+
+ if (!ignoreMaxHeight && autoExpandBounds.max !== -1) {
+ newHeight = Math.min(newHeight, autoExpandBounds.max);
+ }
+
+ base.height(Math.ceil(Math.max(newHeight, autoExpandBounds.min)));
+ };
+
+ /**
+ * Destroys the editor, removing all elements and
+ * event handlers.
+ *
+ * Leaves only the original textarea.
+ *
+ * @function
+ * @name destroy
+ * @memberOf 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) {
+ remove(dropdown);
+ }
+
+ off(globalDoc, 'click', handleDocumentClick);
+
+ // TODO: make off support null nodes?
+ var form = original.form;
+ if (form) {
+ off(form, 'reset', handleFormReset);
+ off(form, 'submit', base.updateOriginal);
+ }
+
+ remove(sourceEditor);
+ remove(toolbar);
+ remove(editorContainer);
+
+ delete original._sceditor;
+ show(original);
+
+ original.required = isRequired;
+ };
+
+
+ /**
+ * 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 {boolean} 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 SCEditor.prototype
+ */
+ base.createDropDown = function (menuItem, name, content, ieFix) {
+ // first click for create second click for close
+ var dropDownCss,
+ dropDownClass = 'sceditor-' + name;
+
+ // Will re-focus the editor. This is needed for IE
+ // as it has special logic to save/restore the selection
+ base.closeDropDown(true);
+
+ // Only close the dropdown if it was already open
+ if (dropdown && hasClass(dropdown, dropDownClass)) {
+ 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) {
+ each(find(content, ':not(input):not(textarea)'),
+ function (_, node) {
+ if (node.nodeType === ELEMENT_NODE) {
+ attr(node, 'unselectable', 'on');
+ }
+ });
+ }
+
+ dropDownCss = extend({
+ top: menuItem.offsetTop,
+ left: menuItem.offsetLeft,
+ marginTop: menuItem.clientHeight
+ }, options.dropDownCss);
+
+ dropdown = createElement('div', {
+ className: 'sceditor-dropdown ' + dropDownClass
+ });
+
+ css(dropdown, dropDownCss);
+ appendChild(dropdown, content);
+ appendChild(editorContainer, dropdown);
+ on(dropdown, '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) {
+ var first = find(dropdown, 'input,textarea')[0];
+ if (first) {
+ first.focus();
+ }
+ }
+ });
+ };
+
+ /**
+ * Handles any document click and closes the dropdown if open
+ * @private
+ */
+ handleDocumentClick = function (e) {
+ // ignore right clicks
+ if (e.which !== 3 && dropdown && !e.defaultPrevented) {
+ autoUpdate();
+
+ base.closeDropDown();
+ }
+ };
+
+ /**
+ * Handles the WYSIWYG editors paste event
+ * @private
+ */
+ handlePasteEvt = function (e) {
+ var isIeOrEdge = IE_VER || edge;
+ var editable = wysiwygBody;
+ var clipboard = e.clipboardData;
+ var loadImage = function (file) {
+ var reader = new FileReader();
+ reader.onload = function (e) {
+ handlePasteData({
+ html: ' '
+ });
+ };
+ reader.readAsDataURL(file);
+ };
+
+ // Modern browsers with clipboard API - everything other than _very_
+ // old android web views and UC browser which doesn't support the
+ // paste event at all.
+ if (clipboard && !isIeOrEdge) {
+ var data$$1 = {};
+ var types = clipboard.types;
+ var items = clipboard.items;
+
+ e.preventDefault();
+
+ for (var i = 0; i < types.length; i++) {
+ // Normalise image pasting to paste as a data-uri
+ if (globalWin.FileReader && items &&
+ IMAGE_MIME_REGEX.test(items[i].type)) {
+ return loadImage(clipboard.items[i].getAsFile());
+ }
+
+ data$$1[types[i]] = clipboard.getData(types[i]);
+ }
+ // Call plugins here with file?
+ data$$1.text = data$$1['text/plain'];
+ data$$1.html = data$$1['text/html'];
+
+ handlePasteData(data$$1);
+ // If contentsFragment exists then we are already waiting for a
+ // previous paste so let the handler for that handle this one too
+ } else if (!pasteContentFragment) {
+ // Save the scroll position so can be restored
+ // when contents is restored
+ var scrollTop = editable.scrollTop;
+
+ rangeHelper.saveRange();
+
+ pasteContentFragment = globalDoc.createDocumentFragment();
+ while (editable.firstChild) {
+ appendChild(pasteContentFragment, editable.firstChild);
+ }
+
+ setTimeout(function () {
+ var html = editable.innerHTML;
+
+ editable.innerHTML = '';
+ appendChild(editable, pasteContentFragment);
+ editable.scrollTop = scrollTop;
+ pasteContentFragment = false;
+
+ rangeHelper.restoreRange();
+
+ handlePasteData({ html: html });
+ }, 0);
+ }
+ };
+
+ /**
+ * Gets the pasted data, filters it and then inserts it.
+ * @param {Object} data
+ * @private
+ */
+ handlePasteData = function (data$$1) {
+ var pasteArea = createElement('div', {}, wysiwygDocument);
+
+ pluginManager.call('pasteRaw', data$$1);
+ trigger(editorContainer, 'pasteraw', data$$1);
+
+ if (data$$1.html) {
+ pasteArea.innerHTML = data$$1.html;
+
+ // fix any invalid nesting
+ fixNesting(pasteArea);
+ } else {
+ pasteArea.innerHTML = entities(data$$1.text || '');
+ }
+
+ var paste = {
+ val: pasteArea.innerHTML
+ };
+
+ if ('fragmentToSource' in format) {
+ paste.val = format
+ .fragmentToSource(paste.val, wysiwygDocument, currentNode);
+ }
+
+ pluginManager.call('paste', paste);
+ trigger(editorContainer, 'paste', paste);
+
+ if ('fragmentToHtml' in format) {
+ paste.val = format
+ .fragmentToHtml(paste.val, currentNode);
+ }
+
+ pluginManager.call('pasteHtml', paste);
+
+ base.wysiwygEditorInsertHtml(paste.val, null, true);
+ };
+
+ /**
+ * Closes any currently open drop down
+ *
+ * @param {boolean} [focus=false] If to focus the editor
+ * after closing the drop down
+ * @function
+ * @name closeDropDown
+ * @memberOf SCEditor.prototype
+ */
+ base.closeDropDown = function (focus) {
+ if (dropdown) {
+ remove(dropdown);
+ dropdown = null;
+ }
+
+ if (focus === true) {
+ base.focus();
+ }
+ };
+
+
+ /**
+ * 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 SCEditor.prototype
+ */
+ base.wysiwygEditorInsertHtml = function (
+ html, endHtml, overrideCodeBlocking
+ ) {
+ var marker, scrollTop, scrollTo,
+ editorHeight = height(wysiwygEditor);
+
+ 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 && closest(currentBlockNode, 'code')) {
+ 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();
+
+ // Scroll the editor after the end of the selection
+ marker = find(wysiwygBody, '#sceditor-end-marker')[0];
+ show(marker);
+ scrollTop = wysiwygBody.scrollTop;
+ scrollTo = (getOffset(marker).top +
+ (marker.offsetHeight * 1.5)) - editorHeight;
+ hide(marker);
+
+ // Only scroll if marker isn't already visible
+ if (scrollTo > scrollTop || scrollTo + editorHeight < scrollTop) {
+ wysiwygBody.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 SCEditor.prototype
+ */
+ base.wysiwygEditorInsertText = function (text, endText) {
+ base.wysiwygEditorInsertHtml(
+ entities(text), 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 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 SCEditor.prototype
+ */
+ base.sourceEditorInsertText = function (text, endText) {
+ var scrollTop, currentValue,
+ startPos = sourceEditor.selectionStart,
+ endPos = sourceEditor.selectionEnd;
+
+ scrollTop = sourceEditor.scrollTop;
+ sourceEditor.focus();
+ 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;
+
+ sourceEditor.scrollTop = scrollTop;
+ sourceEditor.focus();
+
+ triggerValueChanged();
+ };
+
+ /**
+ * Gets the current instance of the rangeHelper class
+ * for the editor.
+ *
+ * @return {RangeHelper}
+ * @function
+ * @name getRangeHelper
+ * @memberOf SCEditor.prototype
+ */
+ base.getRangeHelper = function () {
+ return rangeHelper;
+ };
+
+ /**
+ * Gets or sets the source editor caret position.
+ *
+ * @param {Object} [position]
+ * @return {this}
+ * @function
+ * @since 1.4.5
+ * @name sourceEditorCaret
+ * @memberOf SCEditor.prototype
+ */
+ base.sourceEditorCaret = function (position) {
+ sourceEditor.focus();
+
+ if (position) {
+ sourceEditor.selectionStart = position.start;
+ sourceEditor.selectionEnd = position.end;
+
+ return this;
+ }
+
+ return {
+ start: sourceEditor.selectionStart,
+ end: sourceEditor.selectionEnd
+ };
+ };
+
+ /**
+ * 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 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 SCEditor.prototype
+ */
+ base.val = function (val, filter) {
+ if (!isString(val)) {
+ return base.inSourceMode() ?
+ base.getSourceEditorValue(false) :
+ base.getWysiwygEditorValue(filter);
+ }
+
+ if (!base.inSourceMode()) {
+ if (filter !== false && 'toHtml' in format) {
+ val = format.toHtml(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 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 SCEditor.prototype
+ */
+ // eslint-disable-next-line max-params
+ base.insert = function (
+ 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();
+
+ if (filter !== false && 'fragmentToSource' in format) {
+ html = format
+ .fragmentToSource(html, wysiwygDocument, currentNode);
+ }
+
+ start += html + end;
+ }
+ // TODO: This filter should allow empty tags as it's inserting.
+ if (filter !== false && 'fragmentToHtml' in format) {
+ start = format.fragmentToHtml(start, currentNode);
+ }
+
+ // 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 {boolean} [filter=true]
+ * @return {string}
+ * @function
+ * @name getWysiwygEditorValue
+ * @memberOf 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 = createElement('div', {}, wysiwygDocument);
+ var childNodes = wysiwygBody.childNodes;
+
+ for (var i = 0; i < childNodes.length; i++) {
+ appendChild(tmp, childNodes[i].cloneNode(true));
+ }
+
+ appendChild(wysiwygBody, tmp);
+ fixNesting(tmp);
+ remove(tmp);
+
+ html = tmp.innerHTML;
+
+ // filter the HTML and DOM through any plugins
+ if (filter !== false && format.hasOwnProperty('toSource')) {
+ html = format.toSource(html, wysiwygDocument);
+ }
+
+ return html;
+ };
+
+ /**
+ * Gets the WYSIWYG editor's iFrame Body.
+ *
+ * @return {HTMLElement}
+ * @function
+ * @since 1.4.3
+ * @name getBody
+ * @memberOf SCEditor.prototype
+ */
+ base.getBody = function () {
+ return wysiwygBody;
+ };
+
+ /**
+ * Gets the WYSIWYG editors container area (whole iFrame).
+ *
+ * @return {HTMLElement}
+ * @function
+ * @since 1.4.3
+ * @name getContentAreaContainer
+ * @memberOf 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 {boolean} [filter=true]
+ * @return {string}
+ * @function
+ * @since 1.4.0
+ * @name getSourceEditorValue
+ * @memberOf SCEditor.prototype
+ */
+ base.getSourceEditorValue = function (filter) {
+ var val = sourceEditor.value;
+
+ if (filter !== false && 'toHtml' in format) {
+ val = format.toHtml(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 SCEditor.prototype
+ */
+ base.setWysiwygEditorValue = function (value) {
+ if (!value) {
+ value = '' + (IE_VER ? '' : ' ') + '
';
+ }
+
+ wysiwygBody.innerHTML = value;
+ replaceEmoticons();
+
+ appendNewLine();
+ triggerValueChanged();
+ autoExpand();
+ };
+
+ /**
+ * Sets the text editor value
+ *
+ * @param {string} value
+ * @function
+ * @name setSourceEditorValue
+ * @memberOf SCEditor.prototype
+ */
+ base.setSourceEditorValue = function (value) {
+ sourceEditor.value = 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 SCEditor.prototype
+ */
+ base.updateOriginal = function () {
+ original.value = base.val();
+ };
+
+ /**
+ * Replaces any emoticon codes in the passed HTML
+ * with their emoticon images
+ * @private
+ */
+ replaceEmoticons = function () {
+ if (options.emoticonsEnabled) {
+ replace(wysiwygBody, allEmoticons, options.emoticonsCompat);
+ }
+ };
+
+ /**
+ * If the editor is in source code mode
+ *
+ * @return {boolean}
+ * @function
+ * @name inSourceMode
+ * @memberOf SCEditor.prototype
+ */
+ base.inSourceMode = function () {
+ return hasClass(editorContainer, 'sourceMode');
+ };
+
+ /**
+ * Gets if the editor is in sourceMode
+ *
+ * @return boolean
+ * @function
+ * @name sourceMode
+ * @memberOf SCEditor.prototype
+ */
+ /**
+ * Sets if the editor is in sourceMode
+ *
+ * @param {boolean} enable
+ * @return {this}
+ * @function
+ * @name sourceMode^2
+ * @memberOf 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 SCEditor.prototype
+ */
+ base.toggleSourceMode = function () {
+ var isInSourceMode = base.inSourceMode();
+
+ // don't allow switching to WYSIWYG if doesn't support it
+ if (!isWysiwygSupported && isInSourceMode) {
+ return;
+ }
+
+ if (!isInSourceMode) {
+ rangeHelper.saveRange();
+ rangeHelper.clear();
+ }
+
+ base.blur();
+
+ if (isInSourceMode) {
+ base.setWysiwygEditorValue(base.getSourceEditorValue());
+ } else {
+ base.setSourceEditorValue(base.getWysiwygEditorValue());
+ }
+
+ lastRange = null;
+ toggle(sourceEditor);
+ toggle(wysiwygEditor);
+
+ toggleClass(editorContainer, 'wysiwygMode', isInSourceMode);
+ toggleClass(editorContainer, 'sourceMode', !isInSourceMode);
+
+ updateToolBar();
+ updateActiveButtons();
+ };
+
+ /**
+ * Gets the selected text of the source editor
+ * @return {string}
+ * @private
+ */
+ sourceEditorSelectedText = function () {
+ sourceEditor.focus();
+
+ return sourceEditor.value.substring(
+ sourceEditor.selectionStart,
+ sourceEditor.selectionEnd
+ );
+ };
+
+ /**
+ * 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 (Array.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 SCEditor.prototype
+ */
+ base.execCommand = function (command, param) {
+ var executed = false,
+ commandObj = base.commands[command];
+
+ base.focus();
+
+ // TODO: make configurable
+ // don't apply any commands to code elements
+ if (closest(rangeHelper.parentNode(), 'code')) {
+ return;
+ }
+
+ try {
+ executed = wysiwygDocument.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() {
+ // Don't create new selection if there isn't one (like after
+ // blur event in iOS)
+ if (wysiwygWindow.getSelection() &&
+ wysiwygWindow.getSelection().rangeCount <= 0) {
+ currentSelection = null;
+ // rangeHelper could be null if editor was destroyed
+ // before the timeout had finished
+ } else if (rangeHelper && !rangeHelper.compare(currentSelection)) {
+ currentSelection = rangeHelper.cloneSelected();
+
+ // If the selection is in an inline wrap it in a block.
+ // Fixes #331
+ if (currentSelection && currentSelection.collapsed) {
+ var parent$$1 = currentSelection.startContainer;
+ var offset = currentSelection.startOffset;
+
+ // Handle if selection is placed before/after an element
+ if (offset && parent$$1.nodeType !== TEXT_NODE) {
+ parent$$1 = parent$$1.childNodes[offset];
+ }
+
+ while (parent$$1 && parent$$1.parentNode !== wysiwygBody) {
+ parent$$1 = parent$$1.parentNode;
+ }
+
+ if (parent$$1 && isInline(parent$$1, true)) {
+ rangeHelper.saveRange();
+ wrapInlines(wysiwygBody, wysiwygDocument);
+ rangeHelper.restoreRange();
+ }
+ }
+
+ trigger(editorContainer, 'selectionchanged');
+ }
+
+ isSelectionCheckPending = false;
+ }
+
+ if (isSelectionCheckPending) {
+ return;
+ }
+
+ isSelectionCheckPending = true;
+
+ // Don't need to limit checking if browser supports the Selection API
+ if ('onselectionchange' in wysiwygDocument) {
+ 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);
+
+ trigger(editorContainer, '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 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 SCEditor.prototype
+ * @since 1.4.4
+ */
+ base.currentBlockNode = function () {
+ return currentBlockNode;
+ };
+
+ /**
+ * Updates if buttons are active or not
+ * @private
+ */
+ updateActiveButtons = function () {
+ var firstBlock, parent$$1;
+ var activeClass = 'active';
+ var doc = wysiwygDocument;
+ var isSource = base.sourceMode();
+
+ if (base.readOnly()) {
+ each(find(toolbar, activeClass), function (_, menuItem) {
+ removeClass(menuItem, activeClass);
+ });
+ return;
+ }
+
+ if (!isSource) {
+ parent$$1 = rangeHelper.parentNode();
+ firstBlock = rangeHelper.getFirstBlockParent(parent$$1);
+ }
+
+ for (var j = 0; j < btnStateHandlers.length; j++) {
+ var state = 0;
+ var btn = toolbarButtons[btnStateHandlers[j].name];
+ var stateFn = btnStateHandlers[j].state;
+ var isDisabled = (isSource && !btn._sceTxtMode) ||
+ (!isSource && !btn._sceWysiwygMode);
+
+ if (isString(stateFn)) {
+ if (!isSource) {
+ try {
+ state = doc.queryCommandEnabled(stateFn) ? 0 : -1;
+
+ // eslint-disable-next-line max-depth
+ if (state > -1) {
+ state = doc.queryCommandState(stateFn) ? 1 : 0;
+ }
+ } catch (ex) {}
+ }
+ } else if (!isDisabled) {
+ state = stateFn.call(base, parent$$1, firstBlock);
+ }
+
+ toggleClass(btn, 'disabled', isDisabled || state < 0);
+ toggleClass(btn, activeClass, state > 0);
+ }
+
+ if (icons && icons.update) {
+ icons.update(isSource, parent$$1, firstBlock);
+ }
+ };
+
+ /**
+ * Handles any key press in the WYSIWYG editor
+ *
+ * @private
+ */
+ handleKeyPress = function (e) {
+ // FF bug: https://bugzilla.mozilla.org/show_bug.cgi?id=501496
+ if (e.defaultPrevented) {
+ return;
+ }
+
+ base.closeDropDown();
+
+ // 13 = enter key
+ if (e.which === 13) {
+ var LIST_TAGS = 'li,ul,ol';
+
+ // "Fix" (cludge) for blocklevel elements being duplicated in some
+ // browsers when enter is pressed instead of inserting a newline
+ if (!is(currentBlockNode, LIST_TAGS) &&
+ hasStyling(currentBlockNode)) {
+ lastRange = null;
+
+ var br = createElement('br', {}, wysiwygDocument);
+ 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$2) {
+ var parent$$1 = br.parentNode;
+ var lastChild = parent$$1.lastChild;
+
+ // Sometimes an empty next node is created after the
+ if (lastChild && lastChild.nodeType === TEXT_NODE &&
+ lastChild.nodeValue === '') {
+ remove(lastChild);
+ lastChild = parent$$1.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 (!isInline(parent$$1, true) && lastChild === br &&
+ isInline(br.previousSibling)) {
+ rangeHelper.insertHTML(' ');
+ }
+ }
+
+ e.preventDefault();
+ }
+ }
+ };
+
+ /**
+ * 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 () {
+ // Check all nodes in reverse until either add a new line
+ // or reach a non-empty textnode or BR at which point can
+ // stop checking.
+ rTraverse(wysiwygBody, function (node) {
+ // Last block, add new line after if has styling
+ if (node.nodeType === ELEMENT_NODE &&
+ !/inline/.test(css(node, 'display'))) {
+
+ // Add line break after if has styling
+ if (!is(node, '.sceditor-nlf') && hasStyling(node)) {
+ var paragraph = createElement('p', {}, wysiwygDocument);
+ paragraph.className = 'sceditor-nlf';
+ paragraph.innerHTML = !IE_BR_FIX$2 ? ' ' : '';
+ appendChild(wysiwygBody, paragraph);
+ return false;
+ }
+ }
+
+ // Last non-empty text node or line break.
+ // No need to add line-break after them
+ if ((node.nodeType === 3 && !/^\s*$/.test(node.nodeValue)) ||
+ is(node, 'br')) {
+ return false;
+ }
+ });
+ };
+
+ /**
+ * Handles form reset event
+ * @private
+ */
+ handleFormReset = function () {
+ base.val(original.value);
+ };
+
+ /**
+ * Handles any mousedown press in the WYSIWYG editor
+ * @private
+ */
+ handleMouseDown = function () {
+ base.closeDropDown();
+ lastRange = null;
+ };
+
+ /**
+ * 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 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) {
+ if (pluginManager) {
+ // Send event to all plugins
+ pluginManager.call(e.type + 'Event', e, base);
+ }
+
+ // convert the event into a custom event to send
+ var name = (e.target === sourceEditor ? 'scesrc' : 'scewys') + e.type;
+
+ if (eventHandlers[name]) {
+ eventHandlers[name].forEach(function (fn) {
+ fn.call(base, e);
+ });
+ }
+ };
+
+ /**
+ * 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 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)) {
+ var wysEvent = 'scewys' + events[i];
+ var srcEvent = 'scesrc' + events[i];
+ // Use custom events to allow passing the instance as the
+ // 2nd argument.
+ // Also allows unbinding without unbinding the editors own
+ // event handlers.
+ if (!excludeWysiwyg) {
+ eventHandlers[wysEvent] = eventHandlers[wysEvent] || [];
+ eventHandlers[wysEvent].push(handler);
+ }
+
+ if (!excludeSource) {
+ eventHandlers[srcEvent] = eventHandlers[srcEvent] || [];
+ eventHandlers[srcEvent].push(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 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) {
+ arrayRemove(
+ eventHandlers['scewys' + events[i]] || [], handler);
+ }
+
+ if (!excludeSource) {
+ arrayRemove(
+ eventHandlers['scesrc' + events[i]] || [], handler);
+ }
+ }
+ }
+
+ return base;
+ };
+
+ /**
+ * Blurs the editors input area
+ *
+ * @return {this}
+ * @function
+ * @name blur
+ * @memberOf 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 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 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 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()) {
+ // Already has focus so do nothing
+ if (find(wysiwygDocument, ':focus').length) {
+ return;
+ }
+
+ var container;
+ var 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) {
+ 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$2 && rng && rng.endOffset === 1 && rng.collapsed) {
+ container = rng.endContainer;
+
+ if (container && container.childNodes.length === 1 &&
+ is(container.firstChild, 'br')) {
+ rng.setStartBefore(container.firstChild);
+ rng.collapse(true);
+ rangeHelper.selectRange(rng);
+ }
+ }
+
+ wysiwygWindow.focus();
+ wysiwygBody.focus();
+
+ // Needed for IE
+ 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 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 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 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 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 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 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 (closest(currentBlockNode, 'code')) {
+ return;
+ }
+
+ if (!emoticonsCache) {
+ emoticonsCache = [];
+
+ each(allEmoticons, function (key, html) {
+ emoticonsCache[cachePos++] = [key, html];
+ });
+
+ 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) {
+ if (!options.emoticonsCompat || !/^\s$/.test(curChar)) {
+ e.preventDefault();
+ }
+ }
+ };
+
+ /**
+ * Makes sure emoticons are surrounded by whitespace
+ * @private
+ */
+ emoticonsCheckWhitespace = function () {
+ checkWhitespace(currentBlockNode, rangeHelper);
+ };
+
+ /**
+ * Gets if emoticons are currently enabled
+ * @return {boolean}
+ * @function
+ * @name emoticons
+ * @memberOf SCEditor.prototype
+ * @since 1.4.2
+ */
+ /**
+ * Enables/disables emoticons
+ *
+ * @param {boolean} enable
+ * @return {this}
+ * @function
+ * @name emoticons^2
+ * @memberOf SCEditor.prototype
+ * @since 1.4.2
+ */
+ base.emoticons = function (enable) {
+ if (!enable && enable !== false) {
+ return options.emoticonsEnabled;
+ }
+
+ options.emoticonsEnabled = enable;
+
+ if (enable) {
+ on(wysiwygBody, 'keypress', emoticonsKeyPress);
+
+ if (!base.sourceMode()) {
+ rangeHelper.saveRange();
+
+ replaceEmoticons();
+ triggerValueChanged(false);
+
+ rangeHelper.restoreRange();
+ }
+ } else {
+ var emoticons =
+ find(wysiwygBody, 'img[data-sceditor-emoticon]');
+
+ each(emoticons, function (_, img) {
+ var text = data(img, 'sceditor-emoticon');
+ var textNode = wysiwygDocument.createTextNode(text);
+ img.parentNode.replaceChild(textNode, img);
+ });
+
+ off(wysiwygBody, 'keypress', emoticonsKeyPress);
+
+ triggerValueChanged();
+ }
+
+ return base;
+ };
+
+ /**
+ * Gets the current WYSIWYG editors inline CSS
+ *
+ * @return {string}
+ * @function
+ * @name css
+ * @memberOf SCEditor.prototype
+ * @since 1.4.3
+ */
+ /**
+ * Sets inline CSS for the WYSIWYG editor
+ *
+ * @param {string} css
+ * @return {this}
+ * @function
+ * @name css^2
+ * @memberOf SCEditor.prototype
+ * @since 1.4.3
+ */
+ base.css = function (css$$1) {
+ if (!inlineCss) {
+ inlineCss = createElement('style', {
+ id: 'inline'
+ }, wysiwygDocument);
+
+ appendChild(wysiwygDocument.head, inlineCss);
+ }
+
+ if (!isString(css$$1)) {
+ return inlineCss.styleSheet ?
+ inlineCss.styleSheet.cssText : inlineCss.innerHTML;
+ }
+
+ if (inlineCss.styleSheet) {
+ inlineCss.styleSheet.cssText = css$$1;
+ } else {
+ inlineCss.innerHTML = css$$1;
+ }
+
+ return base;
+ };
+
+ /**
+ * Handles the keydown event, used for shortcuts
+ * @private
+ */
+ handleKeyDown = function (e) {
+ var shortcut = [],
+ SHIFT_KEYS = {
+ '`': '~',
+ '1': '!',
+ '2': '@',
+ '3': '#',
+ '4': '$',
+ '5': '%',
+ '6': '^',
+ '7': '&',
+ '8': '*',
+ '9': '(',
+ '0': ')',
+ '-': '_',
+ '=': '+',
+ ';': ': ',
+ '\'': '"',
+ ',': '<',
+ '.': '>',
+ '/': '?',
+ '\\': '|',
+ '[': '{',
+ ']': '}'
+ },
+ SPECIAL_KEYS = {
+ 8: 'backspace',
+ 9: 'tab',
+ 13: 'enter',
+ 19: 'pause',
+ 20: 'capslock',
+ 27: 'esc',
+ 32: 'space',
+ 33: 'pageup',
+ 34: 'pagedown',
+ 35: 'end',
+ 36: 'home',
+ 37: 'left',
+ 38: 'up',
+ 39: 'right',
+ 40: 'down',
+ 45: 'insert',
+ 46: 'del',
+ 91: 'win',
+ 92: 'win',
+ 93: 'select',
+ 96: '0',
+ 97: '1',
+ 98: '2',
+ 99: '3',
+ 100: '4',
+ 101: '5',
+ 102: '6',
+ 103: '7',
+ 104: '8',
+ 105: '9',
+ 106: '*',
+ 107: '+',
+ 109: '-',
+ 110: '.',
+ 111: '/',
+ 112: 'f1',
+ 113: 'f2',
+ 114: 'f3',
+ 115: 'f4',
+ 116: 'f5',
+ 117: 'f6',
+ 118: 'f7',
+ 119: 'f8',
+ 120: 'f9',
+ 121: 'f10',
+ 122: 'f11',
+ 123: 'f12',
+ 144: 'numlock',
+ 145: 'scrolllock',
+ 186: ';',
+ 187: '=',
+ 188: ',',
+ 189: '-',
+ 190: '.',
+ 191: '/',
+ 192: '`',
+ 219: '[',
+ 220: '\\',
+ 221: ']',
+ 222: '\''
+ },
+ NUMPAD_SHIFT_KEYS = {
+ 109: '-',
+ 110: 'del',
+ 111: '/',
+ 96: '0',
+ 97: '1',
+ 98: '2',
+ 99: '3',
+ 100: '4',
+ 101: '5',
+ 102: '6',
+ 103: '7',
+ 104: '8',
+ 105: '9'
+ },
+ which = e.which,
+ character = SPECIAL_KEYS[which] ||
+ String.fromCharCode(which).toLowerCase();
+
+ if (e.ctrlKey || e.metaKey) {
+ shortcut.push('ctrl');
+ }
+
+ if (e.altKey) {
+ shortcut.push('alt');
+ }
+
+ if (e.shiftKey) {
+ shortcut.push('shift');
+
+ if (NUMPAD_SHIFT_KEYS[which]) {
+ character = NUMPAD_SHIFT_KEYS[which];
+ } else if (SHIFT_KEYS[character]) {
+ character = SHIFT_KEYS[character];
+ }
+ }
+
+ // Shift is 16, ctrl is 17 and alt is 18
+ if (character && (which < 16 || which > 18)) {
+ shortcut.push(character);
+ }
+
+ shortcut = shortcut.join('+');
+ if (shortcutHandlers[shortcut] &&
+ shortcutHandlers[shortcut].call(base) === false) {
+
+ e.stopPropagation();
+ e.preventDefault();
+ }
+ };
+
+ /**
+ * Adds a shortcut handler to the editor
+ * @param {string} shortcut
+ * @param {String|Function} cmd
+ * @return {sceditor}
+ */
+ base.addShortcut = function (shortcut, cmd) {
+ shortcut = shortcut.toLowerCase();
+
+ if (isString(cmd)) {
+ shortcutHandlers[shortcut] = function () {
+ handleCommand(toolbarButtons[cmd], base.commands[cmd]);
+
+ return false;
+ };
+ } else {
+ shortcutHandlers[shortcut] = cmd;
+ }
+
+ return base;
+ };
+
+ /**
+ * Removes a shortcut handler
+ * @param {string} shortcut
+ * @return {sceditor}
+ */
+ base.removeShortcut = function (shortcut) {
+ delete shortcutHandlers[shortcut.toLowerCase()];
+
+ return base;
+ };
+
+ /**
+ * Handles the backspace key press
+ *
+ * Will remove block styling like quotes/code ect if at the start.
+ * @private
+ */
+ handleBackSpace = function (e) {
+ var node, offset, range, parent$$1;
+
+ // 8 is the backspace key
+ if (options.disableBlockRemove || e.which !== 8 ||
+ !(range = rangeHelper.selectedRange())) {
+ return;
+ }
+
+ node = range.startContainer;
+ offset = range.startOffset;
+
+ if (offset !== 0 || !(parent$$1 = currentStyledBlockNode()) ||
+ is(parent$$1, 'body')) {
+ return;
+ }
+
+ while (node !== parent$$1) {
+ while (node.previousSibling) {
+ node = node.previousSibling;
+
+ // Everything but empty text nodes before the cursor
+ // should prevent the style from being removed
+ if (node.nodeType !== TEXT_NODE || node.nodeValue) {
+ return;
+ }
+ }
+
+ if (!(node = node.parentNode)) {
+ return;
+ }
+ }
+
+ // The backspace was pressed at the start of
+ // the container so clear the style
+ base.clearBlockFormatting(parent$$1);
+ e.preventDefault();
+ };
+
+ /**
+ * Gets the first styled block node that contains the cursor
+ * @return {HTMLElement}
+ */
+ currentStyledBlockNode = function () {
+ var block = currentBlockNode;
+
+ while (!hasStyling(block) || isInline(block, true)) {
+ if (!(block = block.parentNode) || is(block, 'body')) {
+ return;
+ }
+ }
+
+ return block;
+ };
+
+ /**
+ * Clears the formatting of the passed block element.
+ *
+ * If block is false, if will clear the styling of the first
+ * block level element that contains the cursor.
+ * @param {HTMLElement} block
+ * @since 1.4.4
+ */
+ base.clearBlockFormatting = function (block) {
+ block = block || currentStyledBlockNode();
+
+ if (!block || is(block, 'body')) {
+ return base;
+ }
+
+ rangeHelper.saveRange();
+
+ block.className = '';
+ lastRange = null;
+
+ attr(block, 'style', '');
+
+ if (!is(block, 'p,div,td')) {
+ convertElement(block, 'p');
+ }
+
+ rangeHelper.restoreRange();
+ return base;
+ };
+
+ /**
+ * Triggers the valueChanged signal if there is
+ * a plugin that handles it.
+ *
+ * If rangeHelper.saveRange() has already been
+ * called, then saveRange should be set to false
+ * to prevent the range being saved twice.
+ *
+ * @since 1.4.5
+ * @param {boolean} saveRange If to call rangeHelper.saveRange().
+ * @private
+ */
+ triggerValueChanged = function (saveRange) {
+ if (!pluginManager ||
+ (!pluginManager.hasHandler('valuechangedEvent') &&
+ !triggerValueChanged.hasHandler)) {
+ return;
+ }
+
+ var currentHtml,
+ sourceMode = base.sourceMode(),
+ hasSelection = !sourceMode && rangeHelper.hasSelection();
+
+ // Composition end isn't guaranteed to fire but must have
+ // ended when triggerValueChanged() is called so reset it
+ isComposing = false;
+
+ // Don't need to save the range if sceditor-start-marker
+ // is present as the range is already saved
+ saveRange = saveRange !== false &&
+ !wysiwygDocument.getElementById('sceditor-start-marker');
+
+ // Clear any current timeout as it's now been triggered
+ if (valueChangedKeyUpTimer) {
+ clearTimeout(valueChangedKeyUpTimer);
+ valueChangedKeyUpTimer = false;
+ }
+
+ if (hasSelection && saveRange) {
+ rangeHelper.saveRange();
+ }
+
+ currentHtml = sourceMode ? sourceEditor.value : wysiwygBody.innerHTML;
+
+ // Only trigger if something has actually changed.
+ if (currentHtml !== triggerValueChanged.lastVal) {
+ triggerValueChanged.lastVal = currentHtml;
+
+ trigger(editorContainer, 'valuechanged', {
+ rawValue: sourceMode ? base.val() : currentHtml
+ });
+ }
+
+ if (hasSelection && saveRange) {
+ rangeHelper.removeMarkers();
+ }
+ };
+
+ /**
+ * Should be called whenever there is a blur event
+ * @private
+ */
+ valueChangedBlur = function () {
+ if (valueChangedKeyUpTimer) {
+ triggerValueChanged();
+ }
+ };
+
+ /**
+ * Should be called whenever there is a keypress event
+ * @param {Event} e The keypress event
+ * @private
+ */
+ valueChangedKeyUp = function (e) {
+ var which = e.which,
+ lastChar = valueChangedKeyUp.lastChar,
+ lastWasSpace = (lastChar === 13 || lastChar === 32),
+ lastWasDelete = (lastChar === 8 || lastChar === 46);
+
+ valueChangedKeyUp.lastChar = which;
+
+ if (isComposing) {
+ return;
+ }
+
+ // 13 = return & 32 = space
+ if (which === 13 || which === 32) {
+ if (!lastWasSpace) {
+ triggerValueChanged();
+ } else {
+ valueChangedKeyUp.triggerNext = true;
+ }
+ // 8 = backspace & 46 = del
+ } else if (which === 8 || which === 46) {
+ if (!lastWasDelete) {
+ triggerValueChanged();
+ } else {
+ valueChangedKeyUp.triggerNext = true;
+ }
+ } else if (valueChangedKeyUp.triggerNext) {
+ triggerValueChanged();
+ valueChangedKeyUp.triggerNext = false;
+ }
+
+ // Clear the previous timeout and set a new one.
+ clearTimeout(valueChangedKeyUpTimer);
+
+ // Trigger the event 1.5s after the last keypress if space
+ // isn't pressed. This might need to be lowered, will need
+ // to look into what the slowest average Chars Per Min is.
+ valueChangedKeyUpTimer = setTimeout(function () {
+ if (!isComposing) {
+ triggerValueChanged();
+ }
+ }, 1500);
+ };
+
+ handleComposition = function (e) {
+ isComposing = /start/i.test(e.type);
+
+ if (!isComposing) {
+ triggerValueChanged();
+ }
+ };
+
+ autoUpdate = function () {
+ base.updateOriginal();
+ };
+
+ // run the initializer
+ init();
+ }
+
+
+ /**
+ * Map containing the loaded SCEditor locales
+ * @type {Object}
+ * @name locale
+ * @memberOf sceditor
+ */
+ SCEditor.locale = {};
+
+ SCEditor.formats = {};
+ SCEditor.icons = {};
+
+
+ /**
+ * Static command helper class
+ * @class command
+ * @name sceditor.command
+ */
+ SCEditor.command =
+ /** @lends sceditor.command */
+ {
+ /**
+ * Gets a command
+ *
+ * @param {string} name
+ * @return {Object|null}
+ * @since v1.3.5
+ */
+ get: function (name) {
+ return defaultCmds[name] || null;
+ },
+
+ /**
+ * Adds a command to the editor or updates an existing
+ * command if a command with the specified name already exists.
+ *
+ * Once a command is add it can be included in the toolbar by
+ * adding it's name to the toolbar option in the constructor. It
+ * can also be executed manually by calling
+ * {@link sceditor.execCommand}
+ *
+ * @example
+ * SCEditor.command.set("hello",
+ * {
+ * exec: function () {
+ * alert("Hello World!");
+ * }
+ * });
+ *
+ * @param {string} name
+ * @param {Object} cmd
+ * @return {this|false} Returns false if name or cmd is false
+ * @since v1.3.5
+ */
+ set: function (name, cmd) {
+ if (!name || !cmd) {
+ return false;
+ }
+
+ // merge any existing command properties
+ cmd = extend(defaultCmds[name] || {}, cmd);
+
+ cmd.remove = function () {
+ SCEditor.command.remove(name);
+ };
+
+ defaultCmds[name] = cmd;
+ return this;
+ },
+
+ /**
+ * Removes a command
+ *
+ * @param {string} name
+ * @return {this}
+ * @since v1.3.5
+ */
+ remove: function (name) {
+ if (defaultCmds[name]) {
+ delete defaultCmds[name];
+ }
+
+ return this;
+ }
+ };
+
+ /**
+ * SCEditor
+ * http://www.sceditor.com/
+ *
+ * Copyright (C) 2017, 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
+ */
+
+ window.sceditor = {
+ command: SCEditor.command,
+ commands: defaultCmds,
+ defaultOptions: defaultOptions,
+
+ ie: ie,
+ ios: ios,
+ isWysiwygSupported: isWysiwygSupported,
+
+ regexEscape: regex,
+ escapeEntities: entities,
+ escapeUriScheme: uriScheme,
+
+ dom: {
+ css: css,
+ attr: attr,
+ removeAttr: removeAttr,
+ is: is,
+ closest: closest,
+ width: width,
+ height: height,
+ traverse: traverse,
+ rTraverse: rTraverse,
+ parseHTML: parseHTML,
+ hasStyling: hasStyling,
+ convertElement: convertElement,
+ blockLevelList: blockLevelList,
+ canHaveChildren: canHaveChildren,
+ isInline: isInline,
+ copyCSS: copyCSS,
+ fixNesting: fixNesting,
+ findCommonAncestor: findCommonAncestor,
+ getSibling: getSibling,
+ removeWhiteSpace: removeWhiteSpace,
+ extractContents: extractContents,
+ getOffset: getOffset,
+ getStyle: getStyle,
+ hasStyle: hasStyle
+ },
+ locale: SCEditor.locale,
+ icons: SCEditor.icons,
+ utils: {
+ each: each,
+ isEmptyObject: isEmptyObject,
+ extend: extend
+ },
+ plugins: PluginManager.plugins,
+ formats: SCEditor.formats,
+ create: function (textarea, options) {
+ options = options || {};
+
+ // Don't allow the editor to be initialised
+ // on it's own source editor
+ if (parent(textarea, '.sceditor-container')) {
+ return;
+ }
+
+ if (options.runWithoutWysiwygSupport || isWysiwygSupported) {
+ /*eslint no-new: off*/
+ (new SCEditor(textarea, options));
+ }
+ },
+ instance: function (textarea) {
+ return textarea._sceditor;
+ }
+ };
+
+ /**
+ * SCEditor
+ * http://www.sceditor.com/
+ *
+ * Copyright (C) 2017, 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
+ */
+
+ // For backwards compatibility
+ $.sceditor = window.sceditor;
+
+ /**
+ * 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|Array|SCEditor|boolean}
+ */
+ $.fn.sceditor = function (options) {
+ var instance;
+ var ret = [];
+
+ this.each(function () {
+ instance = this._sceditor;
+
+ // 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) {
+ $.sceditor.create(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;
+ };
+
+}(jQuery));
diff --git a/public/assets/development/jquery.sceditor.xhtml.js b/public/assets/development/jquery.sceditor.xhtml.js
new file mode 100644
index 0000000..e9dc609
--- /dev/null
+++ b/public/assets/development/jquery.sceditor.xhtml.js
@@ -0,0 +1,8885 @@
+(function ($) {
+ 'use strict';
+
+ $ = $ && $.hasOwnProperty('default') ? $['default'] : $;
+
+ /**
+ * Check if the passed argument is the
+ * the passed type.
+ *
+ * @param {string} type
+ * @param {*} arg
+ * @returns {boolean}
+ */
+ function isTypeof(type, arg) {
+ return typeof arg === type;
+ }
+
+ /**
+ * @type {function(*): boolean}
+ */
+ var isString = isTypeof.bind(null, 'string');
+
+ /**
+ * @type {function(*): boolean}
+ */
+ var isUndefined = isTypeof.bind(null, 'undefined');
+
+ /**
+ * @type {function(*): boolean}
+ */
+ var isFunction = isTypeof.bind(null, 'function');
+
+ /**
+ * @type {function(*): boolean}
+ */
+ var isNumber = isTypeof.bind(null, 'number');
+
+
+ /**
+ * Returns true if an object has no keys
+ *
+ * @param {!Object} obj
+ * @returns {boolean}
+ */
+ function isEmptyObject(obj) {
+ return !Object.keys(obj).length;
+ }
+
+ /**
+ * Extends the first object with any extra objects passed
+ *
+ * If the first argument is boolean and set to true
+ * it will extend child arrays and objects recursively.
+ *
+ * @param {!Object|boolean} targetArg
+ * @param {...Object} source
+ * @return {Object}
+ */
+ function extend(targetArg, sourceArg) {
+ var isTargetBoolean = targetArg === !!targetArg;
+ var i = isTargetBoolean ? 2 : 1;
+ var target = isTargetBoolean ? sourceArg : targetArg;
+ var isDeep = isTargetBoolean ? targetArg : false;
+
+ for (; i < arguments.length; i++) {
+ var source = arguments[i];
+
+ // Copy all properties for jQuery compatibility
+ /* eslint guard-for-in: off */
+ for (var key in source) {
+ var value = source[key];
+
+ // Skip undefined values to match jQuery and
+ // skip if target to prevent infinite loop
+ if (!isUndefined(value)) {
+ var isObject = value !== null && typeof value === 'object' &&
+ Object.getPrototypeOf(value) === Object.prototype;
+ var isArray = Array.isArray(value);
+
+ if (isDeep && (isObject || isArray)) {
+ target[key] = extend(
+ true,
+ target[key] || (isArray ? [] : {}),
+ value
+ );
+ } else {
+ target[key] = value;
+ }
+ }
+ }
+ }
+
+ return target;
+ }
+
+ /**
+ * Removes an item from the passed array
+ *
+ * @param {!Array} arr
+ * @param {*} item
+ */
+ function arrayRemove(arr, item) {
+ var i = arr.indexOf(item);
+
+ if (i > -1) {
+ arr.splice(i, 1);
+ }
+ }
+
+ /**
+ * Iterates over an array or object
+ *
+ * @param {!Object|Array} obj
+ * @param {function(*, *)} fn
+ */
+ function each(obj, fn) {
+ if (Array.isArray(obj) || 'length' in obj && isNumber(obj.length)) {
+ for (var i = 0; i < obj.length; i++) {
+ fn(i, obj[i]);
+ }
+ } else {
+ Object.keys(obj).forEach(function (key) {
+ fn(key, obj[key]);
+ });
+ }
+ }
+
+ /**
+ * Cache of camelCase CSS property names
+ * @type {Object}
+ */
+ var cssPropertyNameCache = {};
+
+ /**
+ * Node type constant for element nodes
+ *
+ * @type {number}
+ */
+ var ELEMENT_NODE = 1;
+
+ /**
+ * Node type constant for text nodes
+ *
+ * @type {number}
+ */
+ var TEXT_NODE = 3;
+
+ /**
+ * Node type constant for comment nodes
+ *
+ * @type {number}
+ */
+
+
+ /**
+ * Node type document nodes
+ *
+ * @type {number}
+ */
+
+
+ /**
+ * Node type constant for document fragments
+ *
+ * @type {number}
+ */
+
+
+ function toFloat(value) {
+ value = parseFloat(value);
+
+ return isFinite(value) ? value : 0;
+ }
+
+ /**
+ * Creates an element with the specified attributes
+ *
+ * Will create it in the current document unless context
+ * is specified.
+ *
+ * @param {!string} tag
+ * @param {!Object} [attributes]
+ * @param {!Document} [context]
+ * @returns {!HTMLElement}
+ */
+ function createElement(tag, attributes, context) {
+ var node = (context || document).createElement(tag);
+
+ each(attributes || {}, function (key, value) {
+ if (key === 'style') {
+ node.style.cssText = value;
+ } else if (key in node) {
+ node[key] = value;
+ } else {
+ node.setAttribute(key, value);
+ }
+ });
+
+ return node;
+ }
+
+ /**
+ * Returns an array of parents that matches the selector
+ *
+ * @param {!HTMLElement} node
+ * @param {!string} [selector]
+ * @returns {Array}
+ */
+
+
+ /**
+ * Gets the first parent node that matches the selector
+ *
+ * @param {!HTMLElement} node
+ * @param {!string} [selector]
+ * @returns {HTMLElement|undefined}
+ */
+ function parent(node, selector) {
+ var parent = node || {};
+
+ while ((parent = parent.parentNode) && !/(9|11)/.test(parent.nodeType)) {
+ if (!selector || is(parent, selector)) {
+ return parent;
+ }
+ }
+ }
+
+ /**
+ * Checks the passed node and all parents and
+ * returns the first matching node if any.
+ *
+ * @param {!HTMLElement} node
+ * @param {!string} selector
+ * @returns {HTMLElement|undefined}
+ */
+ function closest(node, selector) {
+ return is(node, selector) ? node : parent(node, selector);
+ }
+
+ /**
+ * Removes the node from the DOM
+ *
+ * @param {!HTMLElement} node
+ */
+ function remove(node) {
+ if (node.parentNode) {
+ node.parentNode.removeChild(node);
+ }
+ }
+
+ /**
+ * Appends child to parent node
+ *
+ * @param {!HTMLElement} node
+ * @param {!HTMLElement} child
+ */
+ function appendChild(node, child) {
+ node.appendChild(child);
+ }
+
+ /**
+ * Finds any child nodes that match the selector
+ *
+ * @param {!HTMLElement} node
+ * @param {!string} selector
+ * @returns {NodeList}
+ */
+ function find(node, selector) {
+ return node.querySelectorAll(selector);
+ }
+
+ /**
+ * For on() and off() if to add/remove the event
+ * to the capture phase
+ *
+ * @type {boolean}
+ */
+ var EVENT_CAPTURE = true;
+
+ /**
+ * For on() and off() if to add/remove the event
+ * to the bubble phase
+ *
+ * @type {boolean}
+ */
+
+
+ /**
+ * Adds an event listener for the specified events.
+ *
+ * Events should be a space separated list of events.
+ *
+ * If selector is specified the handler will only be
+ * called when the event target matches the selector.
+ *
+ * @param {!Node} node
+ * @param {string} events
+ * @param {string} [selector]
+ * @param {function(Object)} fn
+ * @param {boolean} [capture=false]
+ * @see off()
+ */
+ // eslint-disable-next-line max-params
+ function on(node, events, selector, fn, capture) {
+ events.split(' ').forEach(function (event) {
+ var handler;
+
+ if (isString(selector)) {
+ handler = fn['_sce-event-' + event + selector] || function (e) {
+ var target = e.target;
+ while (target && target !== node) {
+ if (is(target, selector)) {
+ fn.call(target, e);
+ return;
+ }
+
+ target = target.parentNode;
+ }
+ };
+
+ fn['_sce-event-' + event + selector] = handler;
+ } else {
+ handler = selector;
+ capture = fn;
+ }
+
+ node.addEventListener(event, handler, capture || false);
+ });
+ }
+
+ /**
+ * Removes an event listener for the specified events.
+ *
+ * @param {!Node} node
+ * @param {string} events
+ * @param {string} [selector]
+ * @param {function(Object)} fn
+ * @param {boolean} [capture=false]
+ * @see on()
+ */
+ // eslint-disable-next-line max-params
+ function off(node, events, selector, fn, capture) {
+ events.split(' ').forEach(function (event) {
+ var handler;
+
+ if (isString(selector)) {
+ handler = fn['_sce-event-' + event + selector];
+ } else {
+ handler = selector;
+ capture = fn;
+ }
+
+ node.removeEventListener(event, handler, capture || false);
+ });
+ }
+
+ /**
+ * If only attr param is specified it will get
+ * the value of the attr param.
+ *
+ * If value is specified but null the attribute
+ * will be removed otherwise the attr value will
+ * be set to the passed value.
+ *
+ * @param {!HTMLElement} node
+ * @param {!string} attr
+ * @param {?string} [value]
+ */
+ function attr(node, attr, value) {
+ if (arguments.length < 3) {
+ return node.getAttribute(attr);
+ }
+
+ // eslint-disable-next-line eqeqeq, no-eq-null
+ if (value == null) {
+ removeAttr(node, attr);
+ } else {
+ node.setAttribute(attr, value);
+ }
+ }
+
+ /**
+ * Removes the specified attribute
+ *
+ * @param {!HTMLElement} node
+ * @param {!string} attr
+ */
+ function removeAttr(node, attr) {
+ node.removeAttribute(attr);
+ }
+
+ /**
+ * Sets the passed elements display to none
+ *
+ * @param {!HTMLElement} node
+ */
+ function hide(node) {
+ css(node, 'display', 'none');
+ }
+
+ /**
+ * Sets the passed elements display to default
+ *
+ * @param {!HTMLElement} node
+ */
+ function show(node) {
+ css(node, 'display', '');
+ }
+
+ /**
+ * Toggles an elements visibility
+ *
+ * @param {!HTMLElement} node
+ */
+ function toggle(node) {
+ if (isVisible(node)) {
+ hide(node);
+ } else {
+ show(node);
+ }
+ }
+
+ /**
+ * Gets a computed CSS values or sets an inline CSS value
+ *
+ * Rules should be in camelCase format and not
+ * hyphenated like CSS properties.
+ *
+ * @param {!HTMLElement} node
+ * @param {!Object|string} rule
+ * @param {string|number} [value]
+ * @return {string|number|undefined}
+ */
+ function css(node, rule, value) {
+ if (arguments.length < 3) {
+ if (isString(rule)) {
+ return node.nodeType === 1 ? getComputedStyle(node)[rule] : null;
+ }
+
+ each(rule, function (key, value) {
+ css(node, key, value);
+ });
+ } else {
+ // isNaN returns false for null, false and empty strings
+ // so need to check it's truthy or 0
+ var isNumeric = (value || value === 0) && !isNaN(value);
+ node.style[rule] = isNumeric ? value + 'px' : value;
+ }
+ }
+
+
+ /**
+ * Gets or sets thee data attributes on a node
+ *
+ * Unlike the jQuery version this only stores data
+ * in the DOM attributes which means only strings
+ * can be stored.
+ *
+ * @param {Node} node
+ * @param {string} [key]
+ * @param {string} [value]
+ * @return {Object|undefined}
+ */
+ function data(node, key, value) {
+ var argsLength = arguments.length;
+ var data = {};
+
+ if (node.nodeType === ELEMENT_NODE) {
+ if (argsLength === 1) {
+ each(node.attributes, function (_, attr) {
+ if (/^data\-/i.test(attr.name)) {
+ data[attr.name.substr(5)] = attr.value;
+ }
+ });
+
+ return data;
+ }
+
+ if (argsLength === 2) {
+ return attr(node, 'data-' + key);
+ }
+
+ attr(node, 'data-' + key, String(value));
+ }
+ }
+
+ /**
+ * Checks if node matches the given selector.
+ *
+ * @param {?HTMLElement} node
+ * @param {string} selector
+ * @returns {boolean}
+ */
+ function is(node, selector) {
+ var result = false;
+
+ if (node && node.nodeType === ELEMENT_NODE) {
+ result = (node.matches || node.msMatchesSelector ||
+ node.webkitMatchesSelector).call(node, selector);
+ }
+
+ return result;
+ }
+
+
+ /**
+ * Returns true if node contains child otherwise false.
+ *
+ * This differs from the DOM contains() method in that
+ * if node and child are equal this will return false.
+ *
+ * @param {!Node} node
+ * @param {HTMLElement} child
+ * @returns {boolean}
+ */
+ function contains(node, child) {
+ return node !== child && node.contains && node.contains(child);
+ }
+
+ /**
+ * @param {Node} node
+ * @param {string} [selector]
+ * @returns {?HTMLElement}
+ */
+ function previousElementSibling(node, selector) {
+ var prev = node.previousElementSibling;
+
+ if (selector && prev) {
+ return is(prev, selector) ? prev : null;
+ }
+
+ return prev;
+ }
+
+ /**
+ * @param {!Node} node
+ * @param {!Node} refNode
+ * @returns {Node}
+ */
+ function insertBefore(node, refNode) {
+ return refNode.parentNode.insertBefore(node, refNode);
+ }
+
+ /**
+ * @param {?HTMLElement} node
+ * @returns {!Array.}
+ */
+ function classes(node) {
+ return node.className.trim().split(/\s+/);
+ }
+
+ /**
+ * @param {?HTMLElement} node
+ * @param {string} className
+ * @returns {boolean}
+ */
+ function hasClass(node, className) {
+ return is(node, '.' + className);
+ }
+
+ /**
+ * @param {!HTMLElement} node
+ * @param {string} className
+ */
+ function addClass(node, className) {
+ var classList = classes(node);
+
+ if (classList.indexOf(className) < 0) {
+ classList.push(className);
+ }
+
+ node.className = classList.join(' ');
+ }
+
+ /**
+ * @param {!HTMLElement} node
+ * @param {string} className
+ */
+ function removeClass(node, className) {
+ var classList = classes(node);
+
+ arrayRemove(classList, className);
+
+ node.className = classList.join(' ');
+ }
+
+ /**
+ * Toggles a class on node.
+ *
+ * If state is specified and is truthy it will add
+ * the class.
+ *
+ * If state is specified and is falsey it will remove
+ * the class.
+ *
+ * @param {HTMLElement} node
+ * @param {string} className
+ * @param {boolean} [state]
+ */
+ function toggleClass(node, className, state) {
+ state = isUndefined(state) ? !hasClass(node, className) : state;
+
+ if (state) {
+ addClass(node, className);
+ } else {
+ removeClass(node, className);
+ }
+ }
+
+ /**
+ * Gets or sets the width of the passed node.
+ *
+ * @param {HTMLElement} node
+ * @param {number|string} [value]
+ * @returns {number|undefined}
+ */
+ function width(node, value) {
+ if (isUndefined(value)) {
+ var cs = getComputedStyle(node);
+ var padding = toFloat(cs.paddingLeft) + toFloat(cs.paddingRight);
+ var border = toFloat(cs.borderLeftWidth) + toFloat(cs.borderRightWidth);
+
+ return node.offsetWidth - padding - border;
+ }
+
+ css(node, 'width', value);
+ }
+
+ /**
+ * Gets or sets the height of the passed node.
+ *
+ * @param {HTMLElement} node
+ * @param {number|string} [value]
+ * @returns {number|undefined}
+ */
+ function height(node, value) {
+ if (isUndefined(value)) {
+ var cs = getComputedStyle(node);
+ var padding = toFloat(cs.paddingTop) + toFloat(cs.paddingBottom);
+ var border = toFloat(cs.borderTopWidth) + toFloat(cs.borderBottomWidth);
+
+ return node.offsetHeight - padding - border;
+ }
+
+ css(node, 'height', value);
+ }
+
+ /**
+ * Triggers a custom event with the specified name and
+ * sets the detail property to the data object passed.
+ *
+ * @param {HTMLElement} node
+ * @param {string} eventName
+ * @param {Object} [data]
+ */
+ function trigger(node, eventName, data) {
+ var event;
+
+ if (isFunction(window.CustomEvent)) {
+ event = new CustomEvent(eventName, {
+ bubbles: true,
+ cancelable: true,
+ detail: data
+ });
+ } else {
+ event = node.ownerDocument.createEvent('CustomEvent');
+ event.initCustomEvent(eventName, true, true, data);
+ }
+
+ node.dispatchEvent(event);
+ }
+
+ /**
+ * Returns if a node is visible.
+ *
+ * @param {HTMLElement}
+ * @returns {boolean}
+ */
+ function isVisible(node) {
+ return !!node.getClientRects().length;
+ }
+
+ /**
+ * Convert CSS property names into camel case
+ *
+ * @param {string} string
+ * @returns {string}
+ */
+ function camelCase(string) {
+ return string
+ .replace(/^-ms-/, 'ms-')
+ .replace(/-(\w)/g, function (match, char) {
+ return char.toUpperCase();
+ });
+ }
+
+
+ /**
+ * Loop all child nodes of the passed node
+ *
+ * The function should accept 1 parameter being the node.
+ * If the function returns false the loop will be exited.
+ *
+ * @param {HTMLElement} node
+ * @param {function} func Callback which is called with every
+ * child node as the first argument.
+ * @param {boolean} innermostFirst If the innermost node should be passed
+ * to the function before it's parents.
+ * @param {boolean} siblingsOnly If to only traverse the nodes siblings
+ * @param {boolean} [reverse=false] If to traverse the nodes in reverse
+ */
+ // eslint-disable-next-line max-params
+ function traverse(node, func, innermostFirst, siblingsOnly, reverse) {
+ node = reverse ? node.lastChild : node.firstChild;
+
+ while (node) {
+ var next = reverse ? node.previousSibling : node.nextSibling;
+
+ if (
+ (!innermostFirst && func(node) === false) ||
+ (!siblingsOnly && traverse(
+ node, func, innermostFirst, siblingsOnly, reverse
+ ) === false) ||
+ (innermostFirst && func(node) === false)
+ ) {
+ return false;
+ }
+
+ node = next;
+ }
+ }
+
+ /**
+ * Like traverse but loops in reverse
+ * @see traverse
+ */
+ function rTraverse(node, func, innermostFirst, siblingsOnly) {
+ traverse(node, func, innermostFirst, siblingsOnly, true);
+ }
+
+ /**
+ * Parses HTML into a document fragment
+ *
+ * @param {string} html
+ * @param {Document} [context]
+ * @since 1.4.4
+ * @return {DocumentFragment}
+ */
+ function parseHTML(html, context) {
+ context = context || document;
+
+ var ret = context.createDocumentFragment();
+ var tmp = createElement('div', {}, context);
+
+ tmp.innerHTML = html;
+
+ while (tmp.firstChild) {
+ appendChild(ret, tmp.firstChild);
+ }
+
+ return ret;
+ }
+
+ /**
+ * Checks if an element has any styling.
+ *
+ * It has styling if it is not a plain or
or
+ * if it has a class, style attribute or data.
+ *
+ * @param {HTMLElement} elm
+ * @return {boolean}
+ * @since 1.4.4
+ */
+ function hasStyling(node) {
+ return node && (!is(node, 'p,div') || node.className ||
+ attr(node, 'style') || !isEmptyObject(data(node)));
+ }
+
+ /**
+ * Converts an element from one type to another.
+ *
+ * For example it can convert the element to
+ *
+ * @param {HTMLElement} element
+ * @param {string} toTagName
+ * @return {HTMLElement}
+ * @since 1.4.4
+ */
+ function convertElement(element, toTagName) {
+ var newElement = createElement(toTagName, {}, element.ownerDocument);
+
+ each(element.attributes, function (_, attribute) {
+ // Some browsers parse invalid attributes names like
+ // 'size"2' which throw an exception when set, just
+ // ignore these.
+ try {
+ attr(newElement, attribute.name, attribute.value);
+ } catch (ex) {}
+ });
+
+ while (element.firstChild) {
+ appendChild(newElement, element.firstChild);
+ }
+
+ element.parentNode.replaceChild(newElement, element);
+
+ return newElement;
+ }
+
+ /**
+ * List of block level elements separated by bars (|)
+ *
+ * @type {string}
+ */
+ var blockLevelList = '|body|hr|p|div|h1|h2|h3|h4|h5|h6|address|pre|' +
+ 'form|table|tbody|thead|tfoot|th|tr|td|li|ol|ul|blockquote|center|';
+
+ /**
+ * List of elements that do not allow children separated by bars (|)
+ *
+ * @param {Node} node
+ * @return {boolean}
+ * @since 1.4.5
+ */
+ function canHaveChildren(node) {
+ // 1 = Element
+ // 9 = Document
+ // 11 = Document Fragment
+ if (!/11?|9/.test(node.nodeType)) {
+ return false;
+ }
+
+ // List of empty HTML tags separated by bar (|) character.
+ // Source: http://www.w3.org/TR/html4/index/elements.html
+ // Source: http://www.w3.org/TR/html5/syntax.html#void-elements
+ return ('|iframe|area|base|basefont|br|col|frame|hr|img|input|wbr' +
+ '|isindex|link|meta|param|command|embed|keygen|source|track|' +
+ 'object|').indexOf('|' + node.nodeName.toLowerCase() + '|') < 0;
+ }
+
+ /**
+ * Checks if an element is inline
+ *
+ * @param {HTMLElement} elm
+ * @param {boolean} [includeCodeAsBlock=false]
+ * @return {boolean}
+ */
+ function isInline(elm, includeCodeAsBlock) {
+ var tagName,
+ nodeType = (elm || {}).nodeType || TEXT_NODE;
+
+ if (nodeType !== ELEMENT_NODE) {
+ return nodeType === TEXT_NODE;
+ }
+
+ tagName = elm.tagName.toLowerCase();
+
+ if (tagName === 'code') {
+ return !includeCodeAsBlock;
+ }
+
+ return blockLevelList.indexOf('|' + tagName + '|') < 0;
+ }
+
+ /**
+ * Copy the CSS from 1 node to another.
+ *
+ * Only copies CSS defined on the element e.g. style attr.
+ *
+ * @param {HTMLElement} from
+ * @param {HTMLElement} to
+ */
+ function copyCSS(from, to) {
+ to.style.cssText = from.style.cssText + to.style.cssText;
+ }
+
+ /**
+ * Fixes block level elements inside in inline elements.
+ *
+ * Also fixes invalid list nesting by placing nested lists
+ * inside the previous li tag or wrapping them in an li tag.
+ *
+ * @param {HTMLElement} node
+ */
+ function fixNesting(node) {
+ var getLastInlineParent = function (node) {
+ while (isInline(node.parentNode, true)) {
+ node = node.parentNode;
+ }
+
+ return node;
+ };
+
+ traverse(node, function (node) {
+ var list = 'ul,ol',
+ isBlock = !isInline(node, true);
+
+ // Any blocklevel element inside an inline element needs fixing.
+ if (isBlock && isInline(node.parentNode, true)) {
+ var parent = getLastInlineParent(node),
+ before = extractContents(parent, node),
+ middle = node;
+
+ // copy current styling so when moved out of the parent
+ // it still has the same styling
+ copyCSS(parent, middle);
+
+ insertBefore(before, parent);
+ insertBefore(middle, parent);
+ }
+
+ // Fix invalid nested lists which should be wrapped in an li tag
+ if (isBlock && is(node, list) && is(node.parentNode, list)) {
+ var li = previousElementSibling(node, 'li');
+
+ if (!li) {
+ li = createElement('li');
+ insertBefore(li, node);
+ }
+
+ appendChild(li, node);
+ }
+ });
+ }
+
+ /**
+ * Finds the common parent of two nodes
+ *
+ * @param {!HTMLElement} node1
+ * @param {!HTMLElement} node2
+ * @return {?HTMLElement}
+ */
+ function findCommonAncestor(node1, node2) {
+ while ((node1 = node1.parentNode)) {
+ if (contains(node1, node2)) {
+ return node1;
+ }
+ }
+ }
+
+ /**
+ * @param {?Node}
+ * @param {boolean} [previous=false]
+ * @returns {?Node}
+ */
+ function getSibling(node, previous) {
+ if (!node) {
+ return null;
+ }
+
+ return (previous ? node.previousSibling : node.nextSibling) ||
+ getSibling(node.parentNode, previous);
+ }
+
+ /**
+ * Removes unused whitespace from the root and all it's children.
+ *
+ * @param {!HTMLElement} root
+ * @since 1.4.3
+ */
+ function removeWhiteSpace(root) {
+ var nodeValue, nodeType, next, previous, previousSibling,
+ nextNode, trimStart,
+ cssWhiteSpace = css(root, 'whiteSpace'),
+ // Preserve newlines if is pre-line
+ preserveNewLines = /line$/i.test(cssWhiteSpace),
+ node = root.firstChild;
+
+ // Skip pre & pre-wrap with any vendor prefix
+ if (/pre(\-wrap)?$/i.test(cssWhiteSpace)) {
+ return;
+ }
+
+ while (node) {
+ nextNode = node.nextSibling;
+ nodeValue = node.nodeValue;
+ nodeType = node.nodeType;
+
+ if (nodeType === ELEMENT_NODE && node.firstChild) {
+ removeWhiteSpace(node);
+ }
+
+ if (nodeType === TEXT_NODE) {
+ next = getSibling(node);
+ previous = getSibling(node, true);
+ trimStart = false;
+
+ while (hasClass(previous, 'sceditor-ignore')) {
+ previous = getSibling(previous, true);
+ }
+
+ // If previous sibling isn't inline or is a textnode that
+ // ends in whitespace, time the start whitespace
+ if (isInline(node) && previous) {
+ previousSibling = previous;
+
+ while (previousSibling.lastChild) {
+ previousSibling = previousSibling.lastChild;
+
+ // eslint-disable-next-line max-depth
+ while (hasClass(previousSibling, 'sceditor-ignore')) {
+ previousSibling = getSibling(previousSibling, true);
+ }
+ }
+
+ trimStart = previousSibling.nodeType === TEXT_NODE ?
+ /[\t\n\r ]$/.test(previousSibling.nodeValue) :
+ !isInline(previousSibling);
+ }
+
+ // Clear zero width spaces
+ nodeValue = nodeValue.replace(/\u200B/g, '');
+
+ // Strip leading whitespace
+ if (!previous || !isInline(previous) || trimStart) {
+ nodeValue = nodeValue.replace(
+ preserveNewLines ? /^[\t ]+/ : /^[\t\n\r ]+/,
+ ''
+ );
+ }
+
+ // Strip trailing whitespace
+ if (!next || !isInline(next)) {
+ nodeValue = nodeValue.replace(
+ preserveNewLines ? /[\t ]+$/ : /[\t\n\r ]+$/,
+ ''
+ );
+ }
+
+ // Remove empty text nodes
+ if (!nodeValue.length) {
+ remove(node);
+ } else {
+ node.nodeValue = nodeValue.replace(
+ preserveNewLines ? /[\t ]+/g : /[\t\n\r ]+/g,
+ ' '
+ );
+ }
+ }
+
+ node = nextNode;
+ }
+ }
+
+ /**
+ * Extracts all the nodes between the start and end nodes
+ *
+ * @param {HTMLElement} startNode The node to start extracting at
+ * @param {HTMLElement} endNode The node to stop extracting at
+ * @return {DocumentFragment}
+ */
+ function extractContents(startNode, endNode) {
+ var range = startNode.ownerDocument.createRange();
+
+ range.setStartBefore(startNode);
+ range.setEndAfter(endNode);
+
+ return range.extractContents();
+ }
+
+ /**
+ * Gets the offset position of an element
+ *
+ * @param {HTMLElement} node
+ * @return {Object} An object with left and top properties
+ */
+ function getOffset(node) {
+ var left = 0,
+ top = 0;
+
+ while (node) {
+ left += node.offsetLeft;
+ top += node.offsetTop;
+ node = node.offsetParent;
+ }
+
+ return {
+ left: left,
+ top: top
+ };
+ }
+
+ /**
+ * Gets the value of a CSS property from the elements style attribute
+ *
+ * @param {HTMLElement} elm
+ * @param {string} property
+ * @return {string}
+ */
+ function getStyle(elm, property) {
+ var direction, styleValue,
+ elmStyle = elm.style;
+
+ if (!cssPropertyNameCache[property]) {
+ cssPropertyNameCache[property] = camelCase(property);
+ }
+
+ property = cssPropertyNameCache[property];
+ styleValue = elmStyle[property];
+
+ // Add an exception for text-align
+ if ('textAlign' === property) {
+ direction = elmStyle.direction;
+ styleValue = styleValue || css(elm, property);
+
+ if (css(elm.parentNode, property) === styleValue ||
+ css(elm, 'display') !== 'block' || is(elm, 'hr,th')) {
+ return '';
+ }
+
+ // IE changes text-align to the same as the current direction
+ // so skip unless its not the same
+ if ((/right/i.test(styleValue) && direction === 'rtl') ||
+ (/left/i.test(styleValue) && direction === 'ltr')) {
+ return '';
+ }
+ }
+
+ return styleValue;
+ }
+
+ /**
+ * Tests if an element has a style.
+ *
+ * If values are specified it will check that the styles value
+ * matches one of the values
+ *
+ * @param {HTMLElement} elm
+ * @param {string} property
+ * @param {string|array} [values]
+ * @return {boolean}
+ */
+ function hasStyle(elm, property, values) {
+ var styleValue = getStyle(elm, property);
+
+ if (!styleValue) {
+ return false;
+ }
+
+ return !values || styleValue === values ||
+ (Array.isArray(values) && values.indexOf(styleValue) > -1);
+ }
+
+ /**
+ * Default options for SCEditor
+ * @type {Object}
+ */
+ var defaultOptions = {
+ /** @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,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: '#000000,#44B8FF,#1E92F7,#0074D9,#005DC2,#00369B,#b3d5f4|' +
+ '#444444,#C3FFFF,#9DF9FF,#7FDBFF,#68C4E8,#419DC1,#d9f4ff|' +
+ '#666666,#72FF84,#4CEA5E,#2ECC40,#17B529,#008E02,#c0f0c6|' +
+ '#888888,#FFFF44,#FFFA1E,#FFDC00,#E8C500,#C19E00,#fff5b3|' +
+ '#aaaaaa,#FFC95F,#FFA339,#FF851B,#E86E04,#C14700,#ffdbbb|' +
+ '#cccccc,#FF857A,#FF5F54,#FF4136,#E82A1F,#C10300,#ffc6c3|' +
+ '#eeeeee,#FF56FF,#FF30DC,#F012BE,#D900A7,#B20080,#fbb8ec|' +
+ '#ffffff,#F551FF,#CF2BE7,#B10DC9,#9A00B2,#9A00B2,#e8b6ef',
+
+ /**
+ * The locale to use.
+ * @type {string}
+ */
+ locale: attr(document.documentElement, '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 {?number}
+ */
+ width: null,
+
+ /**
+ * Height of the editor including toolbar. Set to null for automatic
+ * height
+ *
+ * @type {?number}
+ */
+ 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 {?number}
+ */
+ resizeMinWidth: null,
+ /**
+ * Min resize to height, set to null for half textarea height or -1 for
+ * unlimited
+ *
+ * @type {?number}
+ */
+ resizeMinHeight: null,
+ /**
+ * Max resize to height, set to null for double textarea height or -1
+ * for unlimited
+ *
+ * @type {?number}
+ */
+ resizeMaxHeight: null,
+ /**
+ * Max resize to width, set to null for double textarea width or -1 for
+ * unlimited
+ *
+ * @type {?number}
+ */
+ 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 {?number}
+ */
+ 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: { }
+ };
+
+ var USER_AGENT = navigator.userAgent;
+
+ /**
+ * Detects the version of IE is being used if any.
+ *
+ * Will be the IE version number or undefined if the
+ * browser is not IE.
+ *
+ * Source: https://gist.github.com/527683 with extra code
+ * for IE 10 & 11 detection.
+ *
+ * @function
+ * @name ie
+ * @type {number}
+ */
+ var ie = (function () {
+ var undef,
+ v = 3,
+ doc = document,
+ div = doc.createElement('div'),
+ all = div.getElementsByTagName('i');
+
+ do {
+ div.innerHTML = '';
+ } while (all[0]);
+
+ // Detect IE 10 as it doesn't support conditional comments.
+ if ((doc.documentMode && doc.all && window.atob)) {
+ v = 10;
+ }
+
+ // Detect IE 11
+ if (v === 4 && doc.documentMode) {
+ v = 11;
+ }
+
+ return v > 4 ? v : undef;
+ }());
+
+ var edge = '-ms-ime-align' in document.documentElement.style;
+
+ /**
+ * Detects if the browser is iOS
+ *
+ * Needed to fix iOS specific bugs
+ *
+ * @function
+ * @name ios
+ * @memberOf jQuery.sceditor
+ * @type {boolean}
+ */
+ var ios = /iPhone|iPod|iPad| wosbrowser\//i.test(USER_AGENT);
+
+ /**
+ * If the browser supports WYSIWYG editing (e.g. older mobile browsers).
+ *
+ * @function
+ * @name isWysiwygSupported
+ * @return {boolean}
+ */
+ var isWysiwygSupported = (function () {
+ var match, isUnsupported;
+
+ var div = document.createElement('div');
+ div.contentEditable = true ;
+
+ // Check if the contentEditable attribute is supported
+ if (!('contentEditable' in document.documentElement) ||
+ div.contentEditable !== 'true') {
+ return false;
+ }
+
+ // I think blackberry supports contentEditable or will at least
+ // give a valid value for the contentEditable detection above
+ // so it isn't included in the below tests.
+
+ // I hate having to do UA sniffing but some mobile browsers say they
+ // support contentediable when it isn't usable, i.e. you can't enter
+ // text.
+ // This is the only way I can think of to detect them which is also how
+ // every other editor I've seen deals with this issue.
+
+ // Exclude Opera mobile and mini
+ isUnsupported = /Opera Mobi|Opera Mini/i.test(USER_AGENT);
+
+ if (/Android/i.test(USER_AGENT)) {
+ isUnsupported = true;
+
+ if (/Safari/.test(USER_AGENT)) {
+ // Android browser 534+ supports content editable
+ // This also matches Chrome which supports content editable too
+ match = /Safari\/(\d+)/.exec(USER_AGENT);
+ isUnsupported = (!match || !match[1] ? true : match[1] < 534);
+ }
+ }
+
+ // The current version of Amazon Silk supports it, older versions didn't
+ // As it uses webkit like Android, assume it's the same and started
+ // working at versions >= 534
+ if (/ Silk\//i.test(USER_AGENT)) {
+ match = /AppleWebKit\/(\d+)/.exec(USER_AGENT);
+ isUnsupported = (!match || !match[1] ? true : match[1] < 534);
+ }
+
+ // iOS 5+ supports content editable
+ if (ios) {
+ // Block any version <= 4_x(_x)
+ isUnsupported = /OS [0-4](_\d)+ like Mac/i.test(USER_AGENT);
+ }
+
+ // Firefox does support WYSIWYG on mobiles so override
+ // any previous value if using FF
+ if (/Firefox/i.test(USER_AGENT)) {
+ isUnsupported = false;
+ }
+
+ if (/OneBrowser/i.test(USER_AGENT)) {
+ isUnsupported = false;
+ }
+
+ // UCBrowser works but doesn't give a unique user agent
+ if (navigator.vendor === 'UCWEB') {
+ isUnsupported = false;
+ }
+
+ // IE <= 9 is not supported any more
+ if (ie <= 9) {
+ isUnsupported = true;
+ }
+
+ return !isUnsupported;
+ }());
+
+ // Must start with a valid scheme
+ // ^
+ // Schemes that are considered safe
+ // (https?|s?ftp|mailto|spotify|skype|ssh|teamspeak|tel):|
+ // Relative schemes (//:) are considered safe
+ // (\\/\\/)|
+ // Image data URI's are considered safe
+ // data:image\\/(png|bmp|gif|p?jpe?g);
+ var VALID_SCHEME_REGEX =
+ /^(https?|s?ftp|mailto|spotify|skype|ssh|teamspeak|tel):|(\/\/)|data:image\/(png|bmp|gif|p?jpe?g);/i;
+
+ /**
+ * Escapes a string so it's safe to use in regex
+ *
+ * @param {string} str
+ * @return {string}
+ */
+ function regex(str) {
+ return str.replace(/([\-.*+?^=!:${}()|\[\]\/\\])/g, '\\$1');
+ }
+
+ /**
+ * Escapes all HTML entities in a string
+ *
+ * If noQuotes is set to false, all single and double
+ * quotes will also be escaped
+ *
+ * @param {string} str
+ * @param {boolean} [noQuotes=true]
+ * @return {string}
+ * @since 1.4.1
+ */
+ function entities(str, noQuotes) {
+ if (!str) {
+ return str;
+ }
+
+ var replacements = {
+ '&': '&',
+ '<': '<',
+ '>': '>',
+ ' ': ' ',
+ '\r\n': ' ',
+ '\r': ' ',
+ '\n': ' '
+ };
+
+ if (noQuotes !== false) {
+ replacements['"'] = '"';
+ replacements['\''] = ''';
+ replacements['`'] = '`';
+ }
+
+ str = str.replace(/ {2}|\r\n|[&<>\r\n'"`]/g, function (match) {
+ return replacements[match] || match;
+ });
+
+ return str;
+ }
+
+ /**
+ * Escape URI scheme.
+ *
+ * Appends the current URL to a url if it has a scheme that is not:
+ *
+ * http
+ * https
+ * sftp
+ * ftp
+ * mailto
+ * spotify
+ * skype
+ * ssh
+ * teamspeak
+ * tel
+ * //
+ * data:image/(png|jpeg|jpg|pjpeg|bmp|gif);
+ *
+ * **IMPORTANT**: This does not escape any HTML in a url, for
+ * that use the escape.entities() method.
+ *
+ * @param {string} url
+ * @return {string}
+ * @since 1.4.5
+ */
+ function uriScheme(url) {
+ var path,
+ // If there is a : before a / then it has a scheme
+ hasScheme = /^[^\/]*:/i,
+ location = window.location;
+
+ // Has no scheme or a valid scheme
+ if ((!url || !hasScheme.test(url)) || VALID_SCHEME_REGEX.test(url)) {
+ return url;
+ }
+
+ path = location.pathname.split('/');
+ path.pop();
+
+ return location.protocol + '//' +
+ location.host +
+ path.join('/') + '/' +
+ url;
+ }
+
+ /**
+ * HTML templates used by the editor and default commands
+ * @type {Object}
+ * @private
+ */
+ var _templates = {
+ html:
+ '' +
+ '' +
+ '' +
+ '' +
+ ' ' +
+ ' ' +
+ '' +
+ '
' +
+ '',
+
+ toolbarButton: '' +
+ '{dispName}
',
+
+ emoticon: ' ',
+
+ fontOpt: '{font} ',
+
+ sizeOpt: '{size} ',
+
+ pastetext:
+ '{label} ' +
+ '
' +
+ ' ' +
+ '
',
+
+ table:
+ '{rows}
' +
+ '{cols}
' +
+ '
',
+
+ image:
+ '{url} ' +
+ '
' +
+ '{width} ' +
+ '
' +
+ '{height} ' +
+ '
' +
+ ' ' +
+ '
',
+
+ email:
+ '{label} ' +
+ '
' +
+ '{desc} ' +
+ '
' +
+ ' ' +
+ '
',
+
+ link:
+ '{url} ' +
+ '
' +
+ '{desc} ' +
+ '
' +
+ '
',
+
+ youtubeMenu:
+ '{label} ' +
+ '
' +
+ ' ' +
+ '
',
+
+ youtube:
+ ''
+ };
+
+ /**
+ * Replaces any params in a template with the passed params.
+ *
+ * If createHtml is passed it will return a DocumentFragment
+ * containing the parsed template.
+ *
+ * @param {string} name
+ * @param {Object} [params]
+ * @param {boolean} [createHtml]
+ * @returns {string|DocumentFragment}
+ * @private
+ */
+ function _tmpl (name, params, createHtml) {
+ var template = _templates[name];
+
+ Object.keys(params).forEach(function (name) {
+ template = template.replace(
+ new RegExp(regex('{' + name + '}'), 'g'), params[name]
+ );
+ });
+
+ if (createHtml) {
+ template = parseHTML(template);
+ }
+
+ return template;
+ }
+
+ // 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 && ie < 11;
+
+ /**
+ * Fixes a bug in FF where it sometimes wraps
+ * new lines in their own list item.
+ * See issue #359
+ */
+ function fixFirefoxListBug(editor) {
+ // Only apply to Firefox as will break other browsers.
+ if ('mozHidden' in document) {
+ var node = editor.getBody();
+ var next;
+
+ while (node) {
+ next = node;
+
+ if (next.firstChild) {
+ next = next.firstChild;
+ } else {
+
+ while (next && !next.nextSibling) {
+ next = next.parentNode;
+ }
+
+ if (next) {
+ next = next.nextSibling;
+ }
+ }
+
+ if (node.nodeType === 3 && /[\n\r\t]+/.test(node.nodeValue)) {
+ // Only remove if newlines are collapsed
+ if (!/^pre/.test(css(node.parentNode, 'whiteSpace'))) {
+ remove(node);
+ }
+ }
+
+ node = next;
+ }
+ }
+ }
+
+
+ /**
+ * Map of all the commands for SCEditor
+ * @type {Object}
+ * @name commands
+ * @memberOf jQuery.sceditor
+ */
+ var defaultCmds = {
+ // 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: {
+ state: function (node) {
+ if (node && node.nodeType === 3) {
+ node = node.parentNode;
+ }
+
+ if (node) {
+ var isLtr = css(node, 'direction') === 'ltr';
+ var align = css(node, 'textAlign');
+
+ return align === 'left' || align === (isLtr ? 'start' : 'end');
+ }
+ },
+ exec: 'justifyleft',
+ tooltip: 'Align left'
+ },
+ // END_COMMAND
+ // START_COMMAND: Centre
+ center: {
+ exec: 'justifycenter',
+ tooltip: 'Center'
+ },
+ // END_COMMAND
+ // START_COMMAND: Right
+ right: {
+ state: function (node) {
+ if (node && node.nodeType === 3) {
+ node = node.parentNode;
+ }
+
+ if (node) {
+ var isLtr = css(node, 'direction') === 'ltr';
+ var align = css(node, 'textAlign');
+
+ return align === 'right' || align === (isLtr ? 'end' : 'start');
+ }
+ },
+ 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 content = createElement('div');
+
+ on(content, 'click', 'a', function (e) {
+ callback(data(this, 'font'));
+ editor.closeDropDown(true);
+ e.preventDefault();
+ });
+
+ editor.opts.fonts.split(',').forEach(function (font) {
+ appendChild(content, _tmpl('fontOpt', {
+ font: font
+ }, true));
+ });
+
+ editor.createDropDown(caller, 'font-picker', content);
+ },
+ exec: function (caller) {
+ var editor = this;
+
+ defaultCmds.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 = createElement('div');
+
+ on(content, 'click', 'a', function (e) {
+ callback(data(this, 'size'));
+ editor.closeDropDown(true);
+ e.preventDefault();
+ });
+
+ for (var i = 1; i <= 7; i++) {
+ appendChild(content, _tmpl('sizeOpt', {
+ size: i
+ }, true));
+ }
+
+ editor.createDropDown(caller, 'fontsize-picker', content);
+ },
+ exec: function (caller) {
+ var editor = this;
+
+ defaultCmds.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 content = createElement('div'),
+ html = '',
+ cmd = defaultCmds.color;
+
+ if (!cmd._htmlCache) {
+ editor.opts.colors.split('|').forEach(function (column) {
+ html += '';
+
+ column.split(',').forEach(function (color) {
+ html +=
+ '
';
+ });
+
+ html += '
';
+ });
+
+ cmd._htmlCache = html;
+ }
+
+ appendChild(content, parseHTML(cmd._htmlCache));
+
+ on(content, 'click', 'a', function (e) {
+ callback(data(this, 'color'));
+ editor.closeDropDown(true);
+ e.preventDefault();
+ });
+
+ editor.createDropDown(caller, 'color-picker', content);
+ },
+ exec: function (caller) {
+ var editor = this;
+
+ defaultCmds.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 = createElement('div'),
+ editor = this;
+
+ appendChild(content, _tmpl('pastetext', {
+ label: editor._(
+ 'Paste your text inside the following box:'
+ ),
+ insert: editor._('Insert')
+ }, true));
+
+ on(content, 'click', '.button', function (e) {
+ val = find(content, '#txt')[0].value;
+
+ 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: function () {
+ fixFirefoxListBug(this);
+ this.execCommand('insertunorderedlist');
+ },
+ tooltip: 'Bullet list'
+ },
+ // END_COMMAND
+ // START_COMMAND: Ordered List
+ orderedlist: {
+ exec: function () {
+ fixFirefoxListBug(this);
+ this.execCommand('insertorderedlist');
+ },
+ tooltip: 'Numbered list'
+ },
+ // END_COMMAND
+ // START_COMMAND: Indent
+ indent: {
+ state: function (parent$$1, firstBlock) {
+ // Only works with lists, for now
+ var range, startParent, endParent;
+
+ if (is(firstBlock, 'li')) {
+ return 0;
+ }
+
+ if (is(firstBlock, '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();
+
+ 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
+ (is(endParent, 'li') && endParent !==
+ endParent.parentNode.lastElementChild)) {
+ return 0;
+ }
+ }
+
+ return -1;
+ },
+ exec: function () {
+ var editor = this,
+ block = 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 (closest(block, 'ul,ol,menu')) {
+ editor.execCommand('indent');
+ }
+ },
+ tooltip: 'Add indent'
+ },
+ // END_COMMAND
+ // START_COMMAND: Outdent
+ outdent: {
+ state: function (parents$$1, firstBlock) {
+ return closest(firstBlock, 'ul,ol,menu') ? 0 : -1;
+ },
+ exec: function () {
+ var block = this.getRangeHelper().getFirstBlockParent();
+ if (closest(block, 'ul,ol,menu')) {
+ this.execCommand('outdent');
+ }
+ },
+ tooltip: 'Remove one indent'
+ },
+ // END_COMMAND
+
+ // START_COMMAND: Table
+ table: {
+ exec: function (caller) {
+ var editor = this,
+ content = createElement('div');
+
+ appendChild(content, _tmpl('table', {
+ rows: editor._('Rows:'),
+ cols: editor._('Cols:'),
+ insert: editor._('Insert')
+ }, true));
+
+ on(content, 'click', '.button', function (e) {
+ var rows = Number(find(content, '#rows')[0].value),
+ cols = Number(find(content, '#cols')[0].value),
+ html = '';
+
+ if (rows > 0 && cols > 0) {
+ html += Array(rows + 1).join(
+ '' +
+ Array(cols + 1).join(
+ '' + (IE_BR_FIX ? '' : ' ') + ' '
+ ) +
+ ' '
+ );
+
+ html += '
';
+
+ editor.wysiwygEditorInsertHtml(html);
+ editor.closeDropDown(true);
+ e.preventDefault();
+ }
+ });
+
+ editor.createDropDown(caller, 'inserttable', content);
+ },
+ tooltip: 'Insert a table'
+ },
+ // END_COMMAND
+
+ // START_COMMAND: Horizontal Rule
+ horizontalrule: {
+ exec: 'inserthorizontalrule',
+ tooltip: 'Insert a horizontal rule'
+ },
+ // END_COMMAND
+
+ // START_COMMAND: Code
+ code: {
+ exec: function () {
+ this.wysiwygEditorInsertHtml(
+ '',
+ (IE_BR_FIX ? '' : ' ') + '
'
+ );
+ },
+ tooltip: 'Code'
+ },
+ // END_COMMAND
+
+ // START_COMMAND: Image
+ image: {
+ _dropDown: function (editor, caller, selected, cb) {
+ var content = createElement('div');
+
+ appendChild(content, _tmpl('image', {
+ url: editor._('URL:'),
+ width: editor._('Width (optional):'),
+ height: editor._('Height (optional):'),
+ insert: editor._('Insert')
+ }, true));
+
+
+ var urlInput = find(content, '#image')[0];
+
+ urlInput.value = selected;
+
+ on(content, 'click', '.button', function (e) {
+ if (urlInput.value) {
+ cb(
+ urlInput.value,
+ find(content, '#width')[0].value,
+ find(content, '#height')[0].value
+ );
+ }
+
+ editor.closeDropDown(true);
+ e.preventDefault();
+ });
+
+ editor.createDropDown(caller, 'insertimage', content);
+ },
+ exec: function (caller) {
+ var editor = this;
+
+ defaultCmds.image._dropDown(
+ editor,
+ caller,
+ '',
+ function (url, width$$1, height$$1) {
+ var attrs = '';
+
+ if (width$$1) {
+ attrs += ' width="' + width$$1 + '"';
+ }
+
+ if (height$$1) {
+ attrs += ' height="' + height$$1 + '"';
+ }
+
+ editor.wysiwygEditorInsertHtml(
+ ' '
+ );
+ }
+ );
+ },
+ tooltip: 'Insert an image'
+ },
+ // END_COMMAND
+
+ // START_COMMAND: E-mail
+ email: {
+ _dropDown: function (editor, caller, cb) {
+ var content = createElement('div');
+
+ appendChild(content, _tmpl('email', {
+ label: editor._('E-mail:'),
+ desc: editor._('Description (optional):'),
+ insert: editor._('Insert')
+ }, true));
+
+ on(content, 'click', '.button', function (e) {
+ var email = find(content, '#email')[0].value;
+
+ if (email) {
+ cb(email, find(content, '#des')[0].value);
+ }
+
+ editor.closeDropDown(true);
+ e.preventDefault();
+ });
+
+ editor.createDropDown(caller, 'insertemail', content);
+ },
+ exec: function (caller) {
+ var editor = this;
+
+ defaultCmds.email._dropDown(
+ editor,
+ caller,
+ function (email, text) {
+ // needed for IE to reset the last range
+ editor.focus();
+
+ if (!editor.getRangeHelper().selectedHtml() || text) {
+ editor.wysiwygEditorInsertHtml(
+ '' +
+ (text || email) +
+ ' '
+ );
+ } else {
+ editor.execCommand('createlink', 'mailto:' + email);
+ }
+ }
+ );
+ },
+ tooltip: 'Insert an email'
+ },
+ // END_COMMAND
+
+ // START_COMMAND: Link
+ link: {
+ _dropDown: function (editor, caller, cb) {
+ var content = createElement('div');
+
+ appendChild(content, _tmpl('link', {
+ url: editor._('URL:'),
+ desc: editor._('Description (optional):'),
+ ins: editor._('Insert')
+ }, true));
+
+ var linkInput = find(content, '#link')[0];
+
+ function insertUrl(e) {
+ if (linkInput.value) {
+ cb(linkInput.value, find(content, '#des')[0].value);
+ }
+
+ editor.closeDropDown(true);
+ e.preventDefault();
+ }
+
+ on(content, 'click', '.button', insertUrl);
+ on(content, 'keypress', function (e) {
+ // 13 = enter key
+ if (e.which === 13 && linkInput.value) {
+ insertUrl(e);
+ }
+ }, EVENT_CAPTURE);
+
+ editor.createDropDown(caller, 'insertlink', content);
+ },
+ exec: function (caller) {
+ var editor = this;
+
+ defaultCmds.link._dropDown(editor, caller, function (url, text) {
+ // 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 (text || !editor.getRangeHelper().selectedHtml()) {
+ text = text || url;
+
+ editor.wysiwygEditorInsertHtml(
+ '' + text + ' '
+ );
+ } else {
+ editor.execCommand('createlink', url);
+ }
+ });
+ },
+ tooltip: 'Insert a link'
+ },
+ // END_COMMAND
+
+ // START_COMMAND: Unlink
+ unlink: {
+ state: function () {
+ return closest(this.currentNode(), 'a') ? 0 : -1;
+ },
+ exec: function () {
+ var anchor = closest(this.currentNode(), 'a');
+
+ if (anchor) {
+ while (anchor.firstChild) {
+ insertBefore(anchor.firstChild, anchor);
+ }
+
+ remove(anchor);
+ }
+ },
+ tooltip: 'Unlink'
+ },
+ // END_COMMAND
+
+
+ // START_COMMAND: Quote
+ quote: {
+ 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,
+ opts = editor.opts,
+ emoticonsRoot = opts.emoticonsRoot || '',
+ emoticonsCompat = opts.emoticonsCompat,
+ rangeHelper = editor.getRangeHelper(),
+ startSpace = emoticonsCompat &&
+ rangeHelper.getOuterText(true, 1) !== ' ' ? ' ' : '',
+ endSpace = emoticonsCompat &&
+ rangeHelper.getOuterText(false, 1) !== ' ' ? ' ' : '',
+ content = createElement('div'),
+ line = createElement('div'),
+ perLine = 0,
+ emoticons = extend(
+ {},
+ opts.emoticons.dropdown,
+ includeMore ? opts.emoticons.more : {}
+ );
+
+ appendChild(content, line);
+
+ perLine = Math.sqrt(Object.keys(emoticons).length);
+
+ on(content, 'click', 'img', function (e) {
+ editor.insert(startSpace + attr(this, 'alt') + endSpace,
+ null, false).closeDropDown(true);
+
+ e.preventDefault();
+ });
+
+ each(emoticons, function (code, emoticon) {
+ appendChild(line, createElement('img', {
+ src: emoticonsRoot + (emoticon.url || emoticon),
+ alt: code,
+ title: emoticon.tooltip || code
+ }));
+
+ if (line.children.length >= perLine) {
+ line = createElement('div');
+ appendChild(content, line);
+ }
+ });
+
+ if (!includeMore && opts.emoticons.more) {
+ moreLink = createElement('a', {
+ className: 'sceditor-more'
+ });
+
+ appendChild(moreLink,
+ document.createTextNode(editor._('More')));
+
+ on(moreLink, 'click', function (e) {
+ editor.createDropDown(
+ caller, 'more-emoticons', createContent(true)
+ );
+
+ e.preventDefault();
+ });
+
+ appendChild(content, moreLink);
+ }
+
+ return content;
+ };
+
+ editor.createDropDown(caller, 'emoticons', createContent(false));
+ },
+ txtExec: function (caller) {
+ defaultCmds.emoticon.exec.call(this, caller);
+ },
+ tooltip: 'Insert an emoticon'
+ },
+ // END_COMMAND
+
+ // START_COMMAND: YouTube
+ youtube: {
+ _dropDown: function (editor, caller, callback) {
+ var content = createElement('div');
+
+ appendChild(content, _tmpl('youtubeMenu', {
+ label: editor._('Video URL:'),
+ insert: editor._('Insert')
+ }, true));
+
+ on(content, 'click', '.button', function (e) {
+ var val = find(content, '#link')[0].value;
+ var idMatch = val.match(/(?:v=|v\/|embed\/|youtu.be\/)(.{11})/);
+ var timeMatch = val.match(/[&|?](?:star)?t=((\d+[hms]?){1,3})/);
+ var time = 0;
+
+ if (timeMatch) {
+ each(timeMatch[1].split(/[hms]/), function (i, val) {
+ if (val !== '') {
+ time = (time * 60) + Number(val);
+ }
+ });
+ }
+
+ if (idMatch && /^[a-zA-Z0-9_\-]{11}$/.test(idMatch[1])) {
+ callback(idMatch[1], time);
+ }
+
+ editor.closeDropDown(true);
+ e.preventDefault();
+ });
+
+ editor.createDropDown(caller, 'insertlink', content);
+ },
+ exec: function (btn) {
+ var editor = this;
+
+ defaultCmds.youtube._dropDown(editor, btn, function (id, time) {
+ editor.wysiwygEditorInsertHtml(_tmpl('youtube', {
+ id: id,
+ time: time
+ }));
+ });
+ },
+ 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(defaultCmds.date._date(this));
+ },
+ txtExec: function () {
+ this.insertText(defaultCmds.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(defaultCmds.time._time());
+ },
+ txtExec: function () {
+ this.insertText(defaultCmds.time._time());
+ },
+ tooltip: 'Insert current time'
+ },
+ // END_COMMAND
+
+
+ // START_COMMAND: Ltr
+ ltr: {
+ state: function (parents$$1, firstBlock) {
+ return firstBlock && firstBlock.style.direction === 'ltr';
+ },
+ exec: function () {
+ var editor = this,
+ rangeHelper = editor.getRangeHelper(),
+ node = rangeHelper.getFirstBlockParent();
+
+ editor.focus();
+
+ if (!node || is(node, 'body')) {
+ editor.execCommand('formatBlock', 'p');
+
+ node = rangeHelper.getFirstBlockParent();
+
+ if (!node || is(node, 'body')) {
+ return;
+ }
+ }
+
+ var toggleValue = css(node, 'direction') === 'ltr' ? '' : 'ltr';
+ css(node, 'direction', toggleValue);
+ },
+ tooltip: 'Left-to-Right'
+ },
+ // END_COMMAND
+
+ // START_COMMAND: Rtl
+ rtl: {
+ state: function (parents$$1, firstBlock) {
+ return firstBlock && firstBlock.style.direction === 'rtl';
+ },
+ exec: function () {
+ var editor = this,
+ rangeHelper = editor.getRangeHelper(),
+ node = rangeHelper.getFirstBlockParent();
+
+ editor.focus();
+
+ if (!node || is(node, 'body')) {
+ editor.execCommand('formatBlock', 'p');
+
+ node = rangeHelper.getFirstBlockParent();
+
+ if (!node || is(node, 'body')) {
+ return;
+ }
+ }
+
+ var toggleValue = css(node, 'direction') === 'rtl' ? '' : 'rtl';
+ css(node, 'direction', toggleValue);
+ },
+ 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: {}
+ };
+
+ var plugins = {};
+
+ /**
+ * Plugin Manager class
+ * @class PluginManager
+ * @name PluginManager
+ */
+ function PluginManager(thisObj) {
+ /**
+ * Alias of this
+ *
+ * @private
+ * @type {Object}
+ */
+ var base = this;
+
+ /**
+ * Array of all currently registered plugins
+ *
+ * @type {Array}
+ * @private
+ */
+ var registeredPlugins = [];
+
+
+ /**
+ * Changes a signals name from "name" into "signalName".
+ *
+ * @param {string} signal
+ * @return {string}
+ * @private
+ */
+ var formatSignalName = function (signal) {
+ return 'signal' + signal.charAt(0).toUpperCase() + signal.slice(1);
+ };
+
+ /**
+ * Calls handlers for a signal
+ *
+ * @see call()
+ * @see callOnlyFirst()
+ * @param {Array} args
+ * @param {boolean} returnAtFirst
+ * @return {*}
+ * @private
+ */
+ var callHandlers = function (args, returnAtFirst) {
+ args = [].slice.call(args);
+
+ var idx, ret,
+ signal = formatSignalName(args.shift());
+
+ for (idx = 0; idx < registeredPlugins.length; idx++) {
+ if (signal in registeredPlugins[idx]) {
+ ret = registeredPlugins[idx][signal].apply(thisObj, args);
+
+ if (returnAtFirst) {
+ return ret;
+ }
+ }
+ }
+ };
+
+ /**
+ * Calls all handlers for the passed signal
+ *
+ * @param {string} signal
+ * @param {...string} args
+ * @function
+ * @name call
+ * @memberOf PluginManager.prototype
+ */
+ base.call = function () {
+ callHandlers(arguments, false);
+ };
+
+ /**
+ * Calls the first handler for a signal, and returns the
+ *
+ * @param {string} signal
+ * @param {...string} args
+ * @return {*} The result of calling the handler
+ * @function
+ * @name callOnlyFirst
+ * @memberOf PluginManager.prototype
+ */
+ base.callOnlyFirst = function () {
+ return callHandlers(arguments, true);
+ };
+
+ /**
+ * Checks if a signal has a handler
+ *
+ * @param {string} signal
+ * @return {boolean}
+ * @function
+ * @name hasHandler
+ * @memberOf PluginManager.prototype
+ */
+ base.hasHandler = function (signal) {
+ var i = registeredPlugins.length;
+ signal = formatSignalName(signal);
+
+ while (i--) {
+ if (signal in registeredPlugins[i]) {
+ return true;
+ }
+ }
+
+ return false;
+ };
+
+ /**
+ * Checks if the plugin exists in plugins
+ *
+ * @param {string} plugin
+ * @return {boolean}
+ * @function
+ * @name exists
+ * @memberOf PluginManager.prototype
+ */
+ base.exists = function (plugin) {
+ if (plugin in plugins) {
+ plugin = plugins[plugin];
+
+ return typeof plugin === 'function' &&
+ typeof plugin.prototype === 'object';
+ }
+
+ return false;
+ };
+
+ /**
+ * Checks if the passed plugin is currently registered.
+ *
+ * @param {string} plugin
+ * @return {boolean}
+ * @function
+ * @name isRegistered
+ * @memberOf PluginManager.prototype
+ */
+ base.isRegistered = function (plugin) {
+ if (base.exists(plugin)) {
+ var idx = registeredPlugins.length;
+
+ while (idx--) {
+ if (registeredPlugins[idx] instanceof plugins[plugin]) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ };
+
+ /**
+ * Registers a plugin to receive signals
+ *
+ * @param {string} plugin
+ * @return {boolean}
+ * @function
+ * @name register
+ * @memberOf PluginManager.prototype
+ */
+ base.register = function (plugin) {
+ if (!base.exists(plugin) || base.isRegistered(plugin)) {
+ return false;
+ }
+
+ plugin = new plugins[plugin]();
+ registeredPlugins.push(plugin);
+
+ if ('init' in plugin) {
+ plugin.init.call(thisObj);
+ }
+
+ return true;
+ };
+
+ /**
+ * Deregisters a plugin.
+ *
+ * @param {string} plugin
+ * @return {boolean}
+ * @function
+ * @name deregister
+ * @memberOf PluginManager.prototype
+ */
+ base.deregister = function (plugin) {
+ var removedPlugin,
+ pluginIdx = registeredPlugins.length,
+ removed = false;
+
+ if (!base.isRegistered(plugin)) {
+ return removed;
+ }
+
+ while (pluginIdx--) {
+ if (registeredPlugins[pluginIdx] instanceof plugins[plugin]) {
+ removedPlugin = registeredPlugins.splice(pluginIdx, 1)[0];
+ removed = true;
+
+ if ('destroy' in removedPlugin) {
+ removedPlugin.destroy.call(thisObj);
+ }
+ }
+ }
+
+ return removed;
+ };
+
+ /**
+ * Clears all plugins and removes the owner reference.
+ *
+ * Calling any functions on this object after calling
+ * destroy will cause a JS error.
+ *
+ * @name destroy
+ * @memberOf PluginManager.prototype
+ */
+ base.destroy = function () {
+ var i = registeredPlugins.length;
+
+ while (i--) {
+ if ('destroy' in registeredPlugins[i]) {
+ registeredPlugins[i].destroy.call(thisObj);
+ }
+ }
+
+ registeredPlugins = [];
+ thisObj = null;
+ };
+ }
+
+ PluginManager.plugins = plugins;
+
+ // 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$1 = ie && ie < 11;
+
+
+ /**
+ * Gets the text, start/end node and offset for
+ * length chars left or right of the passed node
+ * at the specified offset.
+ *
+ * @param {Node} node
+ * @param {number} offset
+ * @param {boolean} isLeft
+ * @param {number} length
+ * @return {Object}
+ * @private
+ */
+ var outerText = function (range, isLeft, length) {
+ var nodeValue, remaining, start, end, node,
+ text = '',
+ next = range.startContainer,
+ offset = range.startOffset;
+
+ // Handle cases where node is a paragraph and offset
+ // refers to the index of a text node.
+ // 3 = text node
+ if (next && next.nodeType !== 3) {
+ next = next.childNodes[offset];
+ offset = 0;
+ }
+
+ start = end = offset;
+
+ while (length > text.length && next && next.nodeType === 3) {
+ nodeValue = next.nodeValue;
+ remaining = length - text.length;
+
+ // If not the first node, start and end should be at their
+ // max values as will be updated when getting the text
+ if (node) {
+ end = nodeValue.length;
+ start = 0;
+ }
+
+ node = next;
+
+ if (isLeft) {
+ start = Math.max(end - remaining, 0);
+ offset = start;
+
+ text = nodeValue.substr(start, end - start) + text;
+ next = node.previousSibling;
+ } else {
+ end = Math.min(remaining, nodeValue.length);
+ offset = start + end;
+
+ text += nodeValue.substr(start, end);
+ next = node.nextSibling;
+ }
+ }
+
+ return {
+ node: node || next,
+ offset: offset,
+ text: text
+ };
+ };
+
+ /**
+ * Range helper
+ *
+ * @class RangeHelper
+ * @name RangeHelper
+ */
+ function RangeHelper(win, d) {
+ var _createMarker, _prepareInput,
+ doc = d || win.contentDocument || win.document,
+ startMarker = 'sceditor-start-marker',
+ endMarker = 'sceditor-end-marker',
+ base = this;
+
+ /**
+ * Inserts HTML into the current range replacing any selected
+ * text.
+ *
+ * If endHTML is specified the selected contents will be put between
+ * html and endHTML. If there is nothing selected html and endHTML are
+ * just concatenate together.
+ *
+ * @param {string} html
+ * @param {string} [endHTML]
+ * @return False on fail
+ * @function
+ * @name insertHTML
+ * @memberOf RangeHelper.prototype
+ */
+ base.insertHTML = function (html, endHTML) {
+ var node, div,
+ range = base.selectedRange();
+
+ if (!range) {
+ return false;
+ }
+
+ if (endHTML) {
+ html += base.selectedHtml() + endHTML;
+ }
+
+ div = createElement('p', {}, doc);
+ node = doc.createDocumentFragment();
+ div.innerHTML = html;
+
+ while (div.firstChild) {
+ appendChild(node, div.firstChild);
+ }
+
+ base.insertNode(node);
+ };
+
+ /**
+ * Prepares HTML to be inserted by adding a zero width space
+ * if the last child is empty and adding the range start/end
+ * markers to the last child.
+ *
+ * @param {Node|string} node
+ * @param {Node|string} [endNode]
+ * @param {boolean} [returnHtml]
+ * @return {Node|string}
+ * @private
+ */
+ _prepareInput = function (node, endNode, returnHtml) {
+ var lastChild,
+ frag = doc.createDocumentFragment();
+
+ if (typeof node === 'string') {
+ if (endNode) {
+ node += base.selectedHtml() + endNode;
+ }
+
+ frag = parseHTML(node);
+ } else {
+ appendChild(frag, node);
+
+ if (endNode) {
+ appendChild(frag, base.selectedRange().extractContents());
+ appendChild(frag, endNode);
+ }
+ }
+
+ if (!(lastChild = frag.lastChild)) {
+ return;
+ }
+
+ while (!isInline(lastChild.lastChild, true)) {
+ lastChild = lastChild.lastChild;
+ }
+
+ if (canHaveChildren(lastChild)) {
+ // Webkit won't allow the cursor to be placed inside an
+ // empty tag, so add a zero width space to it.
+ if (!lastChild.lastChild) {
+ appendChild(lastChild, document.createTextNode('\u200B'));
+ }
+ } else {
+ lastChild = frag;
+ }
+
+ base.removeMarkers();
+
+ // Append marks to last child so when restored cursor will be in
+ // the right place
+ appendChild(lastChild, _createMarker(startMarker));
+ appendChild(lastChild, _createMarker(endMarker));
+
+ if (returnHtml) {
+ var div = createElement('div');
+ appendChild(div, frag);
+
+ return div.innerHTML;
+ }
+
+ return frag;
+ };
+
+ /**
+ * The same as insertHTML except with DOM nodes instead
+ *
+ * Warning: the nodes must belong to the
+ * document they are being inserted into. Some browsers
+ * will throw exceptions if they don't.
+ *
+ * Returns boolean false on fail
+ *
+ * @param {Node} node
+ * @param {Node} endNode
+ * @return {false|undefined}
+ * @function
+ * @name insertNode
+ * @memberOf RangeHelper.prototype
+ */
+ base.insertNode = function (node, endNode) {
+ var input = _prepareInput(node, endNode),
+ range = base.selectedRange(),
+ parent$$1 = range.commonAncestorContainer;
+
+ if (!input) {
+ return false;
+ }
+
+ range.deleteContents();
+
+ // FF allows to be selected but inserting a node
+ // into will cause it not to be displayed so must
+ // insert before the in FF.
+ // 3 = TextNode
+ if (parent$$1 && parent$$1.nodeType !== 3 && !canHaveChildren(parent$$1)) {
+ insertBefore(input, parent$$1);
+ } else {
+ range.insertNode(input);
+ }
+
+ base.restoreRange();
+ };
+
+ /**
+ * Clones the selected Range
+ *
+ * @return {Range}
+ * @function
+ * @name cloneSelected
+ * @memberOf RangeHelper.prototype
+ */
+ base.cloneSelected = function () {
+ var range = base.selectedRange();
+
+ if (range) {
+ return range.cloneRange();
+ }
+ };
+
+ /**
+ * Gets the selected Range
+ *
+ * @return {Range}
+ * @function
+ * @name selectedRange
+ * @memberOf RangeHelper.prototype
+ */
+ base.selectedRange = function () {
+ var range, firstChild,
+ sel = win.getSelection();
+
+ if (!sel) {
+ return;
+ }
+
+ // When creating a new range, set the start to the first child
+ // element of the body element to avoid errors in FF.
+ if (sel.rangeCount <= 0) {
+ firstChild = doc.body;
+ while (firstChild.firstChild) {
+ firstChild = firstChild.firstChild;
+ }
+
+ range = doc.createRange();
+ // Must be setStartBefore otherwise it can cause infinite
+ // loops with lists in WebKit. See issue 442
+ range.setStartBefore(firstChild);
+
+ sel.addRange(range);
+ }
+
+ if (sel.rangeCount > 0) {
+ range = sel.getRangeAt(0);
+ }
+
+ return range;
+ };
+
+ /**
+ * Gets if there is currently a selection
+ *
+ * @return {boolean}
+ * @function
+ * @name hasSelection
+ * @since 1.4.4
+ * @memberOf RangeHelper.prototype
+ */
+ base.hasSelection = function () {
+ var sel = win.getSelection();
+
+ return sel && sel.rangeCount > 0;
+ };
+
+ /**
+ * Gets the currently selected HTML
+ *
+ * @return {string}
+ * @function
+ * @name selectedHtml
+ * @memberOf RangeHelper.prototype
+ */
+ base.selectedHtml = function () {
+ var div,
+ range = base.selectedRange();
+
+ if (range) {
+ div = createElement('p', {}, doc);
+ appendChild(div, range.cloneContents());
+
+ return div.innerHTML;
+ }
+
+ return '';
+ };
+
+ /**
+ * Gets the parent node of the selected contents in the range
+ *
+ * @return {HTMLElement}
+ * @function
+ * @name parentNode
+ * @memberOf RangeHelper.prototype
+ */
+ base.parentNode = function () {
+ var range = base.selectedRange();
+
+ if (range) {
+ return range.commonAncestorContainer;
+ }
+ };
+
+ /**
+ * Gets the first block level parent of the selected
+ * contents of the range.
+ *
+ * @return {HTMLElement}
+ * @function
+ * @name getFirstBlockParent
+ * @memberOf RangeHelper.prototype
+ */
+ /**
+ * Gets the first block level parent of the selected
+ * contents of the range.
+ *
+ * @param {Node} [n] The element to get the first block level parent from
+ * @return {HTMLElement}
+ * @function
+ * @name getFirstBlockParent^2
+ * @since 1.4.1
+ * @memberOf RangeHelper.prototype
+ */
+ base.getFirstBlockParent = function (node) {
+ var func = function (elm) {
+ if (!isInline(elm, true)) {
+ return elm;
+ }
+
+ elm = elm ? elm.parentNode : null;
+
+ return elm ? func(elm) : elm;
+ };
+
+ return func(node || base.parentNode());
+ };
+
+ /**
+ * Inserts a node at either the start or end of the current selection
+ *
+ * @param {Bool} start
+ * @param {Node} node
+ * @function
+ * @name insertNodeAt
+ * @memberOf RangeHelper.prototype
+ */
+ base.insertNodeAt = function (start, node) {
+ var currentRange = base.selectedRange(),
+ range = base.cloneSelected();
+
+ if (!range) {
+ return false;
+ }
+
+ range.collapse(start);
+ range.insertNode(node);
+
+ // Reselect the current range.
+ // Fixes issue with Chrome losing the selection. Issue#82
+ base.selectRange(currentRange);
+ };
+
+ /**
+ * Creates a marker node
+ *
+ * @param {string} id
+ * @return {HTMLSpanElement}
+ * @private
+ */
+ _createMarker = function (id) {
+ base.removeMarker(id);
+
+ var marker = createElement('span', {
+ id: id,
+ className: 'sceditor-selection sceditor-ignore',
+ style: 'display:none;line-height:0'
+ }, doc);
+
+ marker.innerHTML = ' ';
+
+ return marker;
+ };
+
+ /**
+ * Inserts start/end markers for the current selection
+ * which can be used by restoreRange to re-select the
+ * range.
+ *
+ * @memberOf RangeHelper.prototype
+ * @function
+ * @name insertMarkers
+ */
+ base.insertMarkers = function () {
+ var currentRange = base.selectedRange();
+ var startNode = _createMarker(startMarker);
+
+ base.removeMarkers();
+ base.insertNodeAt(true, startNode);
+
+ // Fixes issue with end marker sometimes being placed before
+ // the start marker when the range is collapsed.
+ if (currentRange && currentRange.collapsed) {
+ startNode.parentNode.insertBefore(
+ _createMarker(endMarker), startNode.nextSibling);
+ } else {
+ base.insertNodeAt(false, _createMarker(endMarker));
+ }
+ };
+
+ /**
+ * Gets the marker with the specified ID
+ *
+ * @param {string} id
+ * @return {Node}
+ * @function
+ * @name getMarker
+ * @memberOf RangeHelper.prototype
+ */
+ base.getMarker = function (id) {
+ return doc.getElementById(id);
+ };
+
+ /**
+ * Removes the marker with the specified ID
+ *
+ * @param {string} id
+ * @function
+ * @name removeMarker
+ * @memberOf RangeHelper.prototype
+ */
+ base.removeMarker = function (id) {
+ var marker = base.getMarker(id);
+
+ if (marker) {
+ remove(marker);
+ }
+ };
+
+ /**
+ * Removes the start/end markers
+ *
+ * @function
+ * @name removeMarkers
+ * @memberOf RangeHelper.prototype
+ */
+ base.removeMarkers = function () {
+ base.removeMarker(startMarker);
+ base.removeMarker(endMarker);
+ };
+
+ /**
+ * Saves the current range location. Alias of insertMarkers()
+ *
+ * @function
+ * @name saveRage
+ * @memberOf RangeHelper.prototype
+ */
+ base.saveRange = function () {
+ base.insertMarkers();
+ };
+
+ /**
+ * Select the specified range
+ *
+ * @param {Range} range
+ * @function
+ * @name selectRange
+ * @memberOf RangeHelper.prototype
+ */
+ base.selectRange = function (range) {
+ var lastChild;
+ var sel = win.getSelection();
+ var container = range.endContainer;
+
+ // Check if cursor is set after a BR when the BR is the only
+ // child of the parent. In Firefox this causes a line break
+ // to occur when something is typed. See issue #321
+ if (!IE_BR_FIX$1 && range.collapsed && container &&
+ !isInline(container, true)) {
+
+ lastChild = container.lastChild;
+ while (lastChild && is(lastChild, '.sceditor-ignore')) {
+ lastChild = lastChild.previousSibling;
+ }
+
+ if (is(lastChild, 'br')) {
+ var rng = doc.createRange();
+ rng.setEndAfter(lastChild);
+ rng.collapse(false);
+
+ if (base.compare(range, rng)) {
+ range.setStartBefore(lastChild);
+ range.collapse(true);
+ }
+ }
+ }
+
+ if (sel) {
+ base.clear();
+ sel.addRange(range);
+ }
+ };
+
+ /**
+ * Restores the last range saved by saveRange() or insertMarkers()
+ *
+ * @function
+ * @name restoreRange
+ * @memberOf RangeHelper.prototype
+ */
+ base.restoreRange = function () {
+ var isCollapsed,
+ range = base.selectedRange(),
+ start = base.getMarker(startMarker),
+ end = base.getMarker(endMarker);
+
+ if (!start || !end || !range) {
+ return false;
+ }
+
+ isCollapsed = start.nextSibling === end;
+
+ range = doc.createRange();
+ range.setStartBefore(start);
+ range.setEndAfter(end);
+
+ if (isCollapsed) {
+ range.collapse(true);
+ }
+
+ base.selectRange(range);
+ base.removeMarkers();
+ };
+
+ /**
+ * Selects the text left and right of the current selection
+ *
+ * @param {number} left
+ * @param {number} right
+ * @since 1.4.3
+ * @function
+ * @name selectOuterText
+ * @memberOf RangeHelper.prototype
+ */
+ base.selectOuterText = function (left, right) {
+ var start, end,
+ range = base.cloneSelected();
+
+ if (!range) {
+ return false;
+ }
+
+ range.collapse(false);
+
+ start = outerText(range, true, left);
+ end = outerText(range, false, right);
+
+ range.setStart(start.node, start.offset);
+ range.setEnd(end.node, end.offset);
+
+ base.selectRange(range);
+ };
+
+ /**
+ * Gets the text left or right of the current selection
+ *
+ * @param {boolean} before
+ * @param {number} length
+ * @return {string}
+ * @since 1.4.3
+ * @function
+ * @name selectOuterText
+ * @memberOf RangeHelper.prototype
+ */
+ base.getOuterText = function (before, length) {
+ var range = base.cloneSelected();
+
+ if (!range) {
+ return '';
+ }
+
+ range.collapse(!before);
+
+ return outerText(range, before, length).text;
+ };
+
+ /**
+ * Replaces keywords with values based on the current caret position
+ *
+ * @param {Array} keywords
+ * @param {boolean} includeAfter If to include the text after the
+ * current caret position or just
+ * text before
+ * @param {boolean} keywordsSorted If the keywords array is pre
+ * sorted shortest to longest
+ * @param {number} longestKeyword Length of the longest keyword
+ * @param {boolean} requireWhitespace If the key must be surrounded
+ * by whitespace
+ * @param {string} keypressChar If this is being called from
+ * a keypress event, this should be
+ * set to the pressed character
+ * @return {boolean}
+ * @function
+ * @name replaceKeyword
+ * @memberOf RangeHelper.prototype
+ */
+ // eslint-disable-next-line max-params
+ base.replaceKeyword = function (
+ keywords,
+ includeAfter,
+ keywordsSorted,
+ longestKeyword,
+ requireWhitespace,
+ keypressChar
+ ) {
+ if (!keywordsSorted) {
+ keywords.sort(function (a, b) {
+ return a[0].length - b[0].length;
+ });
+ }
+
+ var outerText, match, matchPos, startIndex,
+ leftLen, charsLeft, keyword, keywordLen,
+ whitespaceRegex = '(^|[\\s\xA0\u2002\u2003\u2009])',
+ keywordIdx = keywords.length,
+ whitespaceLen = requireWhitespace ? 1 : 0,
+ maxKeyLen = longestKeyword ||
+ keywords[keywordIdx - 1][0].length;
+
+ if (requireWhitespace) {
+ maxKeyLen++;
+ }
+
+ keypressChar = keypressChar || '';
+ outerText = base.getOuterText(true, maxKeyLen);
+ leftLen = outerText.length;
+ outerText += keypressChar;
+
+ if (includeAfter) {
+ outerText += base.getOuterText(false, maxKeyLen);
+ }
+
+ while (keywordIdx--) {
+ keyword = keywords[keywordIdx][0];
+ keywordLen = keyword.length;
+ startIndex = Math.max(0, leftLen - keywordLen - whitespaceLen);
+ matchPos = -1;
+
+ if (requireWhitespace) {
+ match = outerText
+ .substr(startIndex)
+ .match(new RegExp(whitespaceRegex +
+ regex(keyword) + whitespaceRegex));
+
+ if (match) {
+ // Add the length of the text that was removed by
+ // substr() and also add 1 for the whitespace
+ matchPos = match.index + startIndex + match[1].length;
+ }
+ } else {
+ matchPos = outerText.indexOf(keyword, startIndex);
+ }
+
+ if (matchPos > -1) {
+ // Make sure the match is between before and
+ // after, not just entirely in one side or the other
+ if (matchPos <= leftLen &&
+ matchPos + keywordLen + whitespaceLen >= leftLen) {
+ charsLeft = leftLen - matchPos;
+
+ // If the keypress char is white space then it should
+ // not be replaced, only chars that are part of the
+ // key should be replaced.
+ base.selectOuterText(
+ charsLeft,
+ keywordLen - charsLeft -
+ (/^\S/.test(keypressChar) ? 1 : 0)
+ );
+
+ base.insertHTML(keywords[keywordIdx][1]);
+ return true;
+ }
+ }
+ }
+
+ return false;
+ };
+
+ /**
+ * Compares two ranges.
+ *
+ * If rangeB is undefined it will be set to
+ * the current selected range
+ *
+ * @param {Range} rngA
+ * @param {Range} [rngB]
+ * @return {boolean}
+ * @function
+ * @name compare
+ * @memberOf RangeHelper.prototype
+ */
+ base.compare = function (rngA, rngB) {
+ if (!rngB) {
+ rngB = base.selectedRange();
+ }
+
+ if (!rngA || !rngB) {
+ return !rngA && !rngB;
+ }
+
+ return rngA.compareBoundaryPoints(Range.END_TO_END, rngB) === 0 &&
+ rngA.compareBoundaryPoints(Range.START_TO_START, rngB) === 0;
+ };
+
+ /**
+ * Removes any current selection
+ *
+ * @since 1.4.6
+ * @function
+ * @name clear
+ * @memberOf RangeHelper.prototype
+ */
+ base.clear = function () {
+ var sel = win.getSelection();
+
+ if (sel) {
+ if (sel.removeAllRanges) {
+ sel.removeAllRanges();
+ } else if (sel.empty) {
+ sel.empty();
+ }
+ }
+ };
+ }
+
+ /**
+ * Checks all emoticons are surrounded by whitespace and
+ * replaces any that aren't with with their emoticon code.
+ *
+ * @param {HTMLElement} node
+ * @param {rangeHelper} rangeHelper
+ * @return {void}
+ */
+ function checkWhitespace(node, rangeHelper) {
+ var noneWsRegex = /[^\s\xA0\u2002\u2003\u2009\u00a0]+/;
+ var emoticons = node && find(node, 'img[data-sceditor-emoticon]');
+
+ if (!node || !emoticons.length) {
+ return;
+ }
+
+ for (var i = 0; i < emoticons.length; i++) {
+ var emoticon = emoticons[i];
+ var parent$$1 = emoticon.parentNode;
+ var prev = emoticon.previousSibling;
+ var next = emoticon.nextSibling;
+
+ if ((!prev || !noneWsRegex.test(prev.nodeValue.slice(-1))) &&
+ (!next || !noneWsRegex.test((next.nodeValue || '')[0]))) {
+ continue;
+ }
+
+ var range = rangeHelper.cloneSelected();
+ var rangeStart = -1;
+ var rangeStartContainer = range.startContainer;
+ var previousText = prev.nodeValue;
+
+ // For IE's HTMLPhraseElement
+ if (previousText === null) {
+ previousText = prev.innerText || '';
+ }
+
+ previousText += data(emoticon, 'sceditor-emoticon');
+
+ // If the cursor is after the removed emoticon, add
+ // the length of the newly added text to it
+ if (rangeStartContainer === next) {
+ rangeStart = previousText.length + range.startOffset;
+ }
+
+ // If the cursor is set before the next node, set it to
+ // the end of the new text node
+ if (rangeStartContainer === node &&
+ node.childNodes[range.startOffset] === next) {
+ rangeStart = previousText.length;
+ }
+
+ // If the cursor is set before the removed emoticon,
+ // just keep it at that position
+ if (rangeStartContainer === prev) {
+ rangeStart = range.startOffset;
+ }
+
+ if (!next || next.nodeType !== TEXT_NODE) {
+ next = parent$$1.insertBefore(
+ parent$$1.ownerDocument.createTextNode(''), next
+ );
+ }
+
+ next.insertData(0, previousText);
+ remove(prev);
+ remove(emoticon);
+
+ // Need to update the range starting position if it's been modified
+ if (rangeStart > -1) {
+ range.setStart(next, rangeStart);
+ range.collapse(true);
+ rangeHelper.selectRange(range);
+ }
+ }
+ }
+
+ /**
+ * Replaces any emoticons inside the root node with images.
+ *
+ * emoticons should be an object where the key is the emoticon
+ * code and the value is the HTML to replace it with.
+ *
+ * @param {HTMLElement} root
+ * @param {Object} emoticons
+ * @param {boolean} emoticonsCompat
+ * @return {void}
+ */
+ function replace(root, emoticons, emoticonsCompat) {
+ var doc = root.ownerDocument;
+ var space = '(^|\\s|\xA0|\u2002|\u2003|\u2009|$)';
+ var emoticonCodes = [];
+ var emoticonRegex = {};
+
+ // TODO: Make this tag configurable.
+ if (parent(root, 'code')) {
+ return;
+ }
+
+ each(emoticons, function (key) {
+ emoticonRegex[key] = new RegExp(space + regex(key) + space);
+ emoticonCodes.push(key);
+ });
+
+ // Sort keys longest to shortest so that longer keys
+ // take precedence (avoids bugs with shorter keys partially
+ // matching longer ones)
+ emoticonCodes.sort(function (a, b) {
+ return b.length - a.length;
+ });
+
+ (function convert(node) {
+ node = node.firstChild;
+
+ while (node) {
+ // TODO: Make this tag configurable.
+ if (node.nodeType === ELEMENT_NODE && !is(node, 'code')) {
+ convert(node);
+ }
+
+ if (node.nodeType === TEXT_NODE) {
+ for (var i = 0; i < emoticonCodes.length; i++) {
+ var text = node.nodeValue;
+ var key = emoticonCodes[i];
+ var index = emoticonsCompat ?
+ text.search(emoticonRegex[key]) :
+ text.indexOf(key);
+
+ if (index > -1) {
+ // When emoticonsCompat is enabled this will be the
+ // position after any white space
+ var startIndex = text.indexOf(key, index);
+ var fragment = parseHTML(emoticons[key], doc);
+ var after = text.substr(startIndex + key.length);
+
+ fragment.appendChild(doc.createTextNode(after));
+
+ node.nodeValue = text.substr(0, startIndex);
+ node.parentNode
+ .insertBefore(fragment, node.nextSibling);
+ }
+ }
+ }
+
+ node = node.nextSibling;
+ }
+ }(root));
+ }
+
+ var globalWin = window;
+ var globalDoc = document;
+
+ var IE_VER = 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$2 = IE_VER && IE_VER < 11;
+
+ var IMAGE_MIME_REGEX = /^image\/(p?jpe?g|gif|png|bmp)$/i;
+
+ /**
+ * Wrap inlines that are in the root in paragraphs.
+ *
+ * @param {HTMLBodyElement} body
+ * @param {Document} doc
+ * @private
+ */
+ function wrapInlines(body, doc) {
+ var wrapper;
+
+ traverse(body, function (node) {
+ if (isInline(node, true)) {
+ if (!wrapper) {
+ wrapper = createElement('p', {}, doc);
+ insertBefore(wrapper, node);
+ }
+
+ if (node.nodeType !== TEXT_NODE || node.nodeValue !== '') {
+ appendChild(wrapper, node);
+ }
+ } else {
+ wrapper = null;
+ }
+ }, false, true);
+ }
+
+ /**
+ * SCEditor - A lightweight WYSIWYG editor
+ *
+ * @param {HTMLTextAreaElement} original The textarea to be converted
+ * @param {Object} userOptions
+ * @class SCEditor
+ * @name SCEditor
+ */
+ function SCEditor(original, userOptions) {
+ /**
+ * Alias of this
+ *
+ * @private
+ */
+ var base = this;
+
+ /**
+ * Editor format like BBCode or HTML
+ */
+ var format;
+
+ /**
+ * The div which contains the editor and toolbar
+ *
+ * @type {HTMLDivElement}
+ * @private
+ */
+ var editorContainer;
+
+ /**
+ * Map of events handlers bound to this instance.
+ *
+ * @type {Object}
+ * @private
+ */
+ var eventHandlers = {};
+
+ /**
+ * The editors toolbar
+ *
+ * @type {HTMLDivElement}
+ * @private
+ */
+ var toolbar;
+
+ /**
+ * The editors iframe which should be in design mode
+ *
+ * @type {HTMLIFrameElement}
+ * @private
+ */
+ var wysiwygEditor;
+
+ /**
+ * The editors window
+ *
+ * @type {Window}
+ * @private
+ */
+ var wysiwygWindow;
+
+ /**
+ * The WYSIWYG editors body element
+ *
+ * @type {HTMLBodyElement}
+ * @private
+ */
+ var wysiwygBody;
+
+ /**
+ * The WYSIWYG editors document
+ *
+ * @type {Document}
+ * @private
+ */
+ var wysiwygDocument;
+
+ /**
+ * The editors textarea for viewing source
+ *
+ * @type {HTMLTextAreaElement}
+ * @private
+ */
+ var sourceEditor;
+
+ /**
+ * The current dropdown
+ *
+ * @type {HTMLDivElement}
+ * @private
+ */
+ var dropdown;
+
+ /**
+ * Store the last cursor position. Needed for IE because it forgets
+ *
+ * @type {Range}
+ * @private
+ */
+ var lastRange;
+
+ /**
+ * If the user is currently composing text via IME
+ * @type {boolean}
+ */
+ var isComposing;
+
+ /**
+ * Timer for valueChanged key handler
+ * @type {number}
+ */
+ var valueChangedKeyUpTimer;
+
+ /**
+ * The editors locale
+ *
+ * @private
+ */
+ var locale;
+
+ /**
+ * Stores a cache of preloaded images
+ *
+ * @private
+ * @type {Array.}
+ */
+ var preLoadCache = [];
+
+ /**
+ * The editors rangeHelper instance
+ *
+ * @type {RangeHelper}
+ * @private
+ */
+ var rangeHelper;
+
+ /**
+ * An array of button state handlers
+ *
+ * @type {Array.}
+ * @private
+ */
+ var btnStateHandlers = [];
+
+ /**
+ * Plugin manager instance
+ *
+ * @type {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 {HTMLStyleElement}
+ * @private
+ */
+ var inlineCss;
+
+ /**
+ * Object containing a list of shortcut handlers
+ *
+ * @type {Object}
+ * @private
+ */
+ var shortcutHandlers = {};
+
+ /**
+ * The min and max heights that autoExpand should stay within
+ *
+ * @type {Object}
+ * @private
+ */
+ var autoExpandBounds;
+
+ /**
+ * Timeout for the autoExpand function to throttle calls
+ *
+ * @private
+ */
+ var autoExpandThrottle;
+
+ /**
+ * Cache of the current toolbar buttons
+ *
+ * @type {Object}
+ * @private
+ */
+ var toolbarButtons = {};
+
+ /**
+ * Last scroll position before maximizing so
+ * it can be restored when finished.
+ *
+ * @type {number}
+ * @private
+ */
+ var maximizeScrollPosition;
+
+ /**
+ * Stores the contents while a paste is taking place.
+ *
+ * Needed to support browsers that lack clipboard API support.
+ *
+ * @type {?DocumentFragment}
+ * @private
+ */
+ var pasteContentFragment;
+
+ /**
+ * All the emoticons from dropdown, more and hidden combined
+ * and with the emoticons root set
+ *
+ * @type {!Object}
+ * @private
+ */
+ var allEmoticons = {};
+
+ /**
+ * Current icon set if any
+ *
+ * @type {?Object}
+ * @private
+ */
+ var icons;
+
+ /**
+ * Private functions
+ * @private
+ */
+ var init,
+ replaceEmoticons,
+ handleCommand,
+ saveRange,
+ initEditor,
+ initPlugins,
+ initLocale,
+ initToolBar,
+ initOptions,
+ initEvents,
+ initResize,
+ initEmoticons,
+ handlePasteEvt,
+ handlePasteData,
+ handleKeyDown,
+ handleBackSpace,
+ handleKeyPress,
+ handleFormReset,
+ handleMouseDown,
+ handleComposition,
+ handleEvent,
+ handleDocumentClick,
+ updateToolBar,
+ updateActiveButtons,
+ sourceEditorSelectedText,
+ appendNewLine,
+ checkSelectionChanged,
+ checkNodeChanged,
+ autofocus,
+ emoticonsKeyPress,
+ emoticonsCheckWhitespace,
+ currentStyledBlockNode,
+ triggerValueChanged,
+ valueChangedBlur,
+ valueChangedKeyUp,
+ autoUpdate,
+ autoExpand;
+
+ /**
+ * All the commands supported by the editor
+ * @name commands
+ * @memberOf SCEditor.prototype
+ */
+ base.commands = extend(true, {}, (userOptions.commands || defaultCmds));
+
+ /**
+ * Options for this editor instance
+ * @name opts
+ * @memberOf SCEditor.prototype
+ */
+ var options = base.opts = extend(
+ true, {}, defaultOptions, userOptions
+ );
+
+ // Don't deep extend emoticons (fixes #565)
+ base.opts.emoticons = userOptions.emoticons || defaultOptions.emoticons;
+
+ /**
+ * Creates the editor iframe and textarea
+ * @private
+ */
+ init = function () {
+ original._sceditor = base;
+
+ // Load locale
+ if (options.locale && options.locale !== 'en') {
+ initLocale();
+ }
+
+ editorContainer = createElement('div', {
+ className: 'sceditor-container'
+ });
+
+ insertBefore(editorContainer, original);
+ css(editorContainer, '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) {
+ addClass(editorContainer, 'ie ie' + IE_VER);
+ }
+
+ isRequired = original.required;
+ original.required = false;
+
+ var FormatCtor = SCEditor.formats[options.format];
+ format = FormatCtor ? new FormatCtor() : {};
+ if ('init' in format) {
+ format.init.call(base);
+ }
+
+ // create the editor
+ initPlugins();
+ initEmoticons();
+ initToolBar();
+ initEditor();
+ initOptions();
+ initEvents();
+
+ // force into source mode if is a browser that can't handle
+ // full editing
+ if (!isWysiwygSupported) {
+ base.toggleSourceMode();
+ }
+
+ updateActiveButtons();
+
+ var loaded = function () {
+ off(globalWin, 'load', loaded);
+
+ if (options.autofocus) {
+ autofocus();
+ }
+
+ autoExpand();
+ appendNewLine();
+ // TODO: use editor doc and window?
+ pluginManager.call('ready');
+ if ('onReady' in format) {
+ format.onReady.call(base);
+ }
+ };
+ on(globalWin, 'load', loaded);
+ if (globalDoc.readyState === 'complete') {
+ loaded();
+ }
+ };
+
+ initPlugins = function () {
+ var plugins = options.plugins;
+
+ plugins = plugins ? plugins.toString().split(',') : [];
+ pluginManager = new PluginManager(base);
+
+ plugins.forEach(function (plugin) {
+ pluginManager.register(plugin.trim());
+ });
+ };
+
+ /**
+ * 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
+ * @private
+ */
+ initEditor = function () {
+ sourceEditor = createElement('textarea');
+ wysiwygEditor = createElement('iframe', {
+ frameborder: 0,
+ allowfullscreen: true
+ });
+
+ /* 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 (options.startInSourceMode) {
+ addClass(editorContainer, 'sourceMode');
+ hide(wysiwygEditor);
+ } else {
+ addClass(editorContainer, 'wysiwygMode');
+ hide(sourceEditor);
+ }
+
+ if (!options.spellcheck) {
+ attr(editorContainer, 'spellcheck', 'false');
+ }
+
+ if (globalWin.location.protocol === 'https:') {
+ // eslint-disable-next-line no-script-url
+ attr(wysiwygEditor, 'src', 'javascript:false');
+ }
+
+ // Add the editor to the container
+ appendChild(editorContainer, wysiwygEditor);
+ appendChild(editorContainer, sourceEditor);
+
+ // TODO: make this optional somehow
+ base.dimensions(
+ options.width || width(original),
+ options.height || height(original)
+ );
+
+ // Add IE version class to the HTML element so can apply
+ // conditional styling without CSS hacks
+ var className = IE_VER ? 'ie ie' + IE_VER : '';
+ // Add ios to HTML so can apply CSS fix to only it
+ className += ios ? ' ios' : '';
+
+ wysiwygDocument = wysiwygEditor.contentDocument;
+ wysiwygDocument.open();
+ wysiwygDocument.write(_tmpl('html', {
+ attrs: ' class="' + className + '"',
+ spellcheck: options.spellcheck ? '' : 'spellcheck="false"',
+ charset: options.charset,
+ style: options.style
+ }));
+ wysiwygDocument.close();
+
+ wysiwygBody = wysiwygDocument.body;
+ wysiwygWindow = wysiwygEditor.contentWindow;
+
+ base.readOnly(!!options.readOnly);
+
+ // iframe overflow fix for iOS, also fixes an IE issue with the
+ // editor not getting focus when clicking inside
+ if (ios || edge || IE_VER) {
+ height(wysiwygBody, '100%');
+
+ if (!IE_VER) {
+ on(wysiwygBody, 'touchend', base.focus);
+ }
+ }
+
+ var tabIndex = attr(original, 'tabindex');
+ attr(sourceEditor, 'tabindex', tabIndex);
+ attr(wysiwygEditor, 'tabindex', tabIndex);
+
+ rangeHelper = new RangeHelper(wysiwygWindow);
+
+ // load any textarea value into the editor
+ hide(original);
+ base.val(original.value);
+
+ var placeholder = options.placeholder ||
+ attr(original, 'placeholder');
+
+ if (placeholder) {
+ sourceEditor.placeholder = placeholder;
+ attr(wysiwygBody, 'placeholder', placeholder);
+ }
+ };
+
+ /**
+ * Initialises options
+ * @private
+ */
+ initOptions = function () {
+ // auto-update original textbox on blur if option set to true
+ if (options.autoUpdate) {
+ on(wysiwygBody, 'blur', autoUpdate);
+ on(sourceEditor, 'blur', autoUpdate);
+ }
+
+ if (options.rtl === null) {
+ options.rtl = css(sourceEditor, 'direction') === 'rtl';
+ }
+
+ base.rtl(!!options.rtl);
+
+ if (options.autoExpand) {
+ // Need to update when images (or anything else) loads
+ on(wysiwygBody, 'load', autoExpand, EVENT_CAPTURE);
+ on(wysiwygBody, 'input keyup', autoExpand);
+ }
+
+ if (options.resizeEnabled) {
+ initResize();
+ }
+
+ attr(editorContainer, 'id', options.id);
+ base.emoticons(options.emoticonsEnabled);
+ };
+
+ /**
+ * Initialises events
+ * @private
+ */
+ initEvents = function () {
+ var form = original.form;
+ var compositionEvents = 'compositionstart compositionend';
+ var eventsToForward = 'keydown keyup keypress focus blur contextmenu';
+ var checkSelectionEvents = 'onselectionchange' in wysiwygDocument ?
+ 'selectionchange' :
+ 'keyup focus blur contextmenu mouseup touchend click';
+
+ on(globalDoc, 'click', handleDocumentClick);
+
+ if (form) {
+ on(form, 'reset', handleFormReset);
+ on(form, 'submit', base.updateOriginal, EVENT_CAPTURE);
+ }
+
+ on(wysiwygBody, 'keypress', handleKeyPress);
+ on(wysiwygBody, 'keydown', handleKeyDown);
+ on(wysiwygBody, 'keydown', handleBackSpace);
+ on(wysiwygBody, 'keyup', appendNewLine);
+ on(wysiwygBody, 'blur', valueChangedBlur);
+ on(wysiwygBody, 'keyup', valueChangedKeyUp);
+ on(wysiwygBody, 'paste', handlePasteEvt);
+ on(wysiwygBody, compositionEvents, handleComposition);
+ on(wysiwygBody, checkSelectionEvents, checkSelectionChanged);
+ on(wysiwygBody, eventsToForward, handleEvent);
+
+ if (options.emoticonsCompat && globalWin.getSelection) {
+ on(wysiwygBody, 'keyup', emoticonsCheckWhitespace);
+ }
+
+ on(wysiwygBody, 'blur', function () {
+ if (!base.val()) {
+ addClass(wysiwygBody, 'placeholder');
+ }
+ });
+
+ on(wysiwygBody, 'focus', function () {
+ removeClass(wysiwygBody, 'placeholder');
+ });
+
+ on(sourceEditor, 'blur', valueChangedBlur);
+ on(sourceEditor, 'keyup', valueChangedKeyUp);
+ on(sourceEditor, 'keydown', handleKeyDown);
+ on(sourceEditor, compositionEvents, handleComposition);
+ on(sourceEditor, eventsToForward, handleEvent);
+
+ on(wysiwygDocument, 'mousedown', handleMouseDown);
+ on(wysiwygDocument, checkSelectionEvents, checkSelectionChanged);
+ on(wysiwygDocument, 'beforedeactivate keyup mouseup', saveRange);
+ on(wysiwygDocument, 'keyup', appendNewLine);
+ on(wysiwygDocument, 'focus', function () {
+ lastRange = null;
+ });
+
+ on(editorContainer, 'selectionchanged', checkNodeChanged);
+ on(editorContainer, 'selectionchanged', updateActiveButtons);
+ // Custom events to forward
+ on(
+ editorContainer,
+ 'selectionchanged valuechanged nodechanged pasteraw paste',
+ 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 = createElement('div', {
+ className: 'sceditor-toolbar',
+ unselectable: 'on'
+ });
+
+ if (options.icons in SCEditor.icons) {
+ icons = new SCEditor.icons[options.icons]();
+ }
+
+ each(groups, function (_, menuItems) {
+ group = createElement('div', {
+ className: 'sceditor-group'
+ });
+
+ each(menuItems.split(','), function (_, commandName) {
+ var button, shortcut,
+ command = commands[commandName];
+
+ // The commandName must be a valid command and not excluded
+ if (!command || exclude.indexOf(commandName) > -1) {
+ return;
+ }
+
+ shortcut = command.shortcut;
+ button = _tmpl('toolbarButton', {
+ name: commandName,
+ dispName: base._(command.name ||
+ command.tooltip || commandName)
+ }, true).firstChild;
+
+ if (icons && icons.create) {
+ var icon = icons.create(commandName);
+ if (icon) {
+ insertBefore(icons.create(commandName),
+ button.firstChild);
+ addClass(button, 'has-icon');
+ }
+ }
+
+ button._sceTxtMode = !!command.txtExec;
+ button._sceWysiwygMode = !!command.exec;
+ toggleClass(button, 'disabled', !command.exec);
+ on(button, 'click', function (e) {
+ if (!hasClass(button, 'disabled')) {
+ handleCommand(button, command);
+ }
+
+ updateActiveButtons();
+ e.preventDefault();
+ });
+ // Prevent editor losing focus when button clicked
+ on(button, 'mousedown', function (e) {
+ base.closeDropDown();
+ e.preventDefault();
+ });
+
+ if (command.tooltip) {
+ attr(button, '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 (isString(command.exec)) {
+ btnStateHandlers.push({
+ name: commandName,
+ state: command.exec
+ });
+ }
+
+ appendChild(group, button);
+ toolbarButtons[commandName] = button;
+ });
+
+ // Exclude empty groups
+ if (group.firstChild) {
+ appendChild(toolbar, group);
+ }
+ });
+
+ // Append the toolbar to the toolbarContainer option if given
+ appendChild(options.toolbarContainer || editorContainer, toolbar);
+ };
+
+ /**
+ * Creates the resizer.
+ * @private
+ */
+ initResize = function () {
+ var minHeight, maxHeight, minWidth, maxWidth,
+ mouseMoveFunc, mouseUpFunc,
+ grip = createElement('div', {
+ className: 'sceditor-grip'
+ }),
+ // Cover is used to cover the editor iframe so document
+ // still gets mouse move events
+ cover = createElement('div', {
+ className: 'sceditor-resize-cover'
+ }),
+ moveEvents = 'touchmove mousemove',
+ endEvents = 'touchcancel touchend mouseup',
+ startX = 0,
+ startY = 0,
+ newX = 0,
+ newY = 0,
+ startWidth = 0,
+ startHeight = 0,
+ origWidth = width(editorContainer),
+ origHeight = height(editorContainer),
+ 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);
+ }
+
+ e.preventDefault();
+ };
+
+ mouseUpFunc = function (e) {
+ if (!isDragging) {
+ return;
+ }
+
+ isDragging = false;
+
+ hide(cover);
+ removeClass(editorContainer, 'resizing');
+ off(globalDoc, moveEvents, mouseMoveFunc);
+ off(globalDoc, endEvents, mouseUpFunc);
+
+ e.preventDefault();
+ };
+
+ if (icons && icons.create) {
+ var icon = icons.create('grip');
+ if (icon) {
+ appendChild(grip, icon);
+ addClass(grip, 'has-icon');
+ }
+ }
+
+ appendChild(editorContainer, grip);
+ appendChild(editorContainer, cover);
+ hide(cover);
+
+ on(grip, '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 = width(editorContainer);
+ startHeight = height(editorContainer);
+ isDragging = true;
+
+ addClass(editorContainer, 'resizing');
+ show(cover);
+ on(globalDoc, moveEvents, mouseMoveFunc);
+ on(globalDoc, endEvents, mouseUpFunc);
+
+ e.preventDefault();
+ });
+ };
+
+ /**
+ * Prefixes and preloads the emoticon images
+ * @private
+ */
+ initEmoticons = function () {
+ var emoticons = options.emoticons;
+ var root = options.emoticonsRoot || '';
+
+ if (emoticons) {
+ allEmoticons = extend(
+ {}, emoticons.more, emoticons.dropdown, emoticons.hidden
+ );
+ }
+
+ each(allEmoticons, function (key, url) {
+ allEmoticons[key] = _tmpl('emoticon', {
+ key: key,
+ // Prefix emoticon root to emoticon urls
+ url: root + (url.url || url),
+ tooltip: url.tooltip || key
+ });
+
+ // Preload the emoticon
+ if (options.emoticonsEnabled) {
+ preLoadCache.push(createElement('img', {
+ src: root + (url.url || url)
+ }));
+ }
+ });
+ };
+
+ /**
+ * Autofocus the editor
+ * @private
+ */
+ autofocus = function () {
+ var range, txtPos,
+ node = wysiwygBody.firstChild,
+ focusEnd = !!options.autofocusEnd;
+
+ // Can't focus invisible elements
+ if (!isVisible(editorContainer)) {
+ return;
+ }
+
+ if (base.sourceMode()) {
+ txtPos = focusEnd ? sourceEditor.value.length : 0;
+
+ sourceEditor.setSelectionRange(txtPos, txtPos);
+
+ return;
+ }
+
+ removeWhiteSpace(wysiwygBody);
+
+ if (focusEnd) {
+ if (!(node = wysiwygBody.lastChild)) {
+ node = createElement('p', {}, wysiwygDocument);
+ appendChild(wysiwygBody, 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$2 && is(node, 'br') && node.previousSibling) {
+ node = node.previousSibling;
+ }
+ }
+ }
+
+ range = wysiwygDocument.createRange();
+
+ if (!canHaveChildren(node)) {
+ range.setStartBefore(node);
+
+ if (focusEnd) {
+ range.setStartAfter(node);
+ }
+ } else {
+ range.selectNodeContents(node);
+ }
+
+ range.collapse(!focusEnd);
+ rangeHelper.selectRange(range);
+ currentSelection = range;
+
+ if (focusEnd) {
+ wysiwygBody.scrollTop = wysiwygBody.scrollHeight;
+ }
+
+ base.focus();
+ };
+
+ /**
+ * Gets if the editor is read only
+ *
+ * @since 1.3.5
+ * @function
+ * @memberOf SCEditor.prototype
+ * @name readOnly
+ * @return {boolean}
+ */
+ /**
+ * Sets if the editor is read only
+ *
+ * @param {boolean} readOnly
+ * @since 1.3.5
+ * @function
+ * @memberOf SCEditor.prototype
+ * @name readOnly^2
+ * @return {this}
+ */
+ base.readOnly = function (readOnly) {
+ if (typeof readOnly !== 'boolean') {
+ return !sourceEditor.readonly;
+ }
+
+ wysiwygBody.contentEditable = !readOnly;
+ sourceEditor.readonly = !readOnly;
+
+ updateToolBar(readOnly);
+
+ return base;
+ };
+
+ /**
+ * Gets if the editor is in RTL mode
+ *
+ * @since 1.4.1
+ * @function
+ * @memberOf SCEditor.prototype
+ * @name rtl
+ * @return {boolean}
+ */
+ /**
+ * Sets if the editor is in RTL mode
+ *
+ * @param {boolean} rtl
+ * @since 1.4.1
+ * @function
+ * @memberOf SCEditor.prototype
+ * @name rtl^2
+ * @return {this}
+ */
+ base.rtl = function (rtl) {
+ var dir = rtl ? 'rtl' : 'ltr';
+
+ if (typeof rtl !== 'boolean') {
+ return attr(sourceEditor, 'dir') === 'rtl';
+ }
+
+ attr(wysiwygBody, 'dir', dir);
+ attr(sourceEditor, 'dir', dir);
+
+ removeClass(editorContainer, 'rtl');
+ removeClass(editorContainer, 'ltr');
+ addClass(editorContainer, dir);
+
+ if (icons && icons.rtl) {
+ icons.rtl(rtl);
+ }
+
+ return base;
+ };
+
+ /**
+ * Updates the toolbar to disable/enable the appropriate buttons
+ * @private
+ */
+ updateToolBar = function (disable) {
+ var mode = base.inSourceMode() ? '_sceTxtMode' : '_sceWysiwygMode';
+
+ each(toolbarButtons, function (_, button) {
+ toggleClass(button, 'disabled', disable || !button[mode]);
+ });
+ };
+
+ /**
+ * Gets the width of the editor in pixels
+ *
+ * @since 1.3.5
+ * @function
+ * @memberOf SCEditor.prototype
+ * @name width
+ * @return {number}
+ */
+ /**
+ * Sets the width of the editor
+ *
+ * @param {number} width Width in pixels
+ * @since 1.3.5
+ * @function
+ * @memberOf 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 {number} width Width in pixels
+ * @param {boolean} [saveWidth=true] If to store the width
+ * @since 1.4.1
+ * @function
+ * @memberOf SCEditor.prototype
+ * @name width^3
+ * @return {this}
+ */
+ base.width = function (width$$1, saveWidth) {
+ if (!width$$1 && width$$1 !== 0) {
+ return width(editorContainer);
+ }
+
+ base.dimensions(width$$1, 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 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 {number} width Width in px
+ * @param {number} height Height in px
+ * @since 1.4.1
+ * @function
+ * @memberOf 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 {number} width Width in px
+ * @param {number} height Height in px
+ * @param {boolean} [save=true] If to store the new sizes
+ * @since 1.4.1
+ * @function
+ * @memberOf SCEditor.prototype
+ * @name dimensions^3
+ * @return {this}
+ */
+ base.dimensions = function (width$$1, height$$1, save) {
+ // set undefined width/height to boolean false
+ width$$1 = (!width$$1 && width$$1 !== 0) ? false : width$$1;
+ height$$1 = (!height$$1 && height$$1 !== 0) ? false : height$$1;
+
+ if (width$$1 === false && height$$1 === false) {
+ return { width: base.width(), height: base.height() };
+ }
+
+ if (width$$1 !== false) {
+ if (save !== false) {
+ options.width = width$$1;
+ }
+
+ width(editorContainer, width$$1);
+ }
+
+ if (height$$1 !== false) {
+ if (save !== false) {
+ options.height = height$$1;
+ }
+
+ height(editorContainer, height$$1);
+ }
+
+ return base;
+ };
+
+ /**
+ * Gets the height of the editor in px
+ *
+ * @since 1.3.5
+ * @function
+ * @memberOf SCEditor.prototype
+ * @name height
+ * @return {number}
+ */
+ /**
+ * Sets the height of the editor
+ *
+ * @param {number} height Height in px
+ * @since 1.3.5
+ * @function
+ * @memberOf 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 {number} height Height in px
+ * @param {boolean} [saveHeight=true] If to store the height
+ * @since 1.4.1
+ * @function
+ * @memberOf SCEditor.prototype
+ * @name height^3
+ * @return {this}
+ */
+ base.height = function (height$$1, saveHeight) {
+ if (!height$$1 && height$$1 !== 0) {
+ return height(editorContainer);
+ }
+
+ base.dimensions(null, height$$1, saveHeight);
+
+ return base;
+ };
+
+ /**
+ * Gets if the editor is maximised or not
+ *
+ * @since 1.4.1
+ * @function
+ * @memberOf 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 SCEditor.prototype
+ * @name maximize^2
+ * @return {this}
+ */
+ base.maximize = function (maximize) {
+ var maximizeSize = 'sceditor-maximize';
+
+ if (isUndefined(maximize)) {
+ return hasClass(editorContainer, maximizeSize);
+ }
+
+ maximize = !!maximize;
+
+ if (maximize) {
+ maximizeScrollPosition = globalWin.pageYOffset;
+ }
+
+ toggleClass(globalDoc.documentElement, maximizeSize, maximize);
+ toggleClass(globalDoc.body, maximizeSize, maximize);
+ toggleClass(editorContainer, maximizeSize, maximize);
+ base.width(maximize ? '100%' : options.width, false);
+ base.height(maximize ? '100%' : options.height, false);
+
+ if (!maximize) {
+ globalWin.scrollTo(0, maximizeScrollPosition);
+ }
+
+ autoExpand();
+
+ return base;
+ };
+
+ autoExpand = function () {
+ if (options.autoExpand && !autoExpandThrottle) {
+ autoExpandThrottle = setTimeout(base.expandToContent, 200);
+ }
+ };
+
+ /**
+ * Expands or shrinks 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 SCEditor.prototype
+ * @see #resizeToContent
+ */
+ base.expandToContent = function (ignoreMaxHeight) {
+ if (base.maximize()) {
+ return;
+ }
+
+ clearTimeout(autoExpandThrottle);
+ autoExpandThrottle = false;
+
+ if (!autoExpandBounds) {
+ var height$$1 = options.resizeMinHeight || options.height ||
+ height(original);
+
+ autoExpandBounds = {
+ min: height$$1,
+ max: options.resizeMaxHeight || (height$$1 * 2)
+ };
+ }
+
+ var range = globalDoc.createRange();
+ range.selectNodeContents(wysiwygBody);
+
+ var rect = range.getBoundingClientRect();
+ var current = wysiwygDocument.documentElement.clientHeight - 1;
+ var spaceNeeded = rect.bottom - rect.top;
+ var newHeight = base.height() + 1 + (spaceNeeded - current);
+
+ if (!ignoreMaxHeight && autoExpandBounds.max !== -1) {
+ newHeight = Math.min(newHeight, autoExpandBounds.max);
+ }
+
+ base.height(Math.ceil(Math.max(newHeight, autoExpandBounds.min)));
+ };
+
+ /**
+ * Destroys the editor, removing all elements and
+ * event handlers.
+ *
+ * Leaves only the original textarea.
+ *
+ * @function
+ * @name destroy
+ * @memberOf 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) {
+ remove(dropdown);
+ }
+
+ off(globalDoc, 'click', handleDocumentClick);
+
+ // TODO: make off support null nodes?
+ var form = original.form;
+ if (form) {
+ off(form, 'reset', handleFormReset);
+ off(form, 'submit', base.updateOriginal);
+ }
+
+ remove(sourceEditor);
+ remove(toolbar);
+ remove(editorContainer);
+
+ delete original._sceditor;
+ show(original);
+
+ original.required = isRequired;
+ };
+
+
+ /**
+ * 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 {boolean} 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 SCEditor.prototype
+ */
+ base.createDropDown = function (menuItem, name, content, ieFix) {
+ // first click for create second click for close
+ var dropDownCss,
+ dropDownClass = 'sceditor-' + name;
+
+ // Will re-focus the editor. This is needed for IE
+ // as it has special logic to save/restore the selection
+ base.closeDropDown(true);
+
+ // Only close the dropdown if it was already open
+ if (dropdown && hasClass(dropdown, dropDownClass)) {
+ 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) {
+ each(find(content, ':not(input):not(textarea)'),
+ function (_, node) {
+ if (node.nodeType === ELEMENT_NODE) {
+ attr(node, 'unselectable', 'on');
+ }
+ });
+ }
+
+ dropDownCss = extend({
+ top: menuItem.offsetTop,
+ left: menuItem.offsetLeft,
+ marginTop: menuItem.clientHeight
+ }, options.dropDownCss);
+
+ dropdown = createElement('div', {
+ className: 'sceditor-dropdown ' + dropDownClass
+ });
+
+ css(dropdown, dropDownCss);
+ appendChild(dropdown, content);
+ appendChild(editorContainer, dropdown);
+ on(dropdown, '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) {
+ var first = find(dropdown, 'input,textarea')[0];
+ if (first) {
+ first.focus();
+ }
+ }
+ });
+ };
+
+ /**
+ * Handles any document click and closes the dropdown if open
+ * @private
+ */
+ handleDocumentClick = function (e) {
+ // ignore right clicks
+ if (e.which !== 3 && dropdown && !e.defaultPrevented) {
+ autoUpdate();
+
+ base.closeDropDown();
+ }
+ };
+
+ /**
+ * Handles the WYSIWYG editors paste event
+ * @private
+ */
+ handlePasteEvt = function (e) {
+ var isIeOrEdge = IE_VER || edge;
+ var editable = wysiwygBody;
+ var clipboard = e.clipboardData;
+ var loadImage = function (file) {
+ var reader = new FileReader();
+ reader.onload = function (e) {
+ handlePasteData({
+ html: ' '
+ });
+ };
+ reader.readAsDataURL(file);
+ };
+
+ // Modern browsers with clipboard API - everything other than _very_
+ // old android web views and UC browser which doesn't support the
+ // paste event at all.
+ if (clipboard && !isIeOrEdge) {
+ var data$$1 = {};
+ var types = clipboard.types;
+ var items = clipboard.items;
+
+ e.preventDefault();
+
+ for (var i = 0; i < types.length; i++) {
+ // Normalise image pasting to paste as a data-uri
+ if (globalWin.FileReader && items &&
+ IMAGE_MIME_REGEX.test(items[i].type)) {
+ return loadImage(clipboard.items[i].getAsFile());
+ }
+
+ data$$1[types[i]] = clipboard.getData(types[i]);
+ }
+ // Call plugins here with file?
+ data$$1.text = data$$1['text/plain'];
+ data$$1.html = data$$1['text/html'];
+
+ handlePasteData(data$$1);
+ // If contentsFragment exists then we are already waiting for a
+ // previous paste so let the handler for that handle this one too
+ } else if (!pasteContentFragment) {
+ // Save the scroll position so can be restored
+ // when contents is restored
+ var scrollTop = editable.scrollTop;
+
+ rangeHelper.saveRange();
+
+ pasteContentFragment = globalDoc.createDocumentFragment();
+ while (editable.firstChild) {
+ appendChild(pasteContentFragment, editable.firstChild);
+ }
+
+ setTimeout(function () {
+ var html = editable.innerHTML;
+
+ editable.innerHTML = '';
+ appendChild(editable, pasteContentFragment);
+ editable.scrollTop = scrollTop;
+ pasteContentFragment = false;
+
+ rangeHelper.restoreRange();
+
+ handlePasteData({ html: html });
+ }, 0);
+ }
+ };
+
+ /**
+ * Gets the pasted data, filters it and then inserts it.
+ * @param {Object} data
+ * @private
+ */
+ handlePasteData = function (data$$1) {
+ var pasteArea = createElement('div', {}, wysiwygDocument);
+
+ pluginManager.call('pasteRaw', data$$1);
+ trigger(editorContainer, 'pasteraw', data$$1);
+
+ if (data$$1.html) {
+ pasteArea.innerHTML = data$$1.html;
+
+ // fix any invalid nesting
+ fixNesting(pasteArea);
+ } else {
+ pasteArea.innerHTML = entities(data$$1.text || '');
+ }
+
+ var paste = {
+ val: pasteArea.innerHTML
+ };
+
+ if ('fragmentToSource' in format) {
+ paste.val = format
+ .fragmentToSource(paste.val, wysiwygDocument, currentNode);
+ }
+
+ pluginManager.call('paste', paste);
+ trigger(editorContainer, 'paste', paste);
+
+ if ('fragmentToHtml' in format) {
+ paste.val = format
+ .fragmentToHtml(paste.val, currentNode);
+ }
+
+ pluginManager.call('pasteHtml', paste);
+
+ base.wysiwygEditorInsertHtml(paste.val, null, true);
+ };
+
+ /**
+ * Closes any currently open drop down
+ *
+ * @param {boolean} [focus=false] If to focus the editor
+ * after closing the drop down
+ * @function
+ * @name closeDropDown
+ * @memberOf SCEditor.prototype
+ */
+ base.closeDropDown = function (focus) {
+ if (dropdown) {
+ remove(dropdown);
+ dropdown = null;
+ }
+
+ if (focus === true) {
+ base.focus();
+ }
+ };
+
+
+ /**
+ * 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 SCEditor.prototype
+ */
+ base.wysiwygEditorInsertHtml = function (
+ html, endHtml, overrideCodeBlocking
+ ) {
+ var marker, scrollTop, scrollTo,
+ editorHeight = height(wysiwygEditor);
+
+ 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 && closest(currentBlockNode, 'code')) {
+ 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();
+
+ // Scroll the editor after the end of the selection
+ marker = find(wysiwygBody, '#sceditor-end-marker')[0];
+ show(marker);
+ scrollTop = wysiwygBody.scrollTop;
+ scrollTo = (getOffset(marker).top +
+ (marker.offsetHeight * 1.5)) - editorHeight;
+ hide(marker);
+
+ // Only scroll if marker isn't already visible
+ if (scrollTo > scrollTop || scrollTo + editorHeight < scrollTop) {
+ wysiwygBody.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 SCEditor.prototype
+ */
+ base.wysiwygEditorInsertText = function (text, endText) {
+ base.wysiwygEditorInsertHtml(
+ entities(text), 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 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 SCEditor.prototype
+ */
+ base.sourceEditorInsertText = function (text, endText) {
+ var scrollTop, currentValue,
+ startPos = sourceEditor.selectionStart,
+ endPos = sourceEditor.selectionEnd;
+
+ scrollTop = sourceEditor.scrollTop;
+ sourceEditor.focus();
+ 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;
+
+ sourceEditor.scrollTop = scrollTop;
+ sourceEditor.focus();
+
+ triggerValueChanged();
+ };
+
+ /**
+ * Gets the current instance of the rangeHelper class
+ * for the editor.
+ *
+ * @return {RangeHelper}
+ * @function
+ * @name getRangeHelper
+ * @memberOf SCEditor.prototype
+ */
+ base.getRangeHelper = function () {
+ return rangeHelper;
+ };
+
+ /**
+ * Gets or sets the source editor caret position.
+ *
+ * @param {Object} [position]
+ * @return {this}
+ * @function
+ * @since 1.4.5
+ * @name sourceEditorCaret
+ * @memberOf SCEditor.prototype
+ */
+ base.sourceEditorCaret = function (position) {
+ sourceEditor.focus();
+
+ if (position) {
+ sourceEditor.selectionStart = position.start;
+ sourceEditor.selectionEnd = position.end;
+
+ return this;
+ }
+
+ return {
+ start: sourceEditor.selectionStart,
+ end: sourceEditor.selectionEnd
+ };
+ };
+
+ /**
+ * 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 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 SCEditor.prototype
+ */
+ base.val = function (val, filter) {
+ if (!isString(val)) {
+ return base.inSourceMode() ?
+ base.getSourceEditorValue(false) :
+ base.getWysiwygEditorValue(filter);
+ }
+
+ if (!base.inSourceMode()) {
+ if (filter !== false && 'toHtml' in format) {
+ val = format.toHtml(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 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 SCEditor.prototype
+ */
+ // eslint-disable-next-line max-params
+ base.insert = function (
+ 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();
+
+ if (filter !== false && 'fragmentToSource' in format) {
+ html = format
+ .fragmentToSource(html, wysiwygDocument, currentNode);
+ }
+
+ start += html + end;
+ }
+ // TODO: This filter should allow empty tags as it's inserting.
+ if (filter !== false && 'fragmentToHtml' in format) {
+ start = format.fragmentToHtml(start, currentNode);
+ }
+
+ // 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 {boolean} [filter=true]
+ * @return {string}
+ * @function
+ * @name getWysiwygEditorValue
+ * @memberOf 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 = createElement('div', {}, wysiwygDocument);
+ var childNodes = wysiwygBody.childNodes;
+
+ for (var i = 0; i < childNodes.length; i++) {
+ appendChild(tmp, childNodes[i].cloneNode(true));
+ }
+
+ appendChild(wysiwygBody, tmp);
+ fixNesting(tmp);
+ remove(tmp);
+
+ html = tmp.innerHTML;
+
+ // filter the HTML and DOM through any plugins
+ if (filter !== false && format.hasOwnProperty('toSource')) {
+ html = format.toSource(html, wysiwygDocument);
+ }
+
+ return html;
+ };
+
+ /**
+ * Gets the WYSIWYG editor's iFrame Body.
+ *
+ * @return {HTMLElement}
+ * @function
+ * @since 1.4.3
+ * @name getBody
+ * @memberOf SCEditor.prototype
+ */
+ base.getBody = function () {
+ return wysiwygBody;
+ };
+
+ /**
+ * Gets the WYSIWYG editors container area (whole iFrame).
+ *
+ * @return {HTMLElement}
+ * @function
+ * @since 1.4.3
+ * @name getContentAreaContainer
+ * @memberOf 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 {boolean} [filter=true]
+ * @return {string}
+ * @function
+ * @since 1.4.0
+ * @name getSourceEditorValue
+ * @memberOf SCEditor.prototype
+ */
+ base.getSourceEditorValue = function (filter) {
+ var val = sourceEditor.value;
+
+ if (filter !== false && 'toHtml' in format) {
+ val = format.toHtml(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 SCEditor.prototype
+ */
+ base.setWysiwygEditorValue = function (value) {
+ if (!value) {
+ value = '' + (IE_VER ? '' : ' ') + '
';
+ }
+
+ wysiwygBody.innerHTML = value;
+ replaceEmoticons();
+
+ appendNewLine();
+ triggerValueChanged();
+ autoExpand();
+ };
+
+ /**
+ * Sets the text editor value
+ *
+ * @param {string} value
+ * @function
+ * @name setSourceEditorValue
+ * @memberOf SCEditor.prototype
+ */
+ base.setSourceEditorValue = function (value) {
+ sourceEditor.value = 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 SCEditor.prototype
+ */
+ base.updateOriginal = function () {
+ original.value = base.val();
+ };
+
+ /**
+ * Replaces any emoticon codes in the passed HTML
+ * with their emoticon images
+ * @private
+ */
+ replaceEmoticons = function () {
+ if (options.emoticonsEnabled) {
+ replace(wysiwygBody, allEmoticons, options.emoticonsCompat);
+ }
+ };
+
+ /**
+ * If the editor is in source code mode
+ *
+ * @return {boolean}
+ * @function
+ * @name inSourceMode
+ * @memberOf SCEditor.prototype
+ */
+ base.inSourceMode = function () {
+ return hasClass(editorContainer, 'sourceMode');
+ };
+
+ /**
+ * Gets if the editor is in sourceMode
+ *
+ * @return boolean
+ * @function
+ * @name sourceMode
+ * @memberOf SCEditor.prototype
+ */
+ /**
+ * Sets if the editor is in sourceMode
+ *
+ * @param {boolean} enable
+ * @return {this}
+ * @function
+ * @name sourceMode^2
+ * @memberOf 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 SCEditor.prototype
+ */
+ base.toggleSourceMode = function () {
+ var isInSourceMode = base.inSourceMode();
+
+ // don't allow switching to WYSIWYG if doesn't support it
+ if (!isWysiwygSupported && isInSourceMode) {
+ return;
+ }
+
+ if (!isInSourceMode) {
+ rangeHelper.saveRange();
+ rangeHelper.clear();
+ }
+
+ base.blur();
+
+ if (isInSourceMode) {
+ base.setWysiwygEditorValue(base.getSourceEditorValue());
+ } else {
+ base.setSourceEditorValue(base.getWysiwygEditorValue());
+ }
+
+ lastRange = null;
+ toggle(sourceEditor);
+ toggle(wysiwygEditor);
+
+ toggleClass(editorContainer, 'wysiwygMode', isInSourceMode);
+ toggleClass(editorContainer, 'sourceMode', !isInSourceMode);
+
+ updateToolBar();
+ updateActiveButtons();
+ };
+
+ /**
+ * Gets the selected text of the source editor
+ * @return {string}
+ * @private
+ */
+ sourceEditorSelectedText = function () {
+ sourceEditor.focus();
+
+ return sourceEditor.value.substring(
+ sourceEditor.selectionStart,
+ sourceEditor.selectionEnd
+ );
+ };
+
+ /**
+ * 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 (Array.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 SCEditor.prototype
+ */
+ base.execCommand = function (command, param) {
+ var executed = false,
+ commandObj = base.commands[command];
+
+ base.focus();
+
+ // TODO: make configurable
+ // don't apply any commands to code elements
+ if (closest(rangeHelper.parentNode(), 'code')) {
+ return;
+ }
+
+ try {
+ executed = wysiwygDocument.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() {
+ // Don't create new selection if there isn't one (like after
+ // blur event in iOS)
+ if (wysiwygWindow.getSelection() &&
+ wysiwygWindow.getSelection().rangeCount <= 0) {
+ currentSelection = null;
+ // rangeHelper could be null if editor was destroyed
+ // before the timeout had finished
+ } else if (rangeHelper && !rangeHelper.compare(currentSelection)) {
+ currentSelection = rangeHelper.cloneSelected();
+
+ // If the selection is in an inline wrap it in a block.
+ // Fixes #331
+ if (currentSelection && currentSelection.collapsed) {
+ var parent$$1 = currentSelection.startContainer;
+ var offset = currentSelection.startOffset;
+
+ // Handle if selection is placed before/after an element
+ if (offset && parent$$1.nodeType !== TEXT_NODE) {
+ parent$$1 = parent$$1.childNodes[offset];
+ }
+
+ while (parent$$1 && parent$$1.parentNode !== wysiwygBody) {
+ parent$$1 = parent$$1.parentNode;
+ }
+
+ if (parent$$1 && isInline(parent$$1, true)) {
+ rangeHelper.saveRange();
+ wrapInlines(wysiwygBody, wysiwygDocument);
+ rangeHelper.restoreRange();
+ }
+ }
+
+ trigger(editorContainer, 'selectionchanged');
+ }
+
+ isSelectionCheckPending = false;
+ }
+
+ if (isSelectionCheckPending) {
+ return;
+ }
+
+ isSelectionCheckPending = true;
+
+ // Don't need to limit checking if browser supports the Selection API
+ if ('onselectionchange' in wysiwygDocument) {
+ 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);
+
+ trigger(editorContainer, '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 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 SCEditor.prototype
+ * @since 1.4.4
+ */
+ base.currentBlockNode = function () {
+ return currentBlockNode;
+ };
+
+ /**
+ * Updates if buttons are active or not
+ * @private
+ */
+ updateActiveButtons = function () {
+ var firstBlock, parent$$1;
+ var activeClass = 'active';
+ var doc = wysiwygDocument;
+ var isSource = base.sourceMode();
+
+ if (base.readOnly()) {
+ each(find(toolbar, activeClass), function (_, menuItem) {
+ removeClass(menuItem, activeClass);
+ });
+ return;
+ }
+
+ if (!isSource) {
+ parent$$1 = rangeHelper.parentNode();
+ firstBlock = rangeHelper.getFirstBlockParent(parent$$1);
+ }
+
+ for (var j = 0; j < btnStateHandlers.length; j++) {
+ var state = 0;
+ var btn = toolbarButtons[btnStateHandlers[j].name];
+ var stateFn = btnStateHandlers[j].state;
+ var isDisabled = (isSource && !btn._sceTxtMode) ||
+ (!isSource && !btn._sceWysiwygMode);
+
+ if (isString(stateFn)) {
+ if (!isSource) {
+ try {
+ state = doc.queryCommandEnabled(stateFn) ? 0 : -1;
+
+ // eslint-disable-next-line max-depth
+ if (state > -1) {
+ state = doc.queryCommandState(stateFn) ? 1 : 0;
+ }
+ } catch (ex) {}
+ }
+ } else if (!isDisabled) {
+ state = stateFn.call(base, parent$$1, firstBlock);
+ }
+
+ toggleClass(btn, 'disabled', isDisabled || state < 0);
+ toggleClass(btn, activeClass, state > 0);
+ }
+
+ if (icons && icons.update) {
+ icons.update(isSource, parent$$1, firstBlock);
+ }
+ };
+
+ /**
+ * Handles any key press in the WYSIWYG editor
+ *
+ * @private
+ */
+ handleKeyPress = function (e) {
+ // FF bug: https://bugzilla.mozilla.org/show_bug.cgi?id=501496
+ if (e.defaultPrevented) {
+ return;
+ }
+
+ base.closeDropDown();
+
+ // 13 = enter key
+ if (e.which === 13) {
+ var LIST_TAGS = 'li,ul,ol';
+
+ // "Fix" (cludge) for blocklevel elements being duplicated in some
+ // browsers when enter is pressed instead of inserting a newline
+ if (!is(currentBlockNode, LIST_TAGS) &&
+ hasStyling(currentBlockNode)) {
+ lastRange = null;
+
+ var br = createElement('br', {}, wysiwygDocument);
+ 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$2) {
+ var parent$$1 = br.parentNode;
+ var lastChild = parent$$1.lastChild;
+
+ // Sometimes an empty next node is created after the
+ if (lastChild && lastChild.nodeType === TEXT_NODE &&
+ lastChild.nodeValue === '') {
+ remove(lastChild);
+ lastChild = parent$$1.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 (!isInline(parent$$1, true) && lastChild === br &&
+ isInline(br.previousSibling)) {
+ rangeHelper.insertHTML(' ');
+ }
+ }
+
+ e.preventDefault();
+ }
+ }
+ };
+
+ /**
+ * 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 () {
+ // Check all nodes in reverse until either add a new line
+ // or reach a non-empty textnode or BR at which point can
+ // stop checking.
+ rTraverse(wysiwygBody, function (node) {
+ // Last block, add new line after if has styling
+ if (node.nodeType === ELEMENT_NODE &&
+ !/inline/.test(css(node, 'display'))) {
+
+ // Add line break after if has styling
+ if (!is(node, '.sceditor-nlf') && hasStyling(node)) {
+ var paragraph = createElement('p', {}, wysiwygDocument);
+ paragraph.className = 'sceditor-nlf';
+ paragraph.innerHTML = !IE_BR_FIX$2 ? ' ' : '';
+ appendChild(wysiwygBody, paragraph);
+ return false;
+ }
+ }
+
+ // Last non-empty text node or line break.
+ // No need to add line-break after them
+ if ((node.nodeType === 3 && !/^\s*$/.test(node.nodeValue)) ||
+ is(node, 'br')) {
+ return false;
+ }
+ });
+ };
+
+ /**
+ * Handles form reset event
+ * @private
+ */
+ handleFormReset = function () {
+ base.val(original.value);
+ };
+
+ /**
+ * Handles any mousedown press in the WYSIWYG editor
+ * @private
+ */
+ handleMouseDown = function () {
+ base.closeDropDown();
+ lastRange = null;
+ };
+
+ /**
+ * 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 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) {
+ if (pluginManager) {
+ // Send event to all plugins
+ pluginManager.call(e.type + 'Event', e, base);
+ }
+
+ // convert the event into a custom event to send
+ var name = (e.target === sourceEditor ? 'scesrc' : 'scewys') + e.type;
+
+ if (eventHandlers[name]) {
+ eventHandlers[name].forEach(function (fn) {
+ fn.call(base, e);
+ });
+ }
+ };
+
+ /**
+ * 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 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)) {
+ var wysEvent = 'scewys' + events[i];
+ var srcEvent = 'scesrc' + events[i];
+ // Use custom events to allow passing the instance as the
+ // 2nd argument.
+ // Also allows unbinding without unbinding the editors own
+ // event handlers.
+ if (!excludeWysiwyg) {
+ eventHandlers[wysEvent] = eventHandlers[wysEvent] || [];
+ eventHandlers[wysEvent].push(handler);
+ }
+
+ if (!excludeSource) {
+ eventHandlers[srcEvent] = eventHandlers[srcEvent] || [];
+ eventHandlers[srcEvent].push(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 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) {
+ arrayRemove(
+ eventHandlers['scewys' + events[i]] || [], handler);
+ }
+
+ if (!excludeSource) {
+ arrayRemove(
+ eventHandlers['scesrc' + events[i]] || [], handler);
+ }
+ }
+ }
+
+ return base;
+ };
+
+ /**
+ * Blurs the editors input area
+ *
+ * @return {this}
+ * @function
+ * @name blur
+ * @memberOf 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 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 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 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()) {
+ // Already has focus so do nothing
+ if (find(wysiwygDocument, ':focus').length) {
+ return;
+ }
+
+ var container;
+ var 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) {
+ 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$2 && rng && rng.endOffset === 1 && rng.collapsed) {
+ container = rng.endContainer;
+
+ if (container && container.childNodes.length === 1 &&
+ is(container.firstChild, 'br')) {
+ rng.setStartBefore(container.firstChild);
+ rng.collapse(true);
+ rangeHelper.selectRange(rng);
+ }
+ }
+
+ wysiwygWindow.focus();
+ wysiwygBody.focus();
+
+ // Needed for IE
+ 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 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 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 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 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 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 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 (closest(currentBlockNode, 'code')) {
+ return;
+ }
+
+ if (!emoticonsCache) {
+ emoticonsCache = [];
+
+ each(allEmoticons, function (key, html) {
+ emoticonsCache[cachePos++] = [key, html];
+ });
+
+ 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) {
+ if (!options.emoticonsCompat || !/^\s$/.test(curChar)) {
+ e.preventDefault();
+ }
+ }
+ };
+
+ /**
+ * Makes sure emoticons are surrounded by whitespace
+ * @private
+ */
+ emoticonsCheckWhitespace = function () {
+ checkWhitespace(currentBlockNode, rangeHelper);
+ };
+
+ /**
+ * Gets if emoticons are currently enabled
+ * @return {boolean}
+ * @function
+ * @name emoticons
+ * @memberOf SCEditor.prototype
+ * @since 1.4.2
+ */
+ /**
+ * Enables/disables emoticons
+ *
+ * @param {boolean} enable
+ * @return {this}
+ * @function
+ * @name emoticons^2
+ * @memberOf SCEditor.prototype
+ * @since 1.4.2
+ */
+ base.emoticons = function (enable) {
+ if (!enable && enable !== false) {
+ return options.emoticonsEnabled;
+ }
+
+ options.emoticonsEnabled = enable;
+
+ if (enable) {
+ on(wysiwygBody, 'keypress', emoticonsKeyPress);
+
+ if (!base.sourceMode()) {
+ rangeHelper.saveRange();
+
+ replaceEmoticons();
+ triggerValueChanged(false);
+
+ rangeHelper.restoreRange();
+ }
+ } else {
+ var emoticons =
+ find(wysiwygBody, 'img[data-sceditor-emoticon]');
+
+ each(emoticons, function (_, img) {
+ var text = data(img, 'sceditor-emoticon');
+ var textNode = wysiwygDocument.createTextNode(text);
+ img.parentNode.replaceChild(textNode, img);
+ });
+
+ off(wysiwygBody, 'keypress', emoticonsKeyPress);
+
+ triggerValueChanged();
+ }
+
+ return base;
+ };
+
+ /**
+ * Gets the current WYSIWYG editors inline CSS
+ *
+ * @return {string}
+ * @function
+ * @name css
+ * @memberOf SCEditor.prototype
+ * @since 1.4.3
+ */
+ /**
+ * Sets inline CSS for the WYSIWYG editor
+ *
+ * @param {string} css
+ * @return {this}
+ * @function
+ * @name css^2
+ * @memberOf SCEditor.prototype
+ * @since 1.4.3
+ */
+ base.css = function (css$$1) {
+ if (!inlineCss) {
+ inlineCss = createElement('style', {
+ id: 'inline'
+ }, wysiwygDocument);
+
+ appendChild(wysiwygDocument.head, inlineCss);
+ }
+
+ if (!isString(css$$1)) {
+ return inlineCss.styleSheet ?
+ inlineCss.styleSheet.cssText : inlineCss.innerHTML;
+ }
+
+ if (inlineCss.styleSheet) {
+ inlineCss.styleSheet.cssText = css$$1;
+ } else {
+ inlineCss.innerHTML = css$$1;
+ }
+
+ return base;
+ };
+
+ /**
+ * Handles the keydown event, used for shortcuts
+ * @private
+ */
+ handleKeyDown = function (e) {
+ var shortcut = [],
+ SHIFT_KEYS = {
+ '`': '~',
+ '1': '!',
+ '2': '@',
+ '3': '#',
+ '4': '$',
+ '5': '%',
+ '6': '^',
+ '7': '&',
+ '8': '*',
+ '9': '(',
+ '0': ')',
+ '-': '_',
+ '=': '+',
+ ';': ': ',
+ '\'': '"',
+ ',': '<',
+ '.': '>',
+ '/': '?',
+ '\\': '|',
+ '[': '{',
+ ']': '}'
+ },
+ SPECIAL_KEYS = {
+ 8: 'backspace',
+ 9: 'tab',
+ 13: 'enter',
+ 19: 'pause',
+ 20: 'capslock',
+ 27: 'esc',
+ 32: 'space',
+ 33: 'pageup',
+ 34: 'pagedown',
+ 35: 'end',
+ 36: 'home',
+ 37: 'left',
+ 38: 'up',
+ 39: 'right',
+ 40: 'down',
+ 45: 'insert',
+ 46: 'del',
+ 91: 'win',
+ 92: 'win',
+ 93: 'select',
+ 96: '0',
+ 97: '1',
+ 98: '2',
+ 99: '3',
+ 100: '4',
+ 101: '5',
+ 102: '6',
+ 103: '7',
+ 104: '8',
+ 105: '9',
+ 106: '*',
+ 107: '+',
+ 109: '-',
+ 110: '.',
+ 111: '/',
+ 112: 'f1',
+ 113: 'f2',
+ 114: 'f3',
+ 115: 'f4',
+ 116: 'f5',
+ 117: 'f6',
+ 118: 'f7',
+ 119: 'f8',
+ 120: 'f9',
+ 121: 'f10',
+ 122: 'f11',
+ 123: 'f12',
+ 144: 'numlock',
+ 145: 'scrolllock',
+ 186: ';',
+ 187: '=',
+ 188: ',',
+ 189: '-',
+ 190: '.',
+ 191: '/',
+ 192: '`',
+ 219: '[',
+ 220: '\\',
+ 221: ']',
+ 222: '\''
+ },
+ NUMPAD_SHIFT_KEYS = {
+ 109: '-',
+ 110: 'del',
+ 111: '/',
+ 96: '0',
+ 97: '1',
+ 98: '2',
+ 99: '3',
+ 100: '4',
+ 101: '5',
+ 102: '6',
+ 103: '7',
+ 104: '8',
+ 105: '9'
+ },
+ which = e.which,
+ character = SPECIAL_KEYS[which] ||
+ String.fromCharCode(which).toLowerCase();
+
+ if (e.ctrlKey || e.metaKey) {
+ shortcut.push('ctrl');
+ }
+
+ if (e.altKey) {
+ shortcut.push('alt');
+ }
+
+ if (e.shiftKey) {
+ shortcut.push('shift');
+
+ if (NUMPAD_SHIFT_KEYS[which]) {
+ character = NUMPAD_SHIFT_KEYS[which];
+ } else if (SHIFT_KEYS[character]) {
+ character = SHIFT_KEYS[character];
+ }
+ }
+
+ // Shift is 16, ctrl is 17 and alt is 18
+ if (character && (which < 16 || which > 18)) {
+ shortcut.push(character);
+ }
+
+ shortcut = shortcut.join('+');
+ if (shortcutHandlers[shortcut] &&
+ shortcutHandlers[shortcut].call(base) === false) {
+
+ e.stopPropagation();
+ e.preventDefault();
+ }
+ };
+
+ /**
+ * Adds a shortcut handler to the editor
+ * @param {string} shortcut
+ * @param {String|Function} cmd
+ * @return {sceditor}
+ */
+ base.addShortcut = function (shortcut, cmd) {
+ shortcut = shortcut.toLowerCase();
+
+ if (isString(cmd)) {
+ shortcutHandlers[shortcut] = function () {
+ handleCommand(toolbarButtons[cmd], base.commands[cmd]);
+
+ return false;
+ };
+ } else {
+ shortcutHandlers[shortcut] = cmd;
+ }
+
+ return base;
+ };
+
+ /**
+ * Removes a shortcut handler
+ * @param {string} shortcut
+ * @return {sceditor}
+ */
+ base.removeShortcut = function (shortcut) {
+ delete shortcutHandlers[shortcut.toLowerCase()];
+
+ return base;
+ };
+
+ /**
+ * Handles the backspace key press
+ *
+ * Will remove block styling like quotes/code ect if at the start.
+ * @private
+ */
+ handleBackSpace = function (e) {
+ var node, offset, range, parent$$1;
+
+ // 8 is the backspace key
+ if (options.disableBlockRemove || e.which !== 8 ||
+ !(range = rangeHelper.selectedRange())) {
+ return;
+ }
+
+ node = range.startContainer;
+ offset = range.startOffset;
+
+ if (offset !== 0 || !(parent$$1 = currentStyledBlockNode()) ||
+ is(parent$$1, 'body')) {
+ return;
+ }
+
+ while (node !== parent$$1) {
+ while (node.previousSibling) {
+ node = node.previousSibling;
+
+ // Everything but empty text nodes before the cursor
+ // should prevent the style from being removed
+ if (node.nodeType !== TEXT_NODE || node.nodeValue) {
+ return;
+ }
+ }
+
+ if (!(node = node.parentNode)) {
+ return;
+ }
+ }
+
+ // The backspace was pressed at the start of
+ // the container so clear the style
+ base.clearBlockFormatting(parent$$1);
+ e.preventDefault();
+ };
+
+ /**
+ * Gets the first styled block node that contains the cursor
+ * @return {HTMLElement}
+ */
+ currentStyledBlockNode = function () {
+ var block = currentBlockNode;
+
+ while (!hasStyling(block) || isInline(block, true)) {
+ if (!(block = block.parentNode) || is(block, 'body')) {
+ return;
+ }
+ }
+
+ return block;
+ };
+
+ /**
+ * Clears the formatting of the passed block element.
+ *
+ * If block is false, if will clear the styling of the first
+ * block level element that contains the cursor.
+ * @param {HTMLElement} block
+ * @since 1.4.4
+ */
+ base.clearBlockFormatting = function (block) {
+ block = block || currentStyledBlockNode();
+
+ if (!block || is(block, 'body')) {
+ return base;
+ }
+
+ rangeHelper.saveRange();
+
+ block.className = '';
+ lastRange = null;
+
+ attr(block, 'style', '');
+
+ if (!is(block, 'p,div,td')) {
+ convertElement(block, 'p');
+ }
+
+ rangeHelper.restoreRange();
+ return base;
+ };
+
+ /**
+ * Triggers the valueChanged signal if there is
+ * a plugin that handles it.
+ *
+ * If rangeHelper.saveRange() has already been
+ * called, then saveRange should be set to false
+ * to prevent the range being saved twice.
+ *
+ * @since 1.4.5
+ * @param {boolean} saveRange If to call rangeHelper.saveRange().
+ * @private
+ */
+ triggerValueChanged = function (saveRange) {
+ if (!pluginManager ||
+ (!pluginManager.hasHandler('valuechangedEvent') &&
+ !triggerValueChanged.hasHandler)) {
+ return;
+ }
+
+ var currentHtml,
+ sourceMode = base.sourceMode(),
+ hasSelection = !sourceMode && rangeHelper.hasSelection();
+
+ // Composition end isn't guaranteed to fire but must have
+ // ended when triggerValueChanged() is called so reset it
+ isComposing = false;
+
+ // Don't need to save the range if sceditor-start-marker
+ // is present as the range is already saved
+ saveRange = saveRange !== false &&
+ !wysiwygDocument.getElementById('sceditor-start-marker');
+
+ // Clear any current timeout as it's now been triggered
+ if (valueChangedKeyUpTimer) {
+ clearTimeout(valueChangedKeyUpTimer);
+ valueChangedKeyUpTimer = false;
+ }
+
+ if (hasSelection && saveRange) {
+ rangeHelper.saveRange();
+ }
+
+ currentHtml = sourceMode ? sourceEditor.value : wysiwygBody.innerHTML;
+
+ // Only trigger if something has actually changed.
+ if (currentHtml !== triggerValueChanged.lastVal) {
+ triggerValueChanged.lastVal = currentHtml;
+
+ trigger(editorContainer, 'valuechanged', {
+ rawValue: sourceMode ? base.val() : currentHtml
+ });
+ }
+
+ if (hasSelection && saveRange) {
+ rangeHelper.removeMarkers();
+ }
+ };
+
+ /**
+ * Should be called whenever there is a blur event
+ * @private
+ */
+ valueChangedBlur = function () {
+ if (valueChangedKeyUpTimer) {
+ triggerValueChanged();
+ }
+ };
+
+ /**
+ * Should be called whenever there is a keypress event
+ * @param {Event} e The keypress event
+ * @private
+ */
+ valueChangedKeyUp = function (e) {
+ var which = e.which,
+ lastChar = valueChangedKeyUp.lastChar,
+ lastWasSpace = (lastChar === 13 || lastChar === 32),
+ lastWasDelete = (lastChar === 8 || lastChar === 46);
+
+ valueChangedKeyUp.lastChar = which;
+
+ if (isComposing) {
+ return;
+ }
+
+ // 13 = return & 32 = space
+ if (which === 13 || which === 32) {
+ if (!lastWasSpace) {
+ triggerValueChanged();
+ } else {
+ valueChangedKeyUp.triggerNext = true;
+ }
+ // 8 = backspace & 46 = del
+ } else if (which === 8 || which === 46) {
+ if (!lastWasDelete) {
+ triggerValueChanged();
+ } else {
+ valueChangedKeyUp.triggerNext = true;
+ }
+ } else if (valueChangedKeyUp.triggerNext) {
+ triggerValueChanged();
+ valueChangedKeyUp.triggerNext = false;
+ }
+
+ // Clear the previous timeout and set a new one.
+ clearTimeout(valueChangedKeyUpTimer);
+
+ // Trigger the event 1.5s after the last keypress if space
+ // isn't pressed. This might need to be lowered, will need
+ // to look into what the slowest average Chars Per Min is.
+ valueChangedKeyUpTimer = setTimeout(function () {
+ if (!isComposing) {
+ triggerValueChanged();
+ }
+ }, 1500);
+ };
+
+ handleComposition = function (e) {
+ isComposing = /start/i.test(e.type);
+
+ if (!isComposing) {
+ triggerValueChanged();
+ }
+ };
+
+ autoUpdate = function () {
+ base.updateOriginal();
+ };
+
+ // run the initializer
+ init();
+ }
+
+
+ /**
+ * Map containing the loaded SCEditor locales
+ * @type {Object}
+ * @name locale
+ * @memberOf sceditor
+ */
+ SCEditor.locale = {};
+
+ SCEditor.formats = {};
+ SCEditor.icons = {};
+
+
+ /**
+ * Static command helper class
+ * @class command
+ * @name sceditor.command
+ */
+ SCEditor.command =
+ /** @lends sceditor.command */
+ {
+ /**
+ * Gets a command
+ *
+ * @param {string} name
+ * @return {Object|null}
+ * @since v1.3.5
+ */
+ get: function (name) {
+ return defaultCmds[name] || null;
+ },
+
+ /**
+ * Adds a command to the editor or updates an existing
+ * command if a command with the specified name already exists.
+ *
+ * Once a command is add it can be included in the toolbar by
+ * adding it's name to the toolbar option in the constructor. It
+ * can also be executed manually by calling
+ * {@link sceditor.execCommand}
+ *
+ * @example
+ * SCEditor.command.set("hello",
+ * {
+ * exec: function () {
+ * alert("Hello World!");
+ * }
+ * });
+ *
+ * @param {string} name
+ * @param {Object} cmd
+ * @return {this|false} Returns false if name or cmd is false
+ * @since v1.3.5
+ */
+ set: function (name, cmd) {
+ if (!name || !cmd) {
+ return false;
+ }
+
+ // merge any existing command properties
+ cmd = extend(defaultCmds[name] || {}, cmd);
+
+ cmd.remove = function () {
+ SCEditor.command.remove(name);
+ };
+
+ defaultCmds[name] = cmd;
+ return this;
+ },
+
+ /**
+ * Removes a command
+ *
+ * @param {string} name
+ * @return {this}
+ * @since v1.3.5
+ */
+ remove: function (name) {
+ if (defaultCmds[name]) {
+ delete defaultCmds[name];
+ }
+
+ return this;
+ }
+ };
+
+ /**
+ * SCEditor
+ * http://www.sceditor.com/
+ *
+ * Copyright (C) 2017, 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
+ */
+
+ window.sceditor = {
+ command: SCEditor.command,
+ commands: defaultCmds,
+ defaultOptions: defaultOptions,
+
+ ie: ie,
+ ios: ios,
+ isWysiwygSupported: isWysiwygSupported,
+
+ regexEscape: regex,
+ escapeEntities: entities,
+ escapeUriScheme: uriScheme,
+
+ dom: {
+ css: css,
+ attr: attr,
+ removeAttr: removeAttr,
+ is: is,
+ closest: closest,
+ width: width,
+ height: height,
+ traverse: traverse,
+ rTraverse: rTraverse,
+ parseHTML: parseHTML,
+ hasStyling: hasStyling,
+ convertElement: convertElement,
+ blockLevelList: blockLevelList,
+ canHaveChildren: canHaveChildren,
+ isInline: isInline,
+ copyCSS: copyCSS,
+ fixNesting: fixNesting,
+ findCommonAncestor: findCommonAncestor,
+ getSibling: getSibling,
+ removeWhiteSpace: removeWhiteSpace,
+ extractContents: extractContents,
+ getOffset: getOffset,
+ getStyle: getStyle,
+ hasStyle: hasStyle
+ },
+ locale: SCEditor.locale,
+ icons: SCEditor.icons,
+ utils: {
+ each: each,
+ isEmptyObject: isEmptyObject,
+ extend: extend
+ },
+ plugins: PluginManager.plugins,
+ formats: SCEditor.formats,
+ create: function (textarea, options) {
+ options = options || {};
+
+ // Don't allow the editor to be initialised
+ // on it's own source editor
+ if (parent(textarea, '.sceditor-container')) {
+ return;
+ }
+
+ if (options.runWithoutWysiwygSupport || isWysiwygSupported) {
+ /*eslint no-new: off*/
+ (new SCEditor(textarea, options));
+ }
+ },
+ instance: function (textarea) {
+ return textarea._sceditor;
+ }
+ };
+
+ /**
+ * SCEditor
+ * http://www.sceditor.com/
+ *
+ * Copyright (C) 2017, 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
+ */
+
+ // For backwards compatibility
+ $.sceditor = window.sceditor;
+
+ /**
+ * 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|Array|SCEditor|boolean}
+ */
+ $.fn.sceditor = function (options) {
+ var instance;
+ var ret = [];
+
+ this.each(function () {
+ instance = this._sceditor;
+
+ // 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) {
+ $.sceditor.create(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;
+ };
+
+}(jQuery));
+;/**
+ * SCEditor XHTML Plugin
+ * http://www.sceditor.com/
+ *
+ * Copyright (C) 2017, Sam Clarke (samclarke.com)
+ *
+ * SCEditor is licensed under the MIT license:
+ * http://www.opensource.org/licenses/mit-license.php
+ *
+ * @author Sam Clarke
+ */
+(function (sceditor) {
+ 'use strict';
+
+ 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 dom = sceditor.dom;
+ var utils = sceditor.utils;
+
+ var css = dom.css;
+ var attr = dom.attr;
+ var is = dom.is;
+ var removeAttr = dom.removeAttr;
+ var convertElement = dom.convertElement;
+ var extend = utils.extend;
+ var each = utils.each;
+ var isEmptyObject = utils.isEmptyObject;
+
+ var getEditorCommand = sceditor.command.get;
+
+ 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;
+
+ getEditorCommand('font')._dropDown(
+ editor,
+ caller,
+ function (font) {
+ editor.insertText('', ' ');
+ }
+ );
+ }
+ },
+ size: {
+ txtExec: function (caller) {
+ var editor = this;
+
+ getEditorCommand('size')._dropDown(
+ editor,
+ caller,
+ function (size) {
+ editor.insertText('', ' ');
+ }
+ );
+ }
+ },
+ color: {
+ txtExec: function (caller) {
+ var editor = this;
+
+ getEditorCommand('color')._dropDown(
+ editor,
+ caller,
+ function (color) {
+ editor.insertText('', ' ');
+ }
+ );
+ }
+ },
+ bulletlist: {
+ txtExec: ['']
+ },
+ orderedlist: {
+ txtExec: ['', ' ']
+ },
+ table: {
+ txtExec: ['']
+ },
+ horizontalrule: {
+ txtExec: [' ']
+ },
+ code: {
+ txtExec: ['', '
']
+ },
+ image: {
+ txtExec: function (caller, selected) {
+ var editor = this;
+
+ getEditorCommand('image')._dropDown(
+ editor,
+ caller,
+ selected,
+ function (url, width, height) {
+ var attrs = '';
+
+ if (width) {
+ attrs += ' width="' + width + '"';
+ }
+
+ if (height) {
+ attrs += ' height="' + height + '"';
+ }
+
+ editor.insertText(
+ ' '
+ );
+ }
+ );
+ }
+ },
+ email: {
+ txtExec: function (caller, selected) {
+ var editor = this;
+
+ getEditorCommand('email')._dropDown(
+ editor,
+ caller,
+ function (url, text) {
+ editor.insertText(
+ '' +
+ (text || selected || url) +
+ ' '
+ );
+ }
+ );
+ }
+ },
+ link: {
+ txtExec: function (caller, selected) {
+ var editor = this;
+
+ getEditorCommand('link')._dropDown(
+ editor,
+ caller,
+ function (url, text) {
+ editor.insertText(
+ '' +
+ (text || selected || url) +
+ ' '
+ );
+ }
+ );
+ }
+ },
+ quote: {
+ txtExec: ['', ' ']
+ },
+ youtube: {
+ txtExec: function (caller) {
+ var editor = this;
+
+ getEditorCommand('youtube')._dropDown(
+ editor,
+ caller,
+ function (id, time) {
+ 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;
+
+ // TODO: use escape.entities
+ /**
+ * Escapes XHTML entities
+ *
+ * @param {string} str
+ * @return {string}
+ * @private
+ */
+ function escapeEntities(str) {
+ var entities = {
+ '&': '&',
+ '<': '<',
+ '>': '>',
+ '"': '"',
+ '\xa0': ' '
+ };
+
+ return !str ? '' : str.replace(/[&<>"\xa0]/g, function (entity) {
+ return entities[entity] || entity;
+ });
+ };
+
+ /**
+ * @param {string} str
+ * @return {string}
+ * @private
+ */
+ function trim(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
+ */
+ function serializeNode(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
+ */
+ function handleDoc(node) {
+ var child = node.firstChild;
+
+ while (child) {
+ serializeNode(child);
+ child = child.nextSibling;
+ }
+ };
+
+ /**
+ * Handles element nodes
+ * @param {Node} node
+ * @return {void}
+ * @private
+ */
+ function handleElement(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(css(node, 'whiteSpace')),
+ selfClosing = !node.firstChild && !dom.canHaveChildren(node) &&
+ !isIframe;
+
+ if (is(node, '.sceditor-ignore')) {
+ return;
+ }
+
+ output('<' + tagName, !parentIsPre && canIndent(node));
+ while (attrIdx--) {
+ attr = node.attributes[attrIdx];
+
+ attrValue = attr.value;
+
+ output(' ' + attr.name.toLowerCase() + '="' +
+ escapeEntities(attrValue) + '"', false);
+ }
+ output(selfClosing ? ' />' : '>', false);
+
+ if (!isIframe) {
+ child = firstChild;
+ }
+
+ while (child) {
+ currentIndent++;
+
+ serializeNode(child, isPre);
+ child = child.nextSibling;
+
+ currentIndent--;
+ }
+
+ if (!selfClosing) {
+ output(
+ '' + tagName + '>',
+ !isPre && !isIframe && canIndent(node) &&
+ firstChild && canIndent(firstChild)
+ );
+ }
+ };
+
+ /**
+ * Handles CDATA nodes
+ * @param {Node} node
+ * @return {void}
+ * @private
+ */
+ function handleCdata(node) {
+ output('');
+ };
+
+ /**
+ * Handles comment nodes
+ * @param {Node} node
+ * @return {void}
+ * @private
+ */
+ function handleComment(node) {
+ output('');
+ };
+
+ /**
+ * Handles text nodes
+ * @param {Node} node
+ * @return {void}
+ * @private
+ */
+ function handleText(node, parentIsPre) {
+ var text = node.nodeValue;
+
+ if (!parentIsPre) {
+ text = trim(text);
+ }
+
+ if (text) {
+ output(escapeEntities(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
+ */
+ function output(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
+ */
+ function canIndent(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
+ */
+ function xhtmlFormat() {
+ var base = this;
+
+ /**
+ * Tag converters cache
+ * @type {Object}
+ * @private
+ */
+ var tagConvertersCache = {};
+
+ /**
+ * Attributes filter cache
+ * @type {Object}
+ * @private
+ */
+ var attrsCache = {};
+
+ /**
+ * Init
+ * @return {void}
+ */
+ base.init = function () {
+ if (!isEmptyObject(xhtmlFormat.converters || {})) {
+ each(
+ xhtmlFormat.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 {boolean} isFragment
+ * @param {string} html
+ * @param {Document} context
+ * @param {HTMLElement} [parent]
+ * @return {string}
+ * @memberOf jQuery.sceditor.plugins.xhtml.prototype
+ */
+ function toSource(isFragment, html, context) {
+ var xhtml,
+ container = context.createElement('div');
+ container.innerHTML = html;
+
+ css(container, 'visibility', 'hidden');
+ context.body.appendChild(container);
+
+ convertTags(container);
+ removeTags(container);
+ removeAttribs(container);
+
+ if (!isFragment) {
+ wrapInlines(container);
+ }
+
+ xhtml = (new sceditor.XHTMLSerializer()).serialize(container, true);
+
+ context.body.removeChild(container);
+
+ return xhtml;
+ };
+
+ base.toSource = toSource.bind(null, false);
+
+ base.fragmentToSource = toSource.bind(null, true);;
+
+ /**
+ * Runs all converters for the specified tagName
+ * against the DOM node.
+ * @param {string} tagName
+ * @return {Node} node
+ * @private
+ */
+ function convertNode(tagName, node) {
+ if (!tagConvertersCache[tagName]) {
+ return;
+ }
+
+ tagConvertersCache[tagName].forEach(function (converter) {
+ if (converter.tags[tagName]) {
+ each(converter.tags[tagName], function (attr, values) {
+ if (!node.getAttributeNode) {
+ return;
+ }
+
+ attr = node.getAttributeNode(attr);
+
+ if (!attr || values && values.indexOf(attr.value) < 0) {
+ return;
+ }
+
+ converter.conv.call(base, node);
+ });
+ } else if (converter.conv) {
+ converter.conv.call(base, node);
+ }
+ });
+ };
+
+ /**
+ * Converts any tags/attributes to their XHTML equivalents
+ * @param {Node} node
+ * @return {void}
+ * @private
+ */
+ function convertTags(node) {
+ dom.traverse(node, function (node) {
+ var tagName = node.nodeName.toLowerCase();
+
+ convertNode('*', node);
+ convertNode(tagName, node);
+ }, true);
+ };
+
+ /**
+ * Tests if a node is empty and can be removed.
+ *
+ * @param {Node} node
+ * @return {boolean}
+ * @private
+ */
+ function isEmpty(node, excludeBr) {
+ var rect,
+ childNodes = node.childNodes,
+ tagName = node.nodeName.toLowerCase(),
+ nodeValue = node.nodeValue,
+ childrenLength = childNodes.length,
+ allowedEmpty = xhtmlFormat.allowedEmptyTags || [];
+
+ if (excludeBr && tagName === 'br') {
+ return true;
+ }
+
+ if (is(node, '.sceditor-ignore')) {
+ return true;
+ }
+
+ if (allowedEmpty.indexOf(tagName) > -1 || tagName === 'td' ||
+ !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;
+ }
+ }
+
+ // Treat tags with a width and height from CSS as not empty
+ if (node.getBoundingClientRect &&
+ (node.className || node.hasAttributes('style'))) {
+ rect = node.getBoundingClientRect();
+ return !rect.width || !rect.height;
+ }
+
+ 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
+ */
+ function removeTags(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 = xhtmlFormat.allowedTags,
+ firstChild = node.firstChild,
+ disallowedTags = xhtmlFormat.disallowedTags;
+
+ // 3 = text node
+ if (nodeType === 3) {
+ return;
+ }
+
+ if (nodeType === 4) {
+ tagName = '!cdata';
+ } else if (tagName === '!' || nodeType === 8) {
+ tagName = '!comment';
+ }
+
+ if (nodeType === 1) {
+ // skip empty nlf elements (new lines automatically
+ // added after block level elements like quotes)
+ if (is(node, '.sceditor-nlf')) {
+ if (!firstChild || (!IE_BR_FIX &&
+ node.childNodes.length === 1 &&
+ /br/i.test(firstChild.nodeName))) {
+ // Mark as empty,it will be removed by the next code
+ empty = true;
+ } else {
+ node.classList.remove('sceditor-nlf');
+
+ if (!node.className) {
+ removeAttr(node, 'class');
+ }
+ }
+ }
+ }
+
+ if (empty) {
+ remove = true;
+ // 3 is text node which do not get filtered
+ } else if (allowedTags && allowedTags.length) {
+ remove = (allowedTags.indexOf(tagName) < 0);
+ } else if (disallowedTags && disallowedTags.length) {
+ remove = (disallowedTags.indexOf(tagName) > -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
+ */
+ function mergeAttribsFilters(filtersA, filtersB) {
+ var ret = {};
+
+ if (filtersA) {
+ extend(ret, filtersA);
+ }
+
+ if (!filtersB) {
+ return ret;
+ }
+
+ each(filtersB, function (attrName, values) {
+ if (Array.isArray(values)) {
+ ret[attrName] = (ret[attrName] || []).concat(values);
+ } else if (!ret[attrName]) {
+ ret[attrName] = null;
+ }
+ });
+
+ return ret;
+ };
+
+ /**
+ * Wraps adjacent inline child nodes of root
+ * in paragraphs.
+ *
+ * @param {Node} root
+ * @private
+ */
+ function wrapInlines(root) {
+ // Strip empty text nodes so they don't get wrapped.
+ dom.removeWhiteSpace(root);
+
+ var wrapper;
+ var node = root.firstChild;
+ var next;
+ while (node) {
+ next = node.nextSibling;
+
+ if (dom.isInline(node) && !is(node, '.sceditor-ignore')) {
+ if (!wrapper) {
+ wrapper = root.ownerDocument.createElement('p');
+ node.parentNode.insertBefore(wrapper, node);
+ }
+
+ wrapper.appendChild(node);
+ } else {
+ wrapper = null;
+ }
+
+ node = next;
+ }
+ };
+
+ /**
+ * 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
+ */
+ function removeAttribs(node) {
+ var tagName, attr, attrName, attrsLength, validValues, remove,
+ allowedAttribs = xhtmlFormat.allowedAttribs,
+ isAllowed = allowedAttribs &&
+ !isEmptyObject(allowedAttribs),
+ disallowedAttribs = xhtmlFormat.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 &&
+ (!Array.isArray(validValues) ||
+ validValues.indexOf(attr.value) < 0);
+ } else if (isDisallowed) {
+ remove = validValues === null ||
+ (Array.isArray(validValues) &&
+ validValues.indexOf(attr.value) > -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
+ */
+ xhtmlFormat.converters = [
+ {
+ tags: {
+ '*': {
+ width: null
+ }
+ },
+ conv: function (node) {
+ css(node, 'width', attr(node, 'width'));
+ removeAttr(node, 'width');
+ }
+ },
+ {
+ tags: {
+ '*': {
+ height: null
+ }
+ },
+ conv: function (node) {
+ css(node, 'height', attr(node, 'height'));
+ removeAttr(node, 'height');
+ }
+ },
+ {
+ tags: {
+ 'li': {
+ value: null
+ }
+ },
+ conv: function (node) {
+ removeAttr(node, 'value');
+ }
+ },
+ {
+ tags: {
+ '*': {
+ text: null
+ }
+ },
+ conv: function (node) {
+ css(node, 'color', attr(node, 'text'));
+ removeAttr(node, 'text');
+ }
+ },
+ {
+ tags: {
+ '*': {
+ color: null
+ }
+ },
+ conv: function (node) {
+ css(node, 'color', attr(node, 'color'));
+ removeAttr(node, 'color');
+ }
+ },
+ {
+ tags: {
+ '*': {
+ face: null
+ }
+ },
+ conv: function (node) {
+ css(node, 'fontFamily', attr(node, 'face'));
+ removeAttr(node, 'face');
+ }
+ },
+ {
+ tags: {
+ '*': {
+ align: null
+ }
+ },
+ conv: function (node) {
+ css(node, 'textAlign', attr(node, 'align'));
+ removeAttr(node, 'align');
+ }
+ },
+ {
+ tags: {
+ '*': {
+ border: null
+ }
+ },
+ conv: function (node) {
+ css(node, 'borderWidth', attr(node, 'border'));
+ removeAttr(node, 'border');
+ }
+ },
+ {
+ tags: {
+ applet: {
+ name: null
+ },
+ img: {
+ name: null
+ },
+ layer: {
+ name: null
+ },
+ map: {
+ name: null
+ },
+ object: {
+ name: null
+ },
+ param: {
+ name: null
+ }
+ },
+ conv: function (node) {
+ if (!attr(node, 'id')) {
+ attr(node, 'id', attr(node, 'name'));
+ }
+
+ removeAttr(node, 'name');
+ }
+ },
+ {
+ tags: {
+ '*': {
+ vspace: null
+ }
+ },
+ conv: function (node) {
+ css(node, 'marginTop', attr(node, 'vspace') - 0);
+ css(node, 'marginBottom', attr(node, 'vspace') - 0);
+ removeAttr(node, 'vspace');
+ }
+ },
+ {
+ tags: {
+ '*': {
+ hspace: null
+ }
+ },
+ conv: function (node) {
+ css(node, 'marginLeft', attr(node, 'hspace') - 0);
+ css(node, 'marginRight', attr(node, 'hspace') - 0);
+ removeAttr(node, 'hspace');
+ }
+ },
+ {
+ tags: {
+ 'hr': {
+ noshade: null
+ }
+ },
+ conv: function (node) {
+ css(node, 'borderStyle', 'solid');
+ removeAttr(node, 'noshade');
+ }
+ },
+ {
+ tags: {
+ '*': {
+ nowrap: null
+ }
+ },
+ conv: function (node) {
+ css(node, 'whiteSpace', 'nowrap');
+ removeAttr(node, 'nowrap');
+ }
+ },
+ {
+ tags: {
+ big: null
+ },
+ conv: function (node) {
+ css(convertElement(node, 'span'), 'fontSize', 'larger');
+ }
+ },
+ {
+ tags: {
+ small: null
+ },
+ conv: function (node) {
+ css(convertElement(node, 'span'), 'fontSize', 'smaller');
+ }
+ },
+ {
+ tags: {
+ b: null
+ },
+ conv: function (node) {
+ convertElement(node, 'strong');
+ }
+ },
+ {
+ tags: {
+ u: null
+ },
+ conv: function (node) {
+ css(convertElement(node, 'span'), 'textDecoration',
+ 'underline');
+ }
+ },
+ {
+ tags: {
+ s: null,
+ strike: null
+ },
+ conv: function (node) {
+ css(convertElement(node, 'span'), 'textDecoration',
+ 'line-through');
+ }
+ },
+ {
+ tags: {
+ dir: null
+ },
+ conv: function (node) {
+ convertElement(node, 'ul');
+ }
+ },
+ {
+ tags: {
+ center: null
+ },
+ conv: function (node) {
+ css(convertElement(node, 'div'), 'textAlign', 'center');
+ }
+ },
+ {
+ tags: {
+ font: {
+ size: null
+ }
+ },
+ conv: function (node) {
+ css(node, 'fontSize', css(node, 'fontSize'));
+ removeAttr(node, 'size');
+ }
+ },
+ {
+ tags: {
+ font: null
+ },
+ conv: function (node) {
+ // All it's attributes will be converted
+ // by the attribute converters
+ convertElement(node, 'span');
+ }
+ },
+ {
+ tags: {
+ '*': {
+ type: ['_moz']
+ }
+ },
+ conv: function (node) {
+ removeAttr(node, 'type');
+ }
+ },
+ {
+ tags: {
+ '*': {
+ '_moz_dirty': null
+ }
+ },
+ conv: function (node) {
+ removeAttr(node, '_moz_dirty');
+ }
+ },
+ {
+ tags: {
+ '*': {
+ '_moz_editor_bogus_node': null
+ }
+ },
+ conv: function (node) {
+ node.parentNode.removeChild(node);
+ }
+ }
+ ];
+
+ /**
+ * 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
+ */
+ xhtmlFormat.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
+ */
+ xhtmlFormat.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
+ */
+ xhtmlFormat.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
+ */
+ xhtmlFormat.disallowedTags = [];
+
+ /**
+ * Array containing tags which should not be removed when empty.
+ *
+ * @type {Array}
+ * @name jQuery.sceditor.plugins.xhtml.allowedEmptyTags
+ * @since v2.0.0
+ */
+ xhtmlFormat.allowedEmptyTags = [];
+
+ sceditor.formats.xhtml = xhtmlFormat;
+}(sceditor));
diff --git a/public/assets/development/plugins/autosave.js b/public/assets/development/plugins/autosave.js
new file mode 100644
index 0000000..950aca2
--- /dev/null
+++ b/public/assets/development/plugins/autosave.js
@@ -0,0 +1,105 @@
+/**
+ * SCEditor AutoSave Plugin
+ * http://www.sceditor.com/
+ *
+ * Copyright (C) 2017, Sam Clarke (samclarke.com)
+ *
+ * SCEditor is licensed under the MIT license:
+ * http://www.opensource.org/licenses/mit-license.php
+ *
+ * @author Sam Clarke
+ */
+(function (sceditor) {
+ 'use strict';
+
+ var defaultKey = 'sce-autodraft-' + location.pathname + location.search;
+
+ function clear(key) {
+ localStorage.removeItem(key || defaultKey);
+ }
+
+ sceditor.plugins.autosave = function () {
+ var base = this;
+ var editor;
+ var storageKey = defaultKey;
+ // 86400000 = 24 hrs (24 * 60 * 60 * 1000)
+ var expires = 86400000;
+ var saveHandler = function (value) {
+ localStorage.setItem(storageKey, JSON.stringify(value));
+ };
+ var loadHandler = function () {
+ return JSON.parse(localStorage.getItem(storageKey));
+ };
+
+ function gc() {
+ for (var i = 0; i < localStorage.length; i++) {
+ var key = localStorage.key(i);
+
+ if (/^sce\-autodraft\-/.test(key)) {
+ var item = JSON.parse(localStorage.getItem(storageKey));
+ if (item && item.time < Date.now() - expires) {
+ clear(key);
+ }
+ }
+ }
+ }
+
+ base.init = function () {
+ editor = this;
+ var opts = editor.opts && editor.opts.autosave || {};
+
+ saveHandler = opts.save || saveHandler;
+ loadHandler = opts.load || loadHandler;
+ storageKey = opts.storageKey || storageKey;
+ expires = opts.expires || expires;
+
+ gc();
+ };
+
+ base.signalReady = function () {
+ // Add submit event listener to clear autosave
+ var parent = editor.getContentAreaContainer();
+ while (parent) {
+ if (/form/i.test(parent.nodeName)) {
+ parent.addEventListener(
+ 'submit', clear.bind(null, storageKey), true
+ );
+ break;
+ }
+
+ parent = parent.parentNode;
+ }
+
+ var state = loadHandler();
+ if (state) {
+ editor.sourceMode(state.sourceMode);
+ editor.val(state.value, false);
+ editor.focus();
+
+ if (state.sourceMode) {
+ editor.sourceEditorCaret(state.caret);
+ } else {
+ editor.getRangeHelper().restoreRange();
+ }
+ }
+
+ saveHandler({
+ caret: this.sourceEditorCaret(),
+ sourceMode: this.sourceMode(),
+ value: editor.val(null, false),
+ time: Date.now()
+ });
+ };
+
+ base.signalValuechangedEvent = function (e) {
+ saveHandler({
+ caret: this.sourceEditorCaret(),
+ sourceMode: this.sourceMode(),
+ value: e.detail.rawValue,
+ time: Date.now()
+ });
+ };
+ };
+
+ sceditor.plugins.autosave.clear = clear;
+}(sceditor));
diff --git a/public/assets/development/plugins/autoyoutube.js b/public/assets/development/plugins/autoyoutube.js
new file mode 100644
index 0000000..3f3ea39
--- /dev/null
+++ b/public/assets/development/plugins/autoyoutube.js
@@ -0,0 +1,94 @@
+/**
+ * SCEditor Auto Youtube Plugin
+ * http://www.sceditor.com/
+ *
+ * Copyright (C) 2016, Sam Clarke (samclarke.com)
+ *
+ * SCEditor is licensed under the MIT license:
+ * http://www.opensource.org/licenses/mit-license.php
+ *
+ * @author Sam Clarke
+ */
+(function (document, sceditor) {
+ 'use strict';
+
+ var dom = sceditor.dom;
+
+ /*
+ (^|\s) Start of line or space
+ (?:https?:\/\/)? Optional scheme like http://
+ (?:www\.)? Optional www. prefix
+ (?:
+ youtu\.be\/ Ends with .be/ so whatever comes next is the ID
+ |
+ youtube\.com\/watch\?v= Matches the .com version
+ )
+ ([^"&?\/ ]{11}) The actual YT ID
+ (?:\&[\&_\?0-9a-z\#]+)? Any extra URL params
+ (\s|$) End of line or space
+ */
+ var ytUrlRegex = /(^|\s)(?:https?:\/\/)?(?:www\.)?(?:youtu\.be\/|youtube\.com\/watch\?v=)([^"&?\/ ]{11})(?:\&[\&_\?0-9a-z\#]+)?(\s|$)/i;
+
+ function youtubeEmbedCode(id) {
+ return '';
+ }
+
+ function convertYoutubeLinks(root) {
+ var node = root.firstChild;
+
+ while (node) {
+ // 3 is TextNodes
+ if (node.nodeType === 3) {
+ var text = node.nodeValue;
+ var parent = node.parentNode;
+ var match = text.match(ytUrlRegex);
+
+ if (match) {
+ parent.insertBefore(document.createTextNode(
+ text.substr(0, match.index) + match[1]
+ ), node);
+
+ parent.insertBefore(
+ dom.parseHTML(youtubeEmbedCode(match[2])), node
+ );
+
+ node.nodeValue = match[3] +
+ text.substr(match.index + match[0].length);
+ }
+ } else {
+ // TODO: Make this tag configurable.
+ if (!dom.is(node, 'code')) {
+ convertYoutubeLinks(node);
+ }
+ }
+
+ node = node.nextSibling;
+ }
+ };
+
+ sceditor.plugins.autoyoutube = function () {
+ this.signalPasteRaw = function (data) {
+ // TODO: Make this tag configurable.
+ // Skip code tags
+ if (dom.closest(this.currentNode(), 'code')) {
+ return;
+ }
+
+ if (data.html || data.text) {
+ var html = document.createElement('div');
+
+ if (data.html) {
+ html.innerHTML = data.html;
+ } else {
+ html.textContent = data.text;
+ }
+
+ convertYoutubeLinks(html);
+
+ data.html = html.innerHTML;
+ }
+ };
+ };
+})(document, sceditor);
diff --git a/public/assets/development/plugins/dragdrop.js b/public/assets/development/plugins/dragdrop.js
new file mode 100644
index 0000000..b050309
--- /dev/null
+++ b/public/assets/development/plugins/dragdrop.js
@@ -0,0 +1,222 @@
+/**
+ * SCEditor Drag and Drop Plugin
+ * http://www.sceditor.com/
+ *
+ * Copyright (C) 2017, Sam Clarke (samclarke.com)
+ *
+ * SCEditor is licensed under the MIT license:
+ * http://www.opensource.org/licenses/mit-license.php
+ *
+ * @author Sam Clarke
+ */
+(function (sceditor) {
+ 'use strict';
+
+ /**
+ * Place holder GIF shown while image is loading.
+ * @type {string}
+ * @private
+ */
+ var loadingGif = 'data:image/gif;base64,R0lGODlhlgBkAPABAH19ffb29iH5BAAK' +
+ 'AAAAIf4aQ3JlYXRlZCB3aXRoIGFqYXhsb2FkLmluZm8AIf8LTkVUU0NBUEUyLjADAQA' +
+ 'AACwAAAAAlgBkAAAC1YyPqcvtD6OctNqLs968+w+G4kiW5omm6sq27gvH8kzX9o3n+s' +
+ '73/g8MCofEovGITCqXzKbzCY1Kp9Sq9YrNarfcrvcLDovH5LL5jE6r1+y2+w2Py+f0u' +
+ 'v2OvwD2fP6iD/gH6Pc2GIhg2JeQSNjGuLf4GMlYKIloefAIUEl52ZmJyaY5mUhqyFnq' +
+ 'mQr6KRoaMKp66hbLumpQ69oK+5qrOyg4a6qYV2x8jJysvMzc7PwMHS09TV1tfY2drb3' +
+ 'N3e39DR4uPk5ebn6Onq6+zt7u/g4fL99UAAAh+QQACgAAACwAAAAAlgBkAIEAAAB9fX' +
+ '329vYAAAAC3JSPqcvtD6OctNqLs968+w+G4kiW5omm6sq27gvH8kzX9o3n+s73/g8MC' +
+ 'ofEovGITCqXzKbzCY1Kp9Sq9YrNarfcrvcLDovH5LL5jE6r1+y2+w2Py+f0uv2OvwD2' +
+ 'fP4iABgY+CcoCNeHuJdQyLjIaOiWiOj4CEhZ+SbZd/nI2RipqYhQOThKGpAZCuBZyAr' +
+ 'ZprpqSupaCqtaazmLCRqai7rb2av5W5wqSShcm8fc7PwMHS09TV1tfY2drb3N3e39DR' +
+ '4uPk5ebn6Onq6+zt7u/g4fLz9PX29/j5/vVAAAIfkEAAoAAAAsAAAAAJYAZACBAAAAf' +
+ 'X199vb2AAAAAuCUj6nL7Q+jnLTai7PevPsPhuJIluaJpurKtu4Lx/JM1/aN5/rO9/4P' +
+ 'DAqHxKLxiEwql8ym8wmNSqfUqvWKzWq33K73Cw6Lx+Sy+YxOq9fstvsNj8vn9Lr9jr8' +
+ 'E9nz+AgAYGLjQVwhXiJgguAiYgGjo9tinyCjoKLn3hpmJUGmJsBmguUnpCXCJOZraaX' +
+ 'oKShoJe9DqehCqKlnqiZobuzrbyvuIO8xqKpxIPKlwrPCbBx0tPU1dbX2Nna29zd3t/' +
+ 'Q0eLj5OXm5+jp6uvs7e7v4OHy8/T19vf4+fr7/P379UAAAh+QQACgAAACwAAAAAlgBk' +
+ 'AIEAAAB9fX329vYAAAAC4JSPqcvtD6OctNqLs968+w+G4kiW5omm6sq27gvH8kzX9o3' +
+ 'n+s73/g8MCofEovGITCqXzKbzCY1Kp9Sq9YrNarfcrvcLDovH5LL5jE6r1+y2+w2Py+' +
+ 'f0uv2OvwT2fP6iD7gAMEhICAeImIAYiFDoOPi22KcouZfw6BhZGUBZeYlp6LbJiTD6C' +
+ 'Qqg6Vm6eQqqKtkZ24iaKtrKunpQa9tmmju7Wwu7KFtMi3oYDMzompkHHS09TV1tfY2d' +
+ 'rb3N3e39DR4uPk5ebn6Onq6+zt7u/g4fLz9PX29/j5+vv8/f31QAADs=';
+
+ /**
+ * Basic check for browser support
+ * @type {boolean}
+ * @private
+ */
+ var isSupported = typeof window.FileReader !== 'undefined';
+ var base64DataUri = /data:[^;]+;base64,/i;
+
+ function base64DataUriToBlob(url) {
+ // 5 is length of "data:" prefix
+ var mime = url.substr(5, url.indexOf(';') - 5);
+ var data = atob(url.substr(url.indexOf(',') + 1));
+ /* global Uint8Array */
+ var binary = new Uint8Array(data.length);
+
+ for (var i = 0; i < data.length; i++) {
+ binary[i] = data[i].charCodeAt(0);
+ }
+
+ try {
+ return new Blob([binary], { type: mime });
+ } catch (e) {
+ return null;
+ }
+ }
+
+ sceditor.plugins.dragdrop = function () {
+ if (!isSupported) {
+ return;
+ }
+
+ var base = this;
+ var opts;
+ var editor;
+ var handleFile;
+ var container;
+ var cover;
+ var placeholderId = 0;
+
+
+ function hideCover() {
+ cover.style.display = 'none';
+ container.className = container.className.replace(/(^| )dnd( |$)/g, '');
+ }
+
+ function showCover() {
+ if (cover.style.display === 'none') {
+ cover.style.display = 'block';
+ container.className += ' dnd';
+ }
+ }
+
+ function isAllowed(file) {
+ // FF sets type to application/x-moz-file until it has been dropped
+ if (file.type !== 'application/x-moz-file' && opts.allowedTypes &&
+ opts.allowedTypes.indexOf(file.type) < 0) {
+ return false;
+ }
+
+ return opts.isAllowed ? opts.isAllowed(file) : true;
+ };
+
+ function createHolder(toReplace) {
+ var placeholder = document.createElement('img');
+ placeholder.src = loadingGif;
+ placeholder.className = 'sceditor-ignore';
+ placeholder.id = 'sce-dragdrop-' + placeholderId++;
+
+ function replace(html) {
+ var node = editor
+ .getBody()
+ .ownerDocument
+ .getElementById(placeholder.id);
+
+ if (node) {
+ if (typeof html === 'string') {
+ node.insertAdjacentHTML('afterend', html);
+ }
+
+ node.parentNode.removeChild(node);
+ }
+ }
+
+ return function () {
+ if (toReplace) {
+ toReplace.parentNode.replaceChild(placeholder, toReplace);
+ } else {
+ editor.wysiwygEditorInsertHtml(placeholder.outerHTML);
+ }
+
+ return {
+ insert: function (html) {
+ replace(html);
+ },
+ cancel: replace
+ };
+ };
+ }
+
+ function handleDragOver(e) {
+ var dt = e.dataTransfer;
+ var files = dt.files.length || !dt.items ? dt.files : dt.items;
+
+ for (var i = 0; i < files.length; i++) {
+ // Dragging a string should be left to default
+ if (files[i].kind === 'string') {
+ return;
+ }
+ }
+
+ showCover();
+ e.preventDefault();
+ }
+
+ function handleDrop(e) {
+ var dt = e.dataTransfer;
+ var files = dt.files.length || !dt.items ? dt.files : dt.items;
+
+ hideCover();
+
+ for (var i = 0; i < files.length; i++) {
+ // Dragging a string should be left to default
+ if (files[i].kind === 'string') {
+ return;
+ }
+
+ if (isAllowed(files[i])) {
+ handleFile(files[i], createHolder());
+ }
+ }
+
+ e.preventDefault();
+ }
+
+ base.signalReady = function () {
+ editor = this;
+ opts = editor.opts.dragdrop || {};
+ handleFile = opts.handleFile;
+
+ container = editor.getContentAreaContainer().parentNode;
+
+ cover = container.appendChild(sceditor.dom.parseHTML(
+ '' +
+ '
' + editor._('Drop files here') + '
' +
+ '
'
+ ).firstChild);
+
+ container.addEventListener('dragover', handleDragOver);
+ container.addEventListener('dragleave', hideCover);
+ container.addEventListener('dragend', hideCover);
+ container.addEventListener('drop', handleDrop);
+
+ editor.getBody().addEventListener('dragover', handleDragOver);
+ editor.getBody().addEventListener('drop', hideCover);
+ };
+
+ base.signalPasteHtml = function (paste) {
+ if (!('handlePaste' in opts) || opts.handlePaste) {
+ var div = document.createElement('div');
+ div.innerHTML = paste.val;
+
+ var images = div.querySelectorAll('img');
+ for (var i = 0; i < images.length; i++) {
+ var image = images[i];
+
+ if (base64DataUri.test(image.src)) {
+ var file = base64DataUriToBlob(image.src);
+ if (file && isAllowed(file)) {
+ handleFile(file, createHolder(image));
+ } else {
+ image.parentNode.removeChild(image);
+ }
+ }
+ }
+
+ paste.val = div.innerHTML;
+ }
+ };
+ };
+})(sceditor);
diff --git a/public/assets/development/plugins/format.js b/public/assets/development/plugins/format.js
new file mode 100644
index 0000000..bcf4476
--- /dev/null
+++ b/public/assets/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
+ */
+(function (sceditor) {
+ '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.format && opts.format === 'bbcode') {
+ return;
+ }
+
+ if (pOpts) {
+ if (pOpts.tags) {
+ tags = pOpts.tags;
+ }
+
+ if (pOpts.excludeTags) {
+ pOpts.excludeTags.forEach(function (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 + '>', '' + tag + '>');
+ } else {
+ editor.execCommand('formatblock', '<' + tag + '>');
+ }
+
+ };
+
+ /**
+ * Function for the exec and txtExec properties
+ *
+ * @param {node} caller
+ * @private
+ */
+ formatCmd = function (caller) {
+ var editor = this,
+ content = document.createElement('div');
+
+ sceditor.utils.each(tags, function (tag, val) {
+ var link = document.createElement('a');
+ link.className = 'sceditor-option';
+ link.textContent = val.name || val;
+ link.addEventListener('click', function (e) {
+ editor.closeDropDown(true);
+
+ if (val.exec) {
+ val.exec(editor);
+ } else {
+ insertTag(editor, tag);
+ }
+
+ e.preventDefault();
+ });
+
+ content.appendChild(link);
+ });
+
+ editor.createDropDown(caller, 'format', content);
+ };
+ };
+})(sceditor);
diff --git a/public/assets/development/plugins/plaintext.js b/public/assets/development/plugins/plaintext.js
new file mode 100644
index 0000000..7b2bfc6
--- /dev/null
+++ b/public/assets/development/plugins/plaintext.js
@@ -0,0 +1,59 @@
+/**
+ * SCEditor Plain Text Plugin
+ * http://www.sceditor.com/
+ *
+ * Copyright (C) 2016, Sam Clarke (samclarke.com)
+ *
+ * SCEditor is licensed under the MIT license:
+ * http://www.opensource.org/licenses/mit-license.php
+ *
+ * @author Sam Clarke
+ */
+(function (sceditor) {
+ 'use strict';
+
+ var extend = sceditor.utils.extend;
+
+ /**
+ * Options:
+ *
+ * pastetext.addButton - If to replace the plaintext button with a toggle
+ * button that enables and disables plain text mode.
+ *
+ * pastetext.enabled - If the plain text button should be enabled at start
+ * up. Only applies if addButton is enabled.
+ */
+ sceditor.plugins.plaintext = function () {
+ var plainTextEnabled = true;
+
+ this.init = function () {
+ var commands = this.commands;
+ var opts = this.opts;
+
+ if (opts && opts.plaintext && opts.plaintext.addButton) {
+ plainTextEnabled = opts.plaintext.enabled;
+
+ commands.pastetext = extend(commands.pastetext || {}, {
+ state: function () {
+ return plainTextEnabled ? 1 : 0;
+ },
+ exec: function () {
+ plainTextEnabled = !plainTextEnabled;
+ }
+ });
+ }
+ };
+
+ this.signalPasteRaw = function (data) {
+ if (plainTextEnabled) {
+ if (data.html && !data.text) {
+ var div = document.createElement('div');
+ div.innerHTML = data.html;
+ data.text = div.innerText;
+ }
+
+ data.html = null;
+ }
+ };
+ };
+}(sceditor));
diff --git a/public/assets/development/plugins/strictbbcode.js b/public/assets/development/plugins/strictbbcode.js
new file mode 100644
index 0000000..da23bbf
--- /dev/null
+++ b/public/assets/development/plugins/strictbbcode.js
@@ -0,0 +1,21 @@
+/**
+ * SCEditor Strict BBCode Plugin
+ * http://www.sceditor.com/
+ *
+ * Copyright (C) 2016, Sam Clarke (samclarke.com)
+ *
+ * SCEditor is licensed under the MIT license:
+ * http://www.opensource.org/licenses/mit-license.php
+ *
+ * @author Sam Clarke
+ */
+// (function (sceditor) {
+// 'use strict';
+
+// // var extend = sceditor.utils.extend;
+
+// sceditor.plugins.strictbbcode = function () {
+// // override bbcode plugin add and update default bbcodes to have attrs
+// // and override their exec funcs
+// };
+// }(sceditor));
diff --git a/public/assets/development/plugins/undo.js b/public/assets/development/plugins/undo.js
new file mode 100644
index 0000000..2974598
--- /dev/null
+++ b/public/assets/development/plugins/undo.js
@@ -0,0 +1,187 @@
+(function (sceditor) {
+ '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;
+ };
+
+
+ /**
+ * Calculates 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 depending on plugins).
+ * @return {void}
+ */
+ base.signalValuechangedEvent = function (e) {
+ var rawValue = e.detail.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;
+ };
+ };
+}(sceditor));
diff --git a/public/assets/development/plugins/v1compat.js b/public/assets/development/plugins/v1compat.js
new file mode 100644
index 0000000..e67e5f3
--- /dev/null
+++ b/public/assets/development/plugins/v1compat.js
@@ -0,0 +1,97 @@
+/**
+ * Version 1 compatibility plugin
+ *
+ * Patches commands and BBCodes set with
+ * command.set and bbcode.set to wrap DOM
+ * node arguments in jQuery objects.
+ *
+ * Should only be used to ease migrating.
+ */
+(function (sceditor, $) {
+ 'use strict';
+
+ var plugins = sceditor.plugins;
+
+ /**
+ * Patches a method to wrap and DOM nodes in a jQuery object
+ * @private
+ */
+ function patchMethodArguments(fn) {
+ if (fn._scePatched) {
+ return fn;
+ }
+
+ var patch = function () {
+ var args = [];
+
+ for (var i = 0; i < arguments.length; i++) {
+ var arg = arguments[i];
+
+ if (arg && arg.nodeType) {
+ args.push($(arg));
+ } else {
+ args.push(arg);
+ }
+ }
+
+ return fn.apply(this, args);
+ };
+
+ patch._scePatched = true;
+ return patch;
+ }
+
+ /**
+ * Patches a method to wrap any return value in a jQuery object
+ * @private
+ */
+ function patchMethodReturn(fn) {
+ if (fn._scePatched) {
+ return fn;
+ }
+
+ var patch = function () {
+ return $(fn.apply(this, arguments));
+ };
+
+ patch._scePatched = true;
+ return patch;
+ }
+
+ var oldSet = sceditor.command.set;
+ sceditor.command.set = function (name, cmd) {
+ if (cmd && typeof cmd.exec === 'function') {
+ cmd.exec = patchMethodArguments(cmd.exec);
+ }
+
+ if (cmd && typeof cmd.txtExec === 'function') {
+ cmd.txtExec = patchMethodArguments(cmd.txtExec);
+ }
+
+ return oldSet.call(this, name, cmd);
+ };
+
+ if (plugins.bbcode) {
+ var oldBBCodeSet = plugins.bbcode.bbcode.set;
+ plugins.bbcode.bbcode.set = function (name, bbcode) {
+ if (bbcode && typeof bbcode.format === 'function') {
+ bbcode.format = patchMethodArguments(bbcode.format);
+ }
+
+ return oldBBCodeSet.call(this, name, bbcode);
+ };
+ };
+
+ var oldCreate = sceditor.create;
+ sceditor.create = function (textarea, options) {
+ oldCreate.call(this, textarea, options);
+
+ if (textarea && textarea._sceditor) {
+ var editor = textarea._sceditor;
+
+ editor.getBody = patchMethodReturn(editor.getBody);
+ editor.getContentAreaContainer =
+ patchMethodReturn(editor.getContentAreaContainer);
+ }
+ };
+}(sceditor, jQuery));
diff --git a/public/assets/development/sceditor.js b/public/assets/development/sceditor.js
new file mode 100644
index 0000000..1d4410d
--- /dev/null
+++ b/public/assets/development/sceditor.js
@@ -0,0 +1,7571 @@
+(function () {
+ 'use strict';
+
+ /**
+ * Check if the passed argument is the
+ * the passed type.
+ *
+ * @param {string} type
+ * @param {*} arg
+ * @returns {boolean}
+ */
+ function isTypeof(type, arg) {
+ return typeof arg === type;
+ }
+
+ /**
+ * @type {function(*): boolean}
+ */
+ var isString = isTypeof.bind(null, 'string');
+
+ /**
+ * @type {function(*): boolean}
+ */
+ var isUndefined = isTypeof.bind(null, 'undefined');
+
+ /**
+ * @type {function(*): boolean}
+ */
+ var isFunction = isTypeof.bind(null, 'function');
+
+ /**
+ * @type {function(*): boolean}
+ */
+ var isNumber = isTypeof.bind(null, 'number');
+
+
+ /**
+ * Returns true if an object has no keys
+ *
+ * @param {!Object} obj
+ * @returns {boolean}
+ */
+ function isEmptyObject(obj) {
+ return !Object.keys(obj).length;
+ }
+
+ /**
+ * Extends the first object with any extra objects passed
+ *
+ * If the first argument is boolean and set to true
+ * it will extend child arrays and objects recursively.
+ *
+ * @param {!Object|boolean} targetArg
+ * @param {...Object} source
+ * @return {Object}
+ */
+ function extend(targetArg, sourceArg) {
+ var isTargetBoolean = targetArg === !!targetArg;
+ var i = isTargetBoolean ? 2 : 1;
+ var target = isTargetBoolean ? sourceArg : targetArg;
+ var isDeep = isTargetBoolean ? targetArg : false;
+
+ for (; i < arguments.length; i++) {
+ var source = arguments[i];
+
+ // Copy all properties for jQuery compatibility
+ /* eslint guard-for-in: off */
+ for (var key in source) {
+ var value = source[key];
+
+ // Skip undefined values to match jQuery and
+ // skip if target to prevent infinite loop
+ if (!isUndefined(value)) {
+ var isObject = value !== null && typeof value === 'object' &&
+ Object.getPrototypeOf(value) === Object.prototype;
+ var isArray = Array.isArray(value);
+
+ if (isDeep && (isObject || isArray)) {
+ target[key] = extend(
+ true,
+ target[key] || (isArray ? [] : {}),
+ value
+ );
+ } else {
+ target[key] = value;
+ }
+ }
+ }
+ }
+
+ return target;
+ }
+
+ /**
+ * Removes an item from the passed array
+ *
+ * @param {!Array} arr
+ * @param {*} item
+ */
+ function arrayRemove(arr, item) {
+ var i = arr.indexOf(item);
+
+ if (i > -1) {
+ arr.splice(i, 1);
+ }
+ }
+
+ /**
+ * Iterates over an array or object
+ *
+ * @param {!Object|Array} obj
+ * @param {function(*, *)} fn
+ */
+ function each(obj, fn) {
+ if (Array.isArray(obj) || 'length' in obj && isNumber(obj.length)) {
+ for (var i = 0; i < obj.length; i++) {
+ fn(i, obj[i]);
+ }
+ } else {
+ Object.keys(obj).forEach(function (key) {
+ fn(key, obj[key]);
+ });
+ }
+ }
+
+ /**
+ * Cache of camelCase CSS property names
+ * @type {Object}
+ */
+ var cssPropertyNameCache = {};
+
+ /**
+ * Node type constant for element nodes
+ *
+ * @type {number}
+ */
+ var ELEMENT_NODE = 1;
+
+ /**
+ * Node type constant for text nodes
+ *
+ * @type {number}
+ */
+ var TEXT_NODE = 3;
+
+ /**
+ * Node type constant for comment nodes
+ *
+ * @type {number}
+ */
+
+
+ /**
+ * Node type document nodes
+ *
+ * @type {number}
+ */
+
+
+ /**
+ * Node type constant for document fragments
+ *
+ * @type {number}
+ */
+
+
+ function toFloat(value) {
+ value = parseFloat(value);
+
+ return isFinite(value) ? value : 0;
+ }
+
+ /**
+ * Creates an element with the specified attributes
+ *
+ * Will create it in the current document unless context
+ * is specified.
+ *
+ * @param {!string} tag
+ * @param {!Object} [attributes]
+ * @param {!Document} [context]
+ * @returns {!HTMLElement}
+ */
+ function createElement(tag, attributes, context) {
+ var node = (context || document).createElement(tag);
+
+ each(attributes || {}, function (key, value) {
+ if (key === 'style') {
+ node.style.cssText = value;
+ } else if (key in node) {
+ node[key] = value;
+ } else {
+ node.setAttribute(key, value);
+ }
+ });
+
+ return node;
+ }
+
+ /**
+ * Returns an array of parents that matches the selector
+ *
+ * @param {!HTMLElement} node
+ * @param {!string} [selector]
+ * @returns {Array}
+ */
+
+
+ /**
+ * Gets the first parent node that matches the selector
+ *
+ * @param {!HTMLElement} node
+ * @param {!string} [selector]
+ * @returns {HTMLElement|undefined}
+ */
+ function parent(node, selector) {
+ var parent = node || {};
+
+ while ((parent = parent.parentNode) && !/(9|11)/.test(parent.nodeType)) {
+ if (!selector || is(parent, selector)) {
+ return parent;
+ }
+ }
+ }
+
+ /**
+ * Checks the passed node and all parents and
+ * returns the first matching node if any.
+ *
+ * @param {!HTMLElement} node
+ * @param {!string} selector
+ * @returns {HTMLElement|undefined}
+ */
+ function closest(node, selector) {
+ return is(node, selector) ? node : parent(node, selector);
+ }
+
+ /**
+ * Removes the node from the DOM
+ *
+ * @param {!HTMLElement} node
+ */
+ function remove(node) {
+ if (node.parentNode) {
+ node.parentNode.removeChild(node);
+ }
+ }
+
+ /**
+ * Appends child to parent node
+ *
+ * @param {!HTMLElement} node
+ * @param {!HTMLElement} child
+ */
+ function appendChild(node, child) {
+ node.appendChild(child);
+ }
+
+ /**
+ * Finds any child nodes that match the selector
+ *
+ * @param {!HTMLElement} node
+ * @param {!string} selector
+ * @returns {NodeList}
+ */
+ function find(node, selector) {
+ return node.querySelectorAll(selector);
+ }
+
+ /**
+ * For on() and off() if to add/remove the event
+ * to the capture phase
+ *
+ * @type {boolean}
+ */
+ var EVENT_CAPTURE = true;
+
+ /**
+ * For on() and off() if to add/remove the event
+ * to the bubble phase
+ *
+ * @type {boolean}
+ */
+
+
+ /**
+ * Adds an event listener for the specified events.
+ *
+ * Events should be a space separated list of events.
+ *
+ * If selector is specified the handler will only be
+ * called when the event target matches the selector.
+ *
+ * @param {!Node} node
+ * @param {string} events
+ * @param {string} [selector]
+ * @param {function(Object)} fn
+ * @param {boolean} [capture=false]
+ * @see off()
+ */
+ // eslint-disable-next-line max-params
+ function on(node, events, selector, fn, capture) {
+ events.split(' ').forEach(function (event) {
+ var handler;
+
+ if (isString(selector)) {
+ handler = fn['_sce-event-' + event + selector] || function (e) {
+ var target = e.target;
+ while (target && target !== node) {
+ if (is(target, selector)) {
+ fn.call(target, e);
+ return;
+ }
+
+ target = target.parentNode;
+ }
+ };
+
+ fn['_sce-event-' + event + selector] = handler;
+ } else {
+ handler = selector;
+ capture = fn;
+ }
+
+ node.addEventListener(event, handler, capture || false);
+ });
+ }
+
+ /**
+ * Removes an event listener for the specified events.
+ *
+ * @param {!Node} node
+ * @param {string} events
+ * @param {string} [selector]
+ * @param {function(Object)} fn
+ * @param {boolean} [capture=false]
+ * @see on()
+ */
+ // eslint-disable-next-line max-params
+ function off(node, events, selector, fn, capture) {
+ events.split(' ').forEach(function (event) {
+ var handler;
+
+ if (isString(selector)) {
+ handler = fn['_sce-event-' + event + selector];
+ } else {
+ handler = selector;
+ capture = fn;
+ }
+
+ node.removeEventListener(event, handler, capture || false);
+ });
+ }
+
+ /**
+ * If only attr param is specified it will get
+ * the value of the attr param.
+ *
+ * If value is specified but null the attribute
+ * will be removed otherwise the attr value will
+ * be set to the passed value.
+ *
+ * @param {!HTMLElement} node
+ * @param {!string} attr
+ * @param {?string} [value]
+ */
+ function attr(node, attr, value) {
+ if (arguments.length < 3) {
+ return node.getAttribute(attr);
+ }
+
+ // eslint-disable-next-line eqeqeq, no-eq-null
+ if (value == null) {
+ removeAttr(node, attr);
+ } else {
+ node.setAttribute(attr, value);
+ }
+ }
+
+ /**
+ * Removes the specified attribute
+ *
+ * @param {!HTMLElement} node
+ * @param {!string} attr
+ */
+ function removeAttr(node, attr) {
+ node.removeAttribute(attr);
+ }
+
+ /**
+ * Sets the passed elements display to none
+ *
+ * @param {!HTMLElement} node
+ */
+ function hide(node) {
+ css(node, 'display', 'none');
+ }
+
+ /**
+ * Sets the passed elements display to default
+ *
+ * @param {!HTMLElement} node
+ */
+ function show(node) {
+ css(node, 'display', '');
+ }
+
+ /**
+ * Toggles an elements visibility
+ *
+ * @param {!HTMLElement} node
+ */
+ function toggle(node) {
+ if (isVisible(node)) {
+ hide(node);
+ } else {
+ show(node);
+ }
+ }
+
+ /**
+ * Gets a computed CSS values or sets an inline CSS value
+ *
+ * Rules should be in camelCase format and not
+ * hyphenated like CSS properties.
+ *
+ * @param {!HTMLElement} node
+ * @param {!Object|string} rule
+ * @param {string|number} [value]
+ * @return {string|number|undefined}
+ */
+ function css(node, rule, value) {
+ if (arguments.length < 3) {
+ if (isString(rule)) {
+ return node.nodeType === 1 ? getComputedStyle(node)[rule] : null;
+ }
+
+ each(rule, function (key, value) {
+ css(node, key, value);
+ });
+ } else {
+ // isNaN returns false for null, false and empty strings
+ // so need to check it's truthy or 0
+ var isNumeric = (value || value === 0) && !isNaN(value);
+ node.style[rule] = isNumeric ? value + 'px' : value;
+ }
+ }
+
+
+ /**
+ * Gets or sets thee data attributes on a node
+ *
+ * Unlike the jQuery version this only stores data
+ * in the DOM attributes which means only strings
+ * can be stored.
+ *
+ * @param {Node} node
+ * @param {string} [key]
+ * @param {string} [value]
+ * @return {Object|undefined}
+ */
+ function data(node, key, value) {
+ var argsLength = arguments.length;
+ var data = {};
+
+ if (node.nodeType === ELEMENT_NODE) {
+ if (argsLength === 1) {
+ each(node.attributes, function (_, attr) {
+ if (/^data\-/i.test(attr.name)) {
+ data[attr.name.substr(5)] = attr.value;
+ }
+ });
+
+ return data;
+ }
+
+ if (argsLength === 2) {
+ return attr(node, 'data-' + key);
+ }
+
+ attr(node, 'data-' + key, String(value));
+ }
+ }
+
+ /**
+ * Checks if node matches the given selector.
+ *
+ * @param {?HTMLElement} node
+ * @param {string} selector
+ * @returns {boolean}
+ */
+ function is(node, selector) {
+ var result = false;
+
+ if (node && node.nodeType === ELEMENT_NODE) {
+ result = (node.matches || node.msMatchesSelector ||
+ node.webkitMatchesSelector).call(node, selector);
+ }
+
+ return result;
+ }
+
+
+ /**
+ * Returns true if node contains child otherwise false.
+ *
+ * This differs from the DOM contains() method in that
+ * if node and child are equal this will return false.
+ *
+ * @param {!Node} node
+ * @param {HTMLElement} child
+ * @returns {boolean}
+ */
+ function contains(node, child) {
+ return node !== child && node.contains && node.contains(child);
+ }
+
+ /**
+ * @param {Node} node
+ * @param {string} [selector]
+ * @returns {?HTMLElement}
+ */
+ function previousElementSibling(node, selector) {
+ var prev = node.previousElementSibling;
+
+ if (selector && prev) {
+ return is(prev, selector) ? prev : null;
+ }
+
+ return prev;
+ }
+
+ /**
+ * @param {!Node} node
+ * @param {!Node} refNode
+ * @returns {Node}
+ */
+ function insertBefore(node, refNode) {
+ return refNode.parentNode.insertBefore(node, refNode);
+ }
+
+ /**
+ * @param {?HTMLElement} node
+ * @returns {!Array.}
+ */
+ function classes(node) {
+ return node.className.trim().split(/\s+/);
+ }
+
+ /**
+ * @param {?HTMLElement} node
+ * @param {string} className
+ * @returns {boolean}
+ */
+ function hasClass(node, className) {
+ return is(node, '.' + className);
+ }
+
+ /**
+ * @param {!HTMLElement} node
+ * @param {string} className
+ */
+ function addClass(node, className) {
+ var classList = classes(node);
+
+ if (classList.indexOf(className) < 0) {
+ classList.push(className);
+ }
+
+ node.className = classList.join(' ');
+ }
+
+ /**
+ * @param {!HTMLElement} node
+ * @param {string} className
+ */
+ function removeClass(node, className) {
+ var classList = classes(node);
+
+ arrayRemove(classList, className);
+
+ node.className = classList.join(' ');
+ }
+
+ /**
+ * Toggles a class on node.
+ *
+ * If state is specified and is truthy it will add
+ * the class.
+ *
+ * If state is specified and is falsey it will remove
+ * the class.
+ *
+ * @param {HTMLElement} node
+ * @param {string} className
+ * @param {boolean} [state]
+ */
+ function toggleClass(node, className, state) {
+ state = isUndefined(state) ? !hasClass(node, className) : state;
+
+ if (state) {
+ addClass(node, className);
+ } else {
+ removeClass(node, className);
+ }
+ }
+
+ /**
+ * Gets or sets the width of the passed node.
+ *
+ * @param {HTMLElement} node
+ * @param {number|string} [value]
+ * @returns {number|undefined}
+ */
+ function width(node, value) {
+ if (isUndefined(value)) {
+ var cs = getComputedStyle(node);
+ var padding = toFloat(cs.paddingLeft) + toFloat(cs.paddingRight);
+ var border = toFloat(cs.borderLeftWidth) + toFloat(cs.borderRightWidth);
+
+ return node.offsetWidth - padding - border;
+ }
+
+ css(node, 'width', value);
+ }
+
+ /**
+ * Gets or sets the height of the passed node.
+ *
+ * @param {HTMLElement} node
+ * @param {number|string} [value]
+ * @returns {number|undefined}
+ */
+ function height(node, value) {
+ if (isUndefined(value)) {
+ var cs = getComputedStyle(node);
+ var padding = toFloat(cs.paddingTop) + toFloat(cs.paddingBottom);
+ var border = toFloat(cs.borderTopWidth) + toFloat(cs.borderBottomWidth);
+
+ return node.offsetHeight - padding - border;
+ }
+
+ css(node, 'height', value);
+ }
+
+ /**
+ * Triggers a custom event with the specified name and
+ * sets the detail property to the data object passed.
+ *
+ * @param {HTMLElement} node
+ * @param {string} eventName
+ * @param {Object} [data]
+ */
+ function trigger(node, eventName, data) {
+ var event;
+
+ if (isFunction(window.CustomEvent)) {
+ event = new CustomEvent(eventName, {
+ bubbles: true,
+ cancelable: true,
+ detail: data
+ });
+ } else {
+ event = node.ownerDocument.createEvent('CustomEvent');
+ event.initCustomEvent(eventName, true, true, data);
+ }
+
+ node.dispatchEvent(event);
+ }
+
+ /**
+ * Returns if a node is visible.
+ *
+ * @param {HTMLElement}
+ * @returns {boolean}
+ */
+ function isVisible(node) {
+ return !!node.getClientRects().length;
+ }
+
+ /**
+ * Convert CSS property names into camel case
+ *
+ * @param {string} string
+ * @returns {string}
+ */
+ function camelCase(string) {
+ return string
+ .replace(/^-ms-/, 'ms-')
+ .replace(/-(\w)/g, function (match, char) {
+ return char.toUpperCase();
+ });
+ }
+
+
+ /**
+ * Loop all child nodes of the passed node
+ *
+ * The function should accept 1 parameter being the node.
+ * If the function returns false the loop will be exited.
+ *
+ * @param {HTMLElement} node
+ * @param {function} func Callback which is called with every
+ * child node as the first argument.
+ * @param {boolean} innermostFirst If the innermost node should be passed
+ * to the function before it's parents.
+ * @param {boolean} siblingsOnly If to only traverse the nodes siblings
+ * @param {boolean} [reverse=false] If to traverse the nodes in reverse
+ */
+ // eslint-disable-next-line max-params
+ function traverse(node, func, innermostFirst, siblingsOnly, reverse) {
+ node = reverse ? node.lastChild : node.firstChild;
+
+ while (node) {
+ var next = reverse ? node.previousSibling : node.nextSibling;
+
+ if (
+ (!innermostFirst && func(node) === false) ||
+ (!siblingsOnly && traverse(
+ node, func, innermostFirst, siblingsOnly, reverse
+ ) === false) ||
+ (innermostFirst && func(node) === false)
+ ) {
+ return false;
+ }
+
+ node = next;
+ }
+ }
+
+ /**
+ * Like traverse but loops in reverse
+ * @see traverse
+ */
+ function rTraverse(node, func, innermostFirst, siblingsOnly) {
+ traverse(node, func, innermostFirst, siblingsOnly, true);
+ }
+
+ /**
+ * Parses HTML into a document fragment
+ *
+ * @param {string} html
+ * @param {Document} [context]
+ * @since 1.4.4
+ * @return {DocumentFragment}
+ */
+ function parseHTML(html, context) {
+ context = context || document;
+
+ var ret = context.createDocumentFragment();
+ var tmp = createElement('div', {}, context);
+
+ tmp.innerHTML = html;
+
+ while (tmp.firstChild) {
+ appendChild(ret, tmp.firstChild);
+ }
+
+ return ret;
+ }
+
+ /**
+ * Checks if an element has any styling.
+ *
+ * It has styling if it is not a plain or
or
+ * if it has a class, style attribute or data.
+ *
+ * @param {HTMLElement} elm
+ * @return {boolean}
+ * @since 1.4.4
+ */
+ function hasStyling(node) {
+ return node && (!is(node, 'p,div') || node.className ||
+ attr(node, 'style') || !isEmptyObject(data(node)));
+ }
+
+ /**
+ * Converts an element from one type to another.
+ *
+ * For example it can convert the element to
+ *
+ * @param {HTMLElement} element
+ * @param {string} toTagName
+ * @return {HTMLElement}
+ * @since 1.4.4
+ */
+ function convertElement(element, toTagName) {
+ var newElement = createElement(toTagName, {}, element.ownerDocument);
+
+ each(element.attributes, function (_, attribute) {
+ // Some browsers parse invalid attributes names like
+ // 'size"2' which throw an exception when set, just
+ // ignore these.
+ try {
+ attr(newElement, attribute.name, attribute.value);
+ } catch (ex) {}
+ });
+
+ while (element.firstChild) {
+ appendChild(newElement, element.firstChild);
+ }
+
+ element.parentNode.replaceChild(newElement, element);
+
+ return newElement;
+ }
+
+ /**
+ * List of block level elements separated by bars (|)
+ *
+ * @type {string}
+ */
+ var blockLevelList = '|body|hr|p|div|h1|h2|h3|h4|h5|h6|address|pre|' +
+ 'form|table|tbody|thead|tfoot|th|tr|td|li|ol|ul|blockquote|center|';
+
+ /**
+ * List of elements that do not allow children separated by bars (|)
+ *
+ * @param {Node} node
+ * @return {boolean}
+ * @since 1.4.5
+ */
+ function canHaveChildren(node) {
+ // 1 = Element
+ // 9 = Document
+ // 11 = Document Fragment
+ if (!/11?|9/.test(node.nodeType)) {
+ return false;
+ }
+
+ // List of empty HTML tags separated by bar (|) character.
+ // Source: http://www.w3.org/TR/html4/index/elements.html
+ // Source: http://www.w3.org/TR/html5/syntax.html#void-elements
+ return ('|iframe|area|base|basefont|br|col|frame|hr|img|input|wbr' +
+ '|isindex|link|meta|param|command|embed|keygen|source|track|' +
+ 'object|').indexOf('|' + node.nodeName.toLowerCase() + '|') < 0;
+ }
+
+ /**
+ * Checks if an element is inline
+ *
+ * @param {HTMLElement} elm
+ * @param {boolean} [includeCodeAsBlock=false]
+ * @return {boolean}
+ */
+ function isInline(elm, includeCodeAsBlock) {
+ var tagName,
+ nodeType = (elm || {}).nodeType || TEXT_NODE;
+
+ if (nodeType !== ELEMENT_NODE) {
+ return nodeType === TEXT_NODE;
+ }
+
+ tagName = elm.tagName.toLowerCase();
+
+ if (tagName === 'code') {
+ return !includeCodeAsBlock;
+ }
+
+ return blockLevelList.indexOf('|' + tagName + '|') < 0;
+ }
+
+ /**
+ * Copy the CSS from 1 node to another.
+ *
+ * Only copies CSS defined on the element e.g. style attr.
+ *
+ * @param {HTMLElement} from
+ * @param {HTMLElement} to
+ */
+ function copyCSS(from, to) {
+ to.style.cssText = from.style.cssText + to.style.cssText;
+ }
+
+ /**
+ * Fixes block level elements inside in inline elements.
+ *
+ * Also fixes invalid list nesting by placing nested lists
+ * inside the previous li tag or wrapping them in an li tag.
+ *
+ * @param {HTMLElement} node
+ */
+ function fixNesting(node) {
+ var getLastInlineParent = function (node) {
+ while (isInline(node.parentNode, true)) {
+ node = node.parentNode;
+ }
+
+ return node;
+ };
+
+ traverse(node, function (node) {
+ var list = 'ul,ol',
+ isBlock = !isInline(node, true);
+
+ // Any blocklevel element inside an inline element needs fixing.
+ if (isBlock && isInline(node.parentNode, true)) {
+ var parent = getLastInlineParent(node),
+ before = extractContents(parent, node),
+ middle = node;
+
+ // copy current styling so when moved out of the parent
+ // it still has the same styling
+ copyCSS(parent, middle);
+
+ insertBefore(before, parent);
+ insertBefore(middle, parent);
+ }
+
+ // Fix invalid nested lists which should be wrapped in an li tag
+ if (isBlock && is(node, list) && is(node.parentNode, list)) {
+ var li = previousElementSibling(node, 'li');
+
+ if (!li) {
+ li = createElement('li');
+ insertBefore(li, node);
+ }
+
+ appendChild(li, node);
+ }
+ });
+ }
+
+ /**
+ * Finds the common parent of two nodes
+ *
+ * @param {!HTMLElement} node1
+ * @param {!HTMLElement} node2
+ * @return {?HTMLElement}
+ */
+ function findCommonAncestor(node1, node2) {
+ while ((node1 = node1.parentNode)) {
+ if (contains(node1, node2)) {
+ return node1;
+ }
+ }
+ }
+
+ /**
+ * @param {?Node}
+ * @param {boolean} [previous=false]
+ * @returns {?Node}
+ */
+ function getSibling(node, previous) {
+ if (!node) {
+ return null;
+ }
+
+ return (previous ? node.previousSibling : node.nextSibling) ||
+ getSibling(node.parentNode, previous);
+ }
+
+ /**
+ * Removes unused whitespace from the root and all it's children.
+ *
+ * @param {!HTMLElement} root
+ * @since 1.4.3
+ */
+ function removeWhiteSpace(root) {
+ var nodeValue, nodeType, next, previous, previousSibling,
+ nextNode, trimStart,
+ cssWhiteSpace = css(root, 'whiteSpace'),
+ // Preserve newlines if is pre-line
+ preserveNewLines = /line$/i.test(cssWhiteSpace),
+ node = root.firstChild;
+
+ // Skip pre & pre-wrap with any vendor prefix
+ if (/pre(\-wrap)?$/i.test(cssWhiteSpace)) {
+ return;
+ }
+
+ while (node) {
+ nextNode = node.nextSibling;
+ nodeValue = node.nodeValue;
+ nodeType = node.nodeType;
+
+ if (nodeType === ELEMENT_NODE && node.firstChild) {
+ removeWhiteSpace(node);
+ }
+
+ if (nodeType === TEXT_NODE) {
+ next = getSibling(node);
+ previous = getSibling(node, true);
+ trimStart = false;
+
+ while (hasClass(previous, 'sceditor-ignore')) {
+ previous = getSibling(previous, true);
+ }
+
+ // If previous sibling isn't inline or is a textnode that
+ // ends in whitespace, time the start whitespace
+ if (isInline(node) && previous) {
+ previousSibling = previous;
+
+ while (previousSibling.lastChild) {
+ previousSibling = previousSibling.lastChild;
+
+ // eslint-disable-next-line max-depth
+ while (hasClass(previousSibling, 'sceditor-ignore')) {
+ previousSibling = getSibling(previousSibling, true);
+ }
+ }
+
+ trimStart = previousSibling.nodeType === TEXT_NODE ?
+ /[\t\n\r ]$/.test(previousSibling.nodeValue) :
+ !isInline(previousSibling);
+ }
+
+ // Clear zero width spaces
+ nodeValue = nodeValue.replace(/\u200B/g, '');
+
+ // Strip leading whitespace
+ if (!previous || !isInline(previous) || trimStart) {
+ nodeValue = nodeValue.replace(
+ preserveNewLines ? /^[\t ]+/ : /^[\t\n\r ]+/,
+ ''
+ );
+ }
+
+ // Strip trailing whitespace
+ if (!next || !isInline(next)) {
+ nodeValue = nodeValue.replace(
+ preserveNewLines ? /[\t ]+$/ : /[\t\n\r ]+$/,
+ ''
+ );
+ }
+
+ // Remove empty text nodes
+ if (!nodeValue.length) {
+ remove(node);
+ } else {
+ node.nodeValue = nodeValue.replace(
+ preserveNewLines ? /[\t ]+/g : /[\t\n\r ]+/g,
+ ' '
+ );
+ }
+ }
+
+ node = nextNode;
+ }
+ }
+
+ /**
+ * Extracts all the nodes between the start and end nodes
+ *
+ * @param {HTMLElement} startNode The node to start extracting at
+ * @param {HTMLElement} endNode The node to stop extracting at
+ * @return {DocumentFragment}
+ */
+ function extractContents(startNode, endNode) {
+ var range = startNode.ownerDocument.createRange();
+
+ range.setStartBefore(startNode);
+ range.setEndAfter(endNode);
+
+ return range.extractContents();
+ }
+
+ /**
+ * Gets the offset position of an element
+ *
+ * @param {HTMLElement} node
+ * @return {Object} An object with left and top properties
+ */
+ function getOffset(node) {
+ var left = 0,
+ top = 0;
+
+ while (node) {
+ left += node.offsetLeft;
+ top += node.offsetTop;
+ node = node.offsetParent;
+ }
+
+ return {
+ left: left,
+ top: top
+ };
+ }
+
+ /**
+ * Gets the value of a CSS property from the elements style attribute
+ *
+ * @param {HTMLElement} elm
+ * @param {string} property
+ * @return {string}
+ */
+ function getStyle(elm, property) {
+ var direction, styleValue,
+ elmStyle = elm.style;
+
+ if (!cssPropertyNameCache[property]) {
+ cssPropertyNameCache[property] = camelCase(property);
+ }
+
+ property = cssPropertyNameCache[property];
+ styleValue = elmStyle[property];
+
+ // Add an exception for text-align
+ if ('textAlign' === property) {
+ direction = elmStyle.direction;
+ styleValue = styleValue || css(elm, property);
+
+ if (css(elm.parentNode, property) === styleValue ||
+ css(elm, 'display') !== 'block' || is(elm, 'hr,th')) {
+ return '';
+ }
+
+ // IE changes text-align to the same as the current direction
+ // so skip unless its not the same
+ if ((/right/i.test(styleValue) && direction === 'rtl') ||
+ (/left/i.test(styleValue) && direction === 'ltr')) {
+ return '';
+ }
+ }
+
+ return styleValue;
+ }
+
+ /**
+ * Tests if an element has a style.
+ *
+ * If values are specified it will check that the styles value
+ * matches one of the values
+ *
+ * @param {HTMLElement} elm
+ * @param {string} property
+ * @param {string|array} [values]
+ * @return {boolean}
+ */
+ function hasStyle(elm, property, values) {
+ var styleValue = getStyle(elm, property);
+
+ if (!styleValue) {
+ return false;
+ }
+
+ return !values || styleValue === values ||
+ (Array.isArray(values) && values.indexOf(styleValue) > -1);
+ }
+
+ /**
+ * Default options for SCEditor
+ * @type {Object}
+ */
+ var defaultOptions = {
+ /** @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,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: '#000000,#44B8FF,#1E92F7,#0074D9,#005DC2,#00369B,#b3d5f4|' +
+ '#444444,#C3FFFF,#9DF9FF,#7FDBFF,#68C4E8,#419DC1,#d9f4ff|' +
+ '#666666,#72FF84,#4CEA5E,#2ECC40,#17B529,#008E02,#c0f0c6|' +
+ '#888888,#FFFF44,#FFFA1E,#FFDC00,#E8C500,#C19E00,#fff5b3|' +
+ '#aaaaaa,#FFC95F,#FFA339,#FF851B,#E86E04,#C14700,#ffdbbb|' +
+ '#cccccc,#FF857A,#FF5F54,#FF4136,#E82A1F,#C10300,#ffc6c3|' +
+ '#eeeeee,#FF56FF,#FF30DC,#F012BE,#D900A7,#B20080,#fbb8ec|' +
+ '#ffffff,#F551FF,#CF2BE7,#B10DC9,#9A00B2,#9A00B2,#e8b6ef',
+
+ /**
+ * The locale to use.
+ * @type {string}
+ */
+ locale: attr(document.documentElement, '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 {?number}
+ */
+ width: null,
+
+ /**
+ * Height of the editor including toolbar. Set to null for automatic
+ * height
+ *
+ * @type {?number}
+ */
+ 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 {?number}
+ */
+ resizeMinWidth: null,
+ /**
+ * Min resize to height, set to null for half textarea height or -1 for
+ * unlimited
+ *
+ * @type {?number}
+ */
+ resizeMinHeight: null,
+ /**
+ * Max resize to height, set to null for double textarea height or -1
+ * for unlimited
+ *
+ * @type {?number}
+ */
+ resizeMaxHeight: null,
+ /**
+ * Max resize to width, set to null for double textarea width or -1 for
+ * unlimited
+ *
+ * @type {?number}
+ */
+ 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 {?number}
+ */
+ 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: { }
+ };
+
+ var USER_AGENT = navigator.userAgent;
+
+ /**
+ * Detects the version of IE is being used if any.
+ *
+ * Will be the IE version number or undefined if the
+ * browser is not IE.
+ *
+ * Source: https://gist.github.com/527683 with extra code
+ * for IE 10 & 11 detection.
+ *
+ * @function
+ * @name ie
+ * @type {number}
+ */
+ var ie = (function () {
+ var undef,
+ v = 3,
+ doc = document,
+ div = doc.createElement('div'),
+ all = div.getElementsByTagName('i');
+
+ do {
+ div.innerHTML = '';
+ } while (all[0]);
+
+ // Detect IE 10 as it doesn't support conditional comments.
+ if ((doc.documentMode && doc.all && window.atob)) {
+ v = 10;
+ }
+
+ // Detect IE 11
+ if (v === 4 && doc.documentMode) {
+ v = 11;
+ }
+
+ return v > 4 ? v : undef;
+ }());
+
+ var edge = '-ms-ime-align' in document.documentElement.style;
+
+ /**
+ * Detects if the browser is iOS
+ *
+ * Needed to fix iOS specific bugs
+ *
+ * @function
+ * @name ios
+ * @memberOf jQuery.sceditor
+ * @type {boolean}
+ */
+ var ios = /iPhone|iPod|iPad| wosbrowser\//i.test(USER_AGENT);
+
+ /**
+ * If the browser supports WYSIWYG editing (e.g. older mobile browsers).
+ *
+ * @function
+ * @name isWysiwygSupported
+ * @return {boolean}
+ */
+ var isWysiwygSupported = (function () {
+ var match, isUnsupported;
+
+ var div = document.createElement('div');
+ div.contentEditable = true ;
+
+ // Check if the contentEditable attribute is supported
+ if (!('contentEditable' in document.documentElement) ||
+ div.contentEditable !== 'true') {
+ return false;
+ }
+
+ // I think blackberry supports contentEditable or will at least
+ // give a valid value for the contentEditable detection above
+ // so it isn't included in the below tests.
+
+ // I hate having to do UA sniffing but some mobile browsers say they
+ // support contentediable when it isn't usable, i.e. you can't enter
+ // text.
+ // This is the only way I can think of to detect them which is also how
+ // every other editor I've seen deals with this issue.
+
+ // Exclude Opera mobile and mini
+ isUnsupported = /Opera Mobi|Opera Mini/i.test(USER_AGENT);
+
+ if (/Android/i.test(USER_AGENT)) {
+ isUnsupported = true;
+
+ if (/Safari/.test(USER_AGENT)) {
+ // Android browser 534+ supports content editable
+ // This also matches Chrome which supports content editable too
+ match = /Safari\/(\d+)/.exec(USER_AGENT);
+ isUnsupported = (!match || !match[1] ? true : match[1] < 534);
+ }
+ }
+
+ // The current version of Amazon Silk supports it, older versions didn't
+ // As it uses webkit like Android, assume it's the same and started
+ // working at versions >= 534
+ if (/ Silk\//i.test(USER_AGENT)) {
+ match = /AppleWebKit\/(\d+)/.exec(USER_AGENT);
+ isUnsupported = (!match || !match[1] ? true : match[1] < 534);
+ }
+
+ // iOS 5+ supports content editable
+ if (ios) {
+ // Block any version <= 4_x(_x)
+ isUnsupported = /OS [0-4](_\d)+ like Mac/i.test(USER_AGENT);
+ }
+
+ // Firefox does support WYSIWYG on mobiles so override
+ // any previous value if using FF
+ if (/Firefox/i.test(USER_AGENT)) {
+ isUnsupported = false;
+ }
+
+ if (/OneBrowser/i.test(USER_AGENT)) {
+ isUnsupported = false;
+ }
+
+ // UCBrowser works but doesn't give a unique user agent
+ if (navigator.vendor === 'UCWEB') {
+ isUnsupported = false;
+ }
+
+ // IE <= 9 is not supported any more
+ if (ie <= 9) {
+ isUnsupported = true;
+ }
+
+ return !isUnsupported;
+ }());
+
+ // Must start with a valid scheme
+ // ^
+ // Schemes that are considered safe
+ // (https?|s?ftp|mailto|spotify|skype|ssh|teamspeak|tel):|
+ // Relative schemes (//:) are considered safe
+ // (\\/\\/)|
+ // Image data URI's are considered safe
+ // data:image\\/(png|bmp|gif|p?jpe?g);
+ var VALID_SCHEME_REGEX =
+ /^(https?|s?ftp|mailto|spotify|skype|ssh|teamspeak|tel):|(\/\/)|data:image\/(png|bmp|gif|p?jpe?g);/i;
+
+ /**
+ * Escapes a string so it's safe to use in regex
+ *
+ * @param {string} str
+ * @return {string}
+ */
+ function regex(str) {
+ return str.replace(/([\-.*+?^=!:${}()|\[\]\/\\])/g, '\\$1');
+ }
+
+ /**
+ * Escapes all HTML entities in a string
+ *
+ * If noQuotes is set to false, all single and double
+ * quotes will also be escaped
+ *
+ * @param {string} str
+ * @param {boolean} [noQuotes=true]
+ * @return {string}
+ * @since 1.4.1
+ */
+ function entities(str, noQuotes) {
+ if (!str) {
+ return str;
+ }
+
+ var replacements = {
+ '&': '&',
+ '<': '<',
+ '>': '>',
+ ' ': ' ',
+ '\r\n': ' ',
+ '\r': ' ',
+ '\n': ' '
+ };
+
+ if (noQuotes !== false) {
+ replacements['"'] = '"';
+ replacements['\''] = ''';
+ replacements['`'] = '`';
+ }
+
+ str = str.replace(/ {2}|\r\n|[&<>\r\n'"`]/g, function (match) {
+ return replacements[match] || match;
+ });
+
+ return str;
+ }
+
+ /**
+ * Escape URI scheme.
+ *
+ * Appends the current URL to a url if it has a scheme that is not:
+ *
+ * http
+ * https
+ * sftp
+ * ftp
+ * mailto
+ * spotify
+ * skype
+ * ssh
+ * teamspeak
+ * tel
+ * //
+ * data:image/(png|jpeg|jpg|pjpeg|bmp|gif);
+ *
+ * **IMPORTANT**: This does not escape any HTML in a url, for
+ * that use the escape.entities() method.
+ *
+ * @param {string} url
+ * @return {string}
+ * @since 1.4.5
+ */
+ function uriScheme(url) {
+ var path,
+ // If there is a : before a / then it has a scheme
+ hasScheme = /^[^\/]*:/i,
+ location = window.location;
+
+ // Has no scheme or a valid scheme
+ if ((!url || !hasScheme.test(url)) || VALID_SCHEME_REGEX.test(url)) {
+ return url;
+ }
+
+ path = location.pathname.split('/');
+ path.pop();
+
+ return location.protocol + '//' +
+ location.host +
+ path.join('/') + '/' +
+ url;
+ }
+
+ /**
+ * HTML templates used by the editor and default commands
+ * @type {Object}
+ * @private
+ */
+ var _templates = {
+ html:
+ '' +
+ '' +
+ '' +
+ '' +
+ ' ' +
+ ' ' +
+ '' +
+ '
' +
+ '',
+
+ toolbarButton: '' +
+ '{dispName}
',
+
+ emoticon: ' ',
+
+ fontOpt: '{font} ',
+
+ sizeOpt: '{size} ',
+
+ pastetext:
+ '{label} ' +
+ '
' +
+ ' ' +
+ '
',
+
+ table:
+ '{rows}
' +
+ '{cols}
' +
+ '
',
+
+ image:
+ '{url} ' +
+ '
' +
+ '{width} ' +
+ '
' +
+ '{height} ' +
+ '
' +
+ ' ' +
+ '
',
+
+ email:
+ '{label} ' +
+ '
' +
+ '{desc} ' +
+ '
' +
+ ' ' +
+ '
',
+
+ link:
+ '{url} ' +
+ '
' +
+ '{desc} ' +
+ '
' +
+ '
',
+
+ youtubeMenu:
+ '{label} ' +
+ '
' +
+ ' ' +
+ '
',
+
+ youtube:
+ ''
+ };
+
+ /**
+ * Replaces any params in a template with the passed params.
+ *
+ * If createHtml is passed it will return a DocumentFragment
+ * containing the parsed template.
+ *
+ * @param {string} name
+ * @param {Object} [params]
+ * @param {boolean} [createHtml]
+ * @returns {string|DocumentFragment}
+ * @private
+ */
+ function _tmpl (name, params, createHtml) {
+ var template = _templates[name];
+
+ Object.keys(params).forEach(function (name) {
+ template = template.replace(
+ new RegExp(regex('{' + name + '}'), 'g'), params[name]
+ );
+ });
+
+ if (createHtml) {
+ template = parseHTML(template);
+ }
+
+ return template;
+ }
+
+ // 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 && ie < 11;
+
+ /**
+ * Fixes a bug in FF where it sometimes wraps
+ * new lines in their own list item.
+ * See issue #359
+ */
+ function fixFirefoxListBug(editor) {
+ // Only apply to Firefox as will break other browsers.
+ if ('mozHidden' in document) {
+ var node = editor.getBody();
+ var next;
+
+ while (node) {
+ next = node;
+
+ if (next.firstChild) {
+ next = next.firstChild;
+ } else {
+
+ while (next && !next.nextSibling) {
+ next = next.parentNode;
+ }
+
+ if (next) {
+ next = next.nextSibling;
+ }
+ }
+
+ if (node.nodeType === 3 && /[\n\r\t]+/.test(node.nodeValue)) {
+ // Only remove if newlines are collapsed
+ if (!/^pre/.test(css(node.parentNode, 'whiteSpace'))) {
+ remove(node);
+ }
+ }
+
+ node = next;
+ }
+ }
+ }
+
+
+ /**
+ * Map of all the commands for SCEditor
+ * @type {Object}
+ * @name commands
+ * @memberOf jQuery.sceditor
+ */
+ var defaultCmds = {
+ // 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: {
+ state: function (node) {
+ if (node && node.nodeType === 3) {
+ node = node.parentNode;
+ }
+
+ if (node) {
+ var isLtr = css(node, 'direction') === 'ltr';
+ var align = css(node, 'textAlign');
+
+ return align === 'left' || align === (isLtr ? 'start' : 'end');
+ }
+ },
+ exec: 'justifyleft',
+ tooltip: 'Align left'
+ },
+ // END_COMMAND
+ // START_COMMAND: Centre
+ center: {
+ exec: 'justifycenter',
+ tooltip: 'Center'
+ },
+ // END_COMMAND
+ // START_COMMAND: Right
+ right: {
+ state: function (node) {
+ if (node && node.nodeType === 3) {
+ node = node.parentNode;
+ }
+
+ if (node) {
+ var isLtr = css(node, 'direction') === 'ltr';
+ var align = css(node, 'textAlign');
+
+ return align === 'right' || align === (isLtr ? 'end' : 'start');
+ }
+ },
+ 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 content = createElement('div');
+
+ on(content, 'click', 'a', function (e) {
+ callback(data(this, 'font'));
+ editor.closeDropDown(true);
+ e.preventDefault();
+ });
+
+ editor.opts.fonts.split(',').forEach(function (font) {
+ appendChild(content, _tmpl('fontOpt', {
+ font: font
+ }, true));
+ });
+
+ editor.createDropDown(caller, 'font-picker', content);
+ },
+ exec: function (caller) {
+ var editor = this;
+
+ defaultCmds.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 = createElement('div');
+
+ on(content, 'click', 'a', function (e) {
+ callback(data(this, 'size'));
+ editor.closeDropDown(true);
+ e.preventDefault();
+ });
+
+ for (var i = 1; i <= 7; i++) {
+ appendChild(content, _tmpl('sizeOpt', {
+ size: i
+ }, true));
+ }
+
+ editor.createDropDown(caller, 'fontsize-picker', content);
+ },
+ exec: function (caller) {
+ var editor = this;
+
+ defaultCmds.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 content = createElement('div'),
+ html = '',
+ cmd = defaultCmds.color;
+
+ if (!cmd._htmlCache) {
+ editor.opts.colors.split('|').forEach(function (column) {
+ html += '';
+
+ column.split(',').forEach(function (color) {
+ html +=
+ '
';
+ });
+
+ html += '
';
+ });
+
+ cmd._htmlCache = html;
+ }
+
+ appendChild(content, parseHTML(cmd._htmlCache));
+
+ on(content, 'click', 'a', function (e) {
+ callback(data(this, 'color'));
+ editor.closeDropDown(true);
+ e.preventDefault();
+ });
+
+ editor.createDropDown(caller, 'color-picker', content);
+ },
+ exec: function (caller) {
+ var editor = this;
+
+ defaultCmds.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 = createElement('div'),
+ editor = this;
+
+ appendChild(content, _tmpl('pastetext', {
+ label: editor._(
+ 'Paste your text inside the following box:'
+ ),
+ insert: editor._('Insert')
+ }, true));
+
+ on(content, 'click', '.button', function (e) {
+ val = find(content, '#txt')[0].value;
+
+ 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: function () {
+ fixFirefoxListBug(this);
+ this.execCommand('insertunorderedlist');
+ },
+ tooltip: 'Bullet list'
+ },
+ // END_COMMAND
+ // START_COMMAND: Ordered List
+ orderedlist: {
+ exec: function () {
+ fixFirefoxListBug(this);
+ this.execCommand('insertorderedlist');
+ },
+ tooltip: 'Numbered list'
+ },
+ // END_COMMAND
+ // START_COMMAND: Indent
+ indent: {
+ state: function (parent$$1, firstBlock) {
+ // Only works with lists, for now
+ var range, startParent, endParent;
+
+ if (is(firstBlock, 'li')) {
+ return 0;
+ }
+
+ if (is(firstBlock, '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();
+
+ 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
+ (is(endParent, 'li') && endParent !==
+ endParent.parentNode.lastElementChild)) {
+ return 0;
+ }
+ }
+
+ return -1;
+ },
+ exec: function () {
+ var editor = this,
+ block = 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 (closest(block, 'ul,ol,menu')) {
+ editor.execCommand('indent');
+ }
+ },
+ tooltip: 'Add indent'
+ },
+ // END_COMMAND
+ // START_COMMAND: Outdent
+ outdent: {
+ state: function (parents$$1, firstBlock) {
+ return closest(firstBlock, 'ul,ol,menu') ? 0 : -1;
+ },
+ exec: function () {
+ var block = this.getRangeHelper().getFirstBlockParent();
+ if (closest(block, 'ul,ol,menu')) {
+ this.execCommand('outdent');
+ }
+ },
+ tooltip: 'Remove one indent'
+ },
+ // END_COMMAND
+
+ // START_COMMAND: Table
+ table: {
+ exec: function (caller) {
+ var editor = this,
+ content = createElement('div');
+
+ appendChild(content, _tmpl('table', {
+ rows: editor._('Rows:'),
+ cols: editor._('Cols:'),
+ insert: editor._('Insert')
+ }, true));
+
+ on(content, 'click', '.button', function (e) {
+ var rows = Number(find(content, '#rows')[0].value),
+ cols = Number(find(content, '#cols')[0].value),
+ html = '';
+
+ if (rows > 0 && cols > 0) {
+ html += Array(rows + 1).join(
+ '' +
+ Array(cols + 1).join(
+ '' + (IE_BR_FIX ? '' : ' ') + ' '
+ ) +
+ ' '
+ );
+
+ html += '
';
+
+ editor.wysiwygEditorInsertHtml(html);
+ editor.closeDropDown(true);
+ e.preventDefault();
+ }
+ });
+
+ editor.createDropDown(caller, 'inserttable', content);
+ },
+ tooltip: 'Insert a table'
+ },
+ // END_COMMAND
+
+ // START_COMMAND: Horizontal Rule
+ horizontalrule: {
+ exec: 'inserthorizontalrule',
+ tooltip: 'Insert a horizontal rule'
+ },
+ // END_COMMAND
+
+ // START_COMMAND: Code
+ code: {
+ exec: function () {
+ this.wysiwygEditorInsertHtml(
+ '',
+ (IE_BR_FIX ? '' : ' ') + '
'
+ );
+ },
+ tooltip: 'Code'
+ },
+ // END_COMMAND
+
+ // START_COMMAND: Image
+ image: {
+ _dropDown: function (editor, caller, selected, cb) {
+ var content = createElement('div');
+
+ appendChild(content, _tmpl('image', {
+ url: editor._('URL:'),
+ width: editor._('Width (optional):'),
+ height: editor._('Height (optional):'),
+ insert: editor._('Insert')
+ }, true));
+
+
+ var urlInput = find(content, '#image')[0];
+
+ urlInput.value = selected;
+
+ on(content, 'click', '.button', function (e) {
+ if (urlInput.value) {
+ cb(
+ urlInput.value,
+ find(content, '#width')[0].value,
+ find(content, '#height')[0].value
+ );
+ }
+
+ editor.closeDropDown(true);
+ e.preventDefault();
+ });
+
+ editor.createDropDown(caller, 'insertimage', content);
+ },
+ exec: function (caller) {
+ var editor = this;
+
+ defaultCmds.image._dropDown(
+ editor,
+ caller,
+ '',
+ function (url, width$$1, height$$1) {
+ var attrs = '';
+
+ if (width$$1) {
+ attrs += ' width="' + width$$1 + '"';
+ }
+
+ if (height$$1) {
+ attrs += ' height="' + height$$1 + '"';
+ }
+
+ editor.wysiwygEditorInsertHtml(
+ ' '
+ );
+ }
+ );
+ },
+ tooltip: 'Insert an image'
+ },
+ // END_COMMAND
+
+ // START_COMMAND: E-mail
+ email: {
+ _dropDown: function (editor, caller, cb) {
+ var content = createElement('div');
+
+ appendChild(content, _tmpl('email', {
+ label: editor._('E-mail:'),
+ desc: editor._('Description (optional):'),
+ insert: editor._('Insert')
+ }, true));
+
+ on(content, 'click', '.button', function (e) {
+ var email = find(content, '#email')[0].value;
+
+ if (email) {
+ cb(email, find(content, '#des')[0].value);
+ }
+
+ editor.closeDropDown(true);
+ e.preventDefault();
+ });
+
+ editor.createDropDown(caller, 'insertemail', content);
+ },
+ exec: function (caller) {
+ var editor = this;
+
+ defaultCmds.email._dropDown(
+ editor,
+ caller,
+ function (email, text) {
+ // needed for IE to reset the last range
+ editor.focus();
+
+ if (!editor.getRangeHelper().selectedHtml() || text) {
+ editor.wysiwygEditorInsertHtml(
+ '' +
+ (text || email) +
+ ' '
+ );
+ } else {
+ editor.execCommand('createlink', 'mailto:' + email);
+ }
+ }
+ );
+ },
+ tooltip: 'Insert an email'
+ },
+ // END_COMMAND
+
+ // START_COMMAND: Link
+ link: {
+ _dropDown: function (editor, caller, cb) {
+ var content = createElement('div');
+
+ appendChild(content, _tmpl('link', {
+ url: editor._('URL:'),
+ desc: editor._('Description (optional):'),
+ ins: editor._('Insert')
+ }, true));
+
+ var linkInput = find(content, '#link')[0];
+
+ function insertUrl(e) {
+ if (linkInput.value) {
+ cb(linkInput.value, find(content, '#des')[0].value);
+ }
+
+ editor.closeDropDown(true);
+ e.preventDefault();
+ }
+
+ on(content, 'click', '.button', insertUrl);
+ on(content, 'keypress', function (e) {
+ // 13 = enter key
+ if (e.which === 13 && linkInput.value) {
+ insertUrl(e);
+ }
+ }, EVENT_CAPTURE);
+
+ editor.createDropDown(caller, 'insertlink', content);
+ },
+ exec: function (caller) {
+ var editor = this;
+
+ defaultCmds.link._dropDown(editor, caller, function (url, text) {
+ // 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 (text || !editor.getRangeHelper().selectedHtml()) {
+ text = text || url;
+
+ editor.wysiwygEditorInsertHtml(
+ '' + text + ' '
+ );
+ } else {
+ editor.execCommand('createlink', url);
+ }
+ });
+ },
+ tooltip: 'Insert a link'
+ },
+ // END_COMMAND
+
+ // START_COMMAND: Unlink
+ unlink: {
+ state: function () {
+ return closest(this.currentNode(), 'a') ? 0 : -1;
+ },
+ exec: function () {
+ var anchor = closest(this.currentNode(), 'a');
+
+ if (anchor) {
+ while (anchor.firstChild) {
+ insertBefore(anchor.firstChild, anchor);
+ }
+
+ remove(anchor);
+ }
+ },
+ tooltip: 'Unlink'
+ },
+ // END_COMMAND
+
+
+ // START_COMMAND: Quote
+ quote: {
+ 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,
+ opts = editor.opts,
+ emoticonsRoot = opts.emoticonsRoot || '',
+ emoticonsCompat = opts.emoticonsCompat,
+ rangeHelper = editor.getRangeHelper(),
+ startSpace = emoticonsCompat &&
+ rangeHelper.getOuterText(true, 1) !== ' ' ? ' ' : '',
+ endSpace = emoticonsCompat &&
+ rangeHelper.getOuterText(false, 1) !== ' ' ? ' ' : '',
+ content = createElement('div'),
+ line = createElement('div'),
+ perLine = 0,
+ emoticons = extend(
+ {},
+ opts.emoticons.dropdown,
+ includeMore ? opts.emoticons.more : {}
+ );
+
+ appendChild(content, line);
+
+ perLine = Math.sqrt(Object.keys(emoticons).length);
+
+ on(content, 'click', 'img', function (e) {
+ editor.insert(startSpace + attr(this, 'alt') + endSpace,
+ null, false).closeDropDown(true);
+
+ e.preventDefault();
+ });
+
+ each(emoticons, function (code, emoticon) {
+ appendChild(line, createElement('img', {
+ src: emoticonsRoot + (emoticon.url || emoticon),
+ alt: code,
+ title: emoticon.tooltip || code
+ }));
+
+ if (line.children.length >= perLine) {
+ line = createElement('div');
+ appendChild(content, line);
+ }
+ });
+
+ if (!includeMore && opts.emoticons.more) {
+ moreLink = createElement('a', {
+ className: 'sceditor-more'
+ });
+
+ appendChild(moreLink,
+ document.createTextNode(editor._('More')));
+
+ on(moreLink, 'click', function (e) {
+ editor.createDropDown(
+ caller, 'more-emoticons', createContent(true)
+ );
+
+ e.preventDefault();
+ });
+
+ appendChild(content, moreLink);
+ }
+
+ return content;
+ };
+
+ editor.createDropDown(caller, 'emoticons', createContent(false));
+ },
+ txtExec: function (caller) {
+ defaultCmds.emoticon.exec.call(this, caller);
+ },
+ tooltip: 'Insert an emoticon'
+ },
+ // END_COMMAND
+
+ // START_COMMAND: YouTube
+ youtube: {
+ _dropDown: function (editor, caller, callback) {
+ var content = createElement('div');
+
+ appendChild(content, _tmpl('youtubeMenu', {
+ label: editor._('Video URL:'),
+ insert: editor._('Insert')
+ }, true));
+
+ on(content, 'click', '.button', function (e) {
+ var val = find(content, '#link')[0].value;
+ var idMatch = val.match(/(?:v=|v\/|embed\/|youtu.be\/)(.{11})/);
+ var timeMatch = val.match(/[&|?](?:star)?t=((\d+[hms]?){1,3})/);
+ var time = 0;
+
+ if (timeMatch) {
+ each(timeMatch[1].split(/[hms]/), function (i, val) {
+ if (val !== '') {
+ time = (time * 60) + Number(val);
+ }
+ });
+ }
+
+ if (idMatch && /^[a-zA-Z0-9_\-]{11}$/.test(idMatch[1])) {
+ callback(idMatch[1], time);
+ }
+
+ editor.closeDropDown(true);
+ e.preventDefault();
+ });
+
+ editor.createDropDown(caller, 'insertlink', content);
+ },
+ exec: function (btn) {
+ var editor = this;
+
+ defaultCmds.youtube._dropDown(editor, btn, function (id, time) {
+ editor.wysiwygEditorInsertHtml(_tmpl('youtube', {
+ id: id,
+ time: time
+ }));
+ });
+ },
+ 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(defaultCmds.date._date(this));
+ },
+ txtExec: function () {
+ this.insertText(defaultCmds.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(defaultCmds.time._time());
+ },
+ txtExec: function () {
+ this.insertText(defaultCmds.time._time());
+ },
+ tooltip: 'Insert current time'
+ },
+ // END_COMMAND
+
+
+ // START_COMMAND: Ltr
+ ltr: {
+ state: function (parents$$1, firstBlock) {
+ return firstBlock && firstBlock.style.direction === 'ltr';
+ },
+ exec: function () {
+ var editor = this,
+ rangeHelper = editor.getRangeHelper(),
+ node = rangeHelper.getFirstBlockParent();
+
+ editor.focus();
+
+ if (!node || is(node, 'body')) {
+ editor.execCommand('formatBlock', 'p');
+
+ node = rangeHelper.getFirstBlockParent();
+
+ if (!node || is(node, 'body')) {
+ return;
+ }
+ }
+
+ var toggleValue = css(node, 'direction') === 'ltr' ? '' : 'ltr';
+ css(node, 'direction', toggleValue);
+ },
+ tooltip: 'Left-to-Right'
+ },
+ // END_COMMAND
+
+ // START_COMMAND: Rtl
+ rtl: {
+ state: function (parents$$1, firstBlock) {
+ return firstBlock && firstBlock.style.direction === 'rtl';
+ },
+ exec: function () {
+ var editor = this,
+ rangeHelper = editor.getRangeHelper(),
+ node = rangeHelper.getFirstBlockParent();
+
+ editor.focus();
+
+ if (!node || is(node, 'body')) {
+ editor.execCommand('formatBlock', 'p');
+
+ node = rangeHelper.getFirstBlockParent();
+
+ if (!node || is(node, 'body')) {
+ return;
+ }
+ }
+
+ var toggleValue = css(node, 'direction') === 'rtl' ? '' : 'rtl';
+ css(node, 'direction', toggleValue);
+ },
+ 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: {}
+ };
+
+ var plugins = {};
+
+ /**
+ * Plugin Manager class
+ * @class PluginManager
+ * @name PluginManager
+ */
+ function PluginManager(thisObj) {
+ /**
+ * Alias of this
+ *
+ * @private
+ * @type {Object}
+ */
+ var base = this;
+
+ /**
+ * Array of all currently registered plugins
+ *
+ * @type {Array}
+ * @private
+ */
+ var registeredPlugins = [];
+
+
+ /**
+ * Changes a signals name from "name" into "signalName".
+ *
+ * @param {string} signal
+ * @return {string}
+ * @private
+ */
+ var formatSignalName = function (signal) {
+ return 'signal' + signal.charAt(0).toUpperCase() + signal.slice(1);
+ };
+
+ /**
+ * Calls handlers for a signal
+ *
+ * @see call()
+ * @see callOnlyFirst()
+ * @param {Array} args
+ * @param {boolean} returnAtFirst
+ * @return {*}
+ * @private
+ */
+ var callHandlers = function (args, returnAtFirst) {
+ args = [].slice.call(args);
+
+ var idx, ret,
+ signal = formatSignalName(args.shift());
+
+ for (idx = 0; idx < registeredPlugins.length; idx++) {
+ if (signal in registeredPlugins[idx]) {
+ ret = registeredPlugins[idx][signal].apply(thisObj, args);
+
+ if (returnAtFirst) {
+ return ret;
+ }
+ }
+ }
+ };
+
+ /**
+ * Calls all handlers for the passed signal
+ *
+ * @param {string} signal
+ * @param {...string} args
+ * @function
+ * @name call
+ * @memberOf PluginManager.prototype
+ */
+ base.call = function () {
+ callHandlers(arguments, false);
+ };
+
+ /**
+ * Calls the first handler for a signal, and returns the
+ *
+ * @param {string} signal
+ * @param {...string} args
+ * @return {*} The result of calling the handler
+ * @function
+ * @name callOnlyFirst
+ * @memberOf PluginManager.prototype
+ */
+ base.callOnlyFirst = function () {
+ return callHandlers(arguments, true);
+ };
+
+ /**
+ * Checks if a signal has a handler
+ *
+ * @param {string} signal
+ * @return {boolean}
+ * @function
+ * @name hasHandler
+ * @memberOf PluginManager.prototype
+ */
+ base.hasHandler = function (signal) {
+ var i = registeredPlugins.length;
+ signal = formatSignalName(signal);
+
+ while (i--) {
+ if (signal in registeredPlugins[i]) {
+ return true;
+ }
+ }
+
+ return false;
+ };
+
+ /**
+ * Checks if the plugin exists in plugins
+ *
+ * @param {string} plugin
+ * @return {boolean}
+ * @function
+ * @name exists
+ * @memberOf PluginManager.prototype
+ */
+ base.exists = function (plugin) {
+ if (plugin in plugins) {
+ plugin = plugins[plugin];
+
+ return typeof plugin === 'function' &&
+ typeof plugin.prototype === 'object';
+ }
+
+ return false;
+ };
+
+ /**
+ * Checks if the passed plugin is currently registered.
+ *
+ * @param {string} plugin
+ * @return {boolean}
+ * @function
+ * @name isRegistered
+ * @memberOf PluginManager.prototype
+ */
+ base.isRegistered = function (plugin) {
+ if (base.exists(plugin)) {
+ var idx = registeredPlugins.length;
+
+ while (idx--) {
+ if (registeredPlugins[idx] instanceof plugins[plugin]) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ };
+
+ /**
+ * Registers a plugin to receive signals
+ *
+ * @param {string} plugin
+ * @return {boolean}
+ * @function
+ * @name register
+ * @memberOf PluginManager.prototype
+ */
+ base.register = function (plugin) {
+ if (!base.exists(plugin) || base.isRegistered(plugin)) {
+ return false;
+ }
+
+ plugin = new plugins[plugin]();
+ registeredPlugins.push(plugin);
+
+ if ('init' in plugin) {
+ plugin.init.call(thisObj);
+ }
+
+ return true;
+ };
+
+ /**
+ * Deregisters a plugin.
+ *
+ * @param {string} plugin
+ * @return {boolean}
+ * @function
+ * @name deregister
+ * @memberOf PluginManager.prototype
+ */
+ base.deregister = function (plugin) {
+ var removedPlugin,
+ pluginIdx = registeredPlugins.length,
+ removed = false;
+
+ if (!base.isRegistered(plugin)) {
+ return removed;
+ }
+
+ while (pluginIdx--) {
+ if (registeredPlugins[pluginIdx] instanceof plugins[plugin]) {
+ removedPlugin = registeredPlugins.splice(pluginIdx, 1)[0];
+ removed = true;
+
+ if ('destroy' in removedPlugin) {
+ removedPlugin.destroy.call(thisObj);
+ }
+ }
+ }
+
+ return removed;
+ };
+
+ /**
+ * Clears all plugins and removes the owner reference.
+ *
+ * Calling any functions on this object after calling
+ * destroy will cause a JS error.
+ *
+ * @name destroy
+ * @memberOf PluginManager.prototype
+ */
+ base.destroy = function () {
+ var i = registeredPlugins.length;
+
+ while (i--) {
+ if ('destroy' in registeredPlugins[i]) {
+ registeredPlugins[i].destroy.call(thisObj);
+ }
+ }
+
+ registeredPlugins = [];
+ thisObj = null;
+ };
+ }
+
+ PluginManager.plugins = plugins;
+
+ // 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$1 = ie && ie < 11;
+
+
+ /**
+ * Gets the text, start/end node and offset for
+ * length chars left or right of the passed node
+ * at the specified offset.
+ *
+ * @param {Node} node
+ * @param {number} offset
+ * @param {boolean} isLeft
+ * @param {number} length
+ * @return {Object}
+ * @private
+ */
+ var outerText = function (range, isLeft, length) {
+ var nodeValue, remaining, start, end, node,
+ text = '',
+ next = range.startContainer,
+ offset = range.startOffset;
+
+ // Handle cases where node is a paragraph and offset
+ // refers to the index of a text node.
+ // 3 = text node
+ if (next && next.nodeType !== 3) {
+ next = next.childNodes[offset];
+ offset = 0;
+ }
+
+ start = end = offset;
+
+ while (length > text.length && next && next.nodeType === 3) {
+ nodeValue = next.nodeValue;
+ remaining = length - text.length;
+
+ // If not the first node, start and end should be at their
+ // max values as will be updated when getting the text
+ if (node) {
+ end = nodeValue.length;
+ start = 0;
+ }
+
+ node = next;
+
+ if (isLeft) {
+ start = Math.max(end - remaining, 0);
+ offset = start;
+
+ text = nodeValue.substr(start, end - start) + text;
+ next = node.previousSibling;
+ } else {
+ end = Math.min(remaining, nodeValue.length);
+ offset = start + end;
+
+ text += nodeValue.substr(start, end);
+ next = node.nextSibling;
+ }
+ }
+
+ return {
+ node: node || next,
+ offset: offset,
+ text: text
+ };
+ };
+
+ /**
+ * Range helper
+ *
+ * @class RangeHelper
+ * @name RangeHelper
+ */
+ function RangeHelper(win, d) {
+ var _createMarker, _prepareInput,
+ doc = d || win.contentDocument || win.document,
+ startMarker = 'sceditor-start-marker',
+ endMarker = 'sceditor-end-marker',
+ base = this;
+
+ /**
+ * Inserts HTML into the current range replacing any selected
+ * text.
+ *
+ * If endHTML is specified the selected contents will be put between
+ * html and endHTML. If there is nothing selected html and endHTML are
+ * just concatenate together.
+ *
+ * @param {string} html
+ * @param {string} [endHTML]
+ * @return False on fail
+ * @function
+ * @name insertHTML
+ * @memberOf RangeHelper.prototype
+ */
+ base.insertHTML = function (html, endHTML) {
+ var node, div,
+ range = base.selectedRange();
+
+ if (!range) {
+ return false;
+ }
+
+ if (endHTML) {
+ html += base.selectedHtml() + endHTML;
+ }
+
+ div = createElement('p', {}, doc);
+ node = doc.createDocumentFragment();
+ div.innerHTML = html;
+
+ while (div.firstChild) {
+ appendChild(node, div.firstChild);
+ }
+
+ base.insertNode(node);
+ };
+
+ /**
+ * Prepares HTML to be inserted by adding a zero width space
+ * if the last child is empty and adding the range start/end
+ * markers to the last child.
+ *
+ * @param {Node|string} node
+ * @param {Node|string} [endNode]
+ * @param {boolean} [returnHtml]
+ * @return {Node|string}
+ * @private
+ */
+ _prepareInput = function (node, endNode, returnHtml) {
+ var lastChild,
+ frag = doc.createDocumentFragment();
+
+ if (typeof node === 'string') {
+ if (endNode) {
+ node += base.selectedHtml() + endNode;
+ }
+
+ frag = parseHTML(node);
+ } else {
+ appendChild(frag, node);
+
+ if (endNode) {
+ appendChild(frag, base.selectedRange().extractContents());
+ appendChild(frag, endNode);
+ }
+ }
+
+ if (!(lastChild = frag.lastChild)) {
+ return;
+ }
+
+ while (!isInline(lastChild.lastChild, true)) {
+ lastChild = lastChild.lastChild;
+ }
+
+ if (canHaveChildren(lastChild)) {
+ // Webkit won't allow the cursor to be placed inside an
+ // empty tag, so add a zero width space to it.
+ if (!lastChild.lastChild) {
+ appendChild(lastChild, document.createTextNode('\u200B'));
+ }
+ } else {
+ lastChild = frag;
+ }
+
+ base.removeMarkers();
+
+ // Append marks to last child so when restored cursor will be in
+ // the right place
+ appendChild(lastChild, _createMarker(startMarker));
+ appendChild(lastChild, _createMarker(endMarker));
+
+ if (returnHtml) {
+ var div = createElement('div');
+ appendChild(div, frag);
+
+ return div.innerHTML;
+ }
+
+ return frag;
+ };
+
+ /**
+ * The same as insertHTML except with DOM nodes instead
+ *
+ * Warning: the nodes must belong to the
+ * document they are being inserted into. Some browsers
+ * will throw exceptions if they don't.
+ *
+ * Returns boolean false on fail
+ *
+ * @param {Node} node
+ * @param {Node} endNode
+ * @return {false|undefined}
+ * @function
+ * @name insertNode
+ * @memberOf RangeHelper.prototype
+ */
+ base.insertNode = function (node, endNode) {
+ var input = _prepareInput(node, endNode),
+ range = base.selectedRange(),
+ parent$$1 = range.commonAncestorContainer;
+
+ if (!input) {
+ return false;
+ }
+
+ range.deleteContents();
+
+ // FF allows to be selected but inserting a node
+ // into will cause it not to be displayed so must
+ // insert before the in FF.
+ // 3 = TextNode
+ if (parent$$1 && parent$$1.nodeType !== 3 && !canHaveChildren(parent$$1)) {
+ insertBefore(input, parent$$1);
+ } else {
+ range.insertNode(input);
+ }
+
+ base.restoreRange();
+ };
+
+ /**
+ * Clones the selected Range
+ *
+ * @return {Range}
+ * @function
+ * @name cloneSelected
+ * @memberOf RangeHelper.prototype
+ */
+ base.cloneSelected = function () {
+ var range = base.selectedRange();
+
+ if (range) {
+ return range.cloneRange();
+ }
+ };
+
+ /**
+ * Gets the selected Range
+ *
+ * @return {Range}
+ * @function
+ * @name selectedRange
+ * @memberOf RangeHelper.prototype
+ */
+ base.selectedRange = function () {
+ var range, firstChild,
+ sel = win.getSelection();
+
+ if (!sel) {
+ return;
+ }
+
+ // When creating a new range, set the start to the first child
+ // element of the body element to avoid errors in FF.
+ if (sel.rangeCount <= 0) {
+ firstChild = doc.body;
+ while (firstChild.firstChild) {
+ firstChild = firstChild.firstChild;
+ }
+
+ range = doc.createRange();
+ // Must be setStartBefore otherwise it can cause infinite
+ // loops with lists in WebKit. See issue 442
+ range.setStartBefore(firstChild);
+
+ sel.addRange(range);
+ }
+
+ if (sel.rangeCount > 0) {
+ range = sel.getRangeAt(0);
+ }
+
+ return range;
+ };
+
+ /**
+ * Gets if there is currently a selection
+ *
+ * @return {boolean}
+ * @function
+ * @name hasSelection
+ * @since 1.4.4
+ * @memberOf RangeHelper.prototype
+ */
+ base.hasSelection = function () {
+ var sel = win.getSelection();
+
+ return sel && sel.rangeCount > 0;
+ };
+
+ /**
+ * Gets the currently selected HTML
+ *
+ * @return {string}
+ * @function
+ * @name selectedHtml
+ * @memberOf RangeHelper.prototype
+ */
+ base.selectedHtml = function () {
+ var div,
+ range = base.selectedRange();
+
+ if (range) {
+ div = createElement('p', {}, doc);
+ appendChild(div, range.cloneContents());
+
+ return div.innerHTML;
+ }
+
+ return '';
+ };
+
+ /**
+ * Gets the parent node of the selected contents in the range
+ *
+ * @return {HTMLElement}
+ * @function
+ * @name parentNode
+ * @memberOf RangeHelper.prototype
+ */
+ base.parentNode = function () {
+ var range = base.selectedRange();
+
+ if (range) {
+ return range.commonAncestorContainer;
+ }
+ };
+
+ /**
+ * Gets the first block level parent of the selected
+ * contents of the range.
+ *
+ * @return {HTMLElement}
+ * @function
+ * @name getFirstBlockParent
+ * @memberOf RangeHelper.prototype
+ */
+ /**
+ * Gets the first block level parent of the selected
+ * contents of the range.
+ *
+ * @param {Node} [n] The element to get the first block level parent from
+ * @return {HTMLElement}
+ * @function
+ * @name getFirstBlockParent^2
+ * @since 1.4.1
+ * @memberOf RangeHelper.prototype
+ */
+ base.getFirstBlockParent = function (node) {
+ var func = function (elm) {
+ if (!isInline(elm, true)) {
+ return elm;
+ }
+
+ elm = elm ? elm.parentNode : null;
+
+ return elm ? func(elm) : elm;
+ };
+
+ return func(node || base.parentNode());
+ };
+
+ /**
+ * Inserts a node at either the start or end of the current selection
+ *
+ * @param {Bool} start
+ * @param {Node} node
+ * @function
+ * @name insertNodeAt
+ * @memberOf RangeHelper.prototype
+ */
+ base.insertNodeAt = function (start, node) {
+ var currentRange = base.selectedRange(),
+ range = base.cloneSelected();
+
+ if (!range) {
+ return false;
+ }
+
+ range.collapse(start);
+ range.insertNode(node);
+
+ // Reselect the current range.
+ // Fixes issue with Chrome losing the selection. Issue#82
+ base.selectRange(currentRange);
+ };
+
+ /**
+ * Creates a marker node
+ *
+ * @param {string} id
+ * @return {HTMLSpanElement}
+ * @private
+ */
+ _createMarker = function (id) {
+ base.removeMarker(id);
+
+ var marker = createElement('span', {
+ id: id,
+ className: 'sceditor-selection sceditor-ignore',
+ style: 'display:none;line-height:0'
+ }, doc);
+
+ marker.innerHTML = ' ';
+
+ return marker;
+ };
+
+ /**
+ * Inserts start/end markers for the current selection
+ * which can be used by restoreRange to re-select the
+ * range.
+ *
+ * @memberOf RangeHelper.prototype
+ * @function
+ * @name insertMarkers
+ */
+ base.insertMarkers = function () {
+ var currentRange = base.selectedRange();
+ var startNode = _createMarker(startMarker);
+
+ base.removeMarkers();
+ base.insertNodeAt(true, startNode);
+
+ // Fixes issue with end marker sometimes being placed before
+ // the start marker when the range is collapsed.
+ if (currentRange && currentRange.collapsed) {
+ startNode.parentNode.insertBefore(
+ _createMarker(endMarker), startNode.nextSibling);
+ } else {
+ base.insertNodeAt(false, _createMarker(endMarker));
+ }
+ };
+
+ /**
+ * Gets the marker with the specified ID
+ *
+ * @param {string} id
+ * @return {Node}
+ * @function
+ * @name getMarker
+ * @memberOf RangeHelper.prototype
+ */
+ base.getMarker = function (id) {
+ return doc.getElementById(id);
+ };
+
+ /**
+ * Removes the marker with the specified ID
+ *
+ * @param {string} id
+ * @function
+ * @name removeMarker
+ * @memberOf RangeHelper.prototype
+ */
+ base.removeMarker = function (id) {
+ var marker = base.getMarker(id);
+
+ if (marker) {
+ remove(marker);
+ }
+ };
+
+ /**
+ * Removes the start/end markers
+ *
+ * @function
+ * @name removeMarkers
+ * @memberOf RangeHelper.prototype
+ */
+ base.removeMarkers = function () {
+ base.removeMarker(startMarker);
+ base.removeMarker(endMarker);
+ };
+
+ /**
+ * Saves the current range location. Alias of insertMarkers()
+ *
+ * @function
+ * @name saveRage
+ * @memberOf RangeHelper.prototype
+ */
+ base.saveRange = function () {
+ base.insertMarkers();
+ };
+
+ /**
+ * Select the specified range
+ *
+ * @param {Range} range
+ * @function
+ * @name selectRange
+ * @memberOf RangeHelper.prototype
+ */
+ base.selectRange = function (range) {
+ var lastChild;
+ var sel = win.getSelection();
+ var container = range.endContainer;
+
+ // Check if cursor is set after a BR when the BR is the only
+ // child of the parent. In Firefox this causes a line break
+ // to occur when something is typed. See issue #321
+ if (!IE_BR_FIX$1 && range.collapsed && container &&
+ !isInline(container, true)) {
+
+ lastChild = container.lastChild;
+ while (lastChild && is(lastChild, '.sceditor-ignore')) {
+ lastChild = lastChild.previousSibling;
+ }
+
+ if (is(lastChild, 'br')) {
+ var rng = doc.createRange();
+ rng.setEndAfter(lastChild);
+ rng.collapse(false);
+
+ if (base.compare(range, rng)) {
+ range.setStartBefore(lastChild);
+ range.collapse(true);
+ }
+ }
+ }
+
+ if (sel) {
+ base.clear();
+ sel.addRange(range);
+ }
+ };
+
+ /**
+ * Restores the last range saved by saveRange() or insertMarkers()
+ *
+ * @function
+ * @name restoreRange
+ * @memberOf RangeHelper.prototype
+ */
+ base.restoreRange = function () {
+ var isCollapsed,
+ range = base.selectedRange(),
+ start = base.getMarker(startMarker),
+ end = base.getMarker(endMarker);
+
+ if (!start || !end || !range) {
+ return false;
+ }
+
+ isCollapsed = start.nextSibling === end;
+
+ range = doc.createRange();
+ range.setStartBefore(start);
+ range.setEndAfter(end);
+
+ if (isCollapsed) {
+ range.collapse(true);
+ }
+
+ base.selectRange(range);
+ base.removeMarkers();
+ };
+
+ /**
+ * Selects the text left and right of the current selection
+ *
+ * @param {number} left
+ * @param {number} right
+ * @since 1.4.3
+ * @function
+ * @name selectOuterText
+ * @memberOf RangeHelper.prototype
+ */
+ base.selectOuterText = function (left, right) {
+ var start, end,
+ range = base.cloneSelected();
+
+ if (!range) {
+ return false;
+ }
+
+ range.collapse(false);
+
+ start = outerText(range, true, left);
+ end = outerText(range, false, right);
+
+ range.setStart(start.node, start.offset);
+ range.setEnd(end.node, end.offset);
+
+ base.selectRange(range);
+ };
+
+ /**
+ * Gets the text left or right of the current selection
+ *
+ * @param {boolean} before
+ * @param {number} length
+ * @return {string}
+ * @since 1.4.3
+ * @function
+ * @name selectOuterText
+ * @memberOf RangeHelper.prototype
+ */
+ base.getOuterText = function (before, length) {
+ var range = base.cloneSelected();
+
+ if (!range) {
+ return '';
+ }
+
+ range.collapse(!before);
+
+ return outerText(range, before, length).text;
+ };
+
+ /**
+ * Replaces keywords with values based on the current caret position
+ *
+ * @param {Array} keywords
+ * @param {boolean} includeAfter If to include the text after the
+ * current caret position or just
+ * text before
+ * @param {boolean} keywordsSorted If the keywords array is pre
+ * sorted shortest to longest
+ * @param {number} longestKeyword Length of the longest keyword
+ * @param {boolean} requireWhitespace If the key must be surrounded
+ * by whitespace
+ * @param {string} keypressChar If this is being called from
+ * a keypress event, this should be
+ * set to the pressed character
+ * @return {boolean}
+ * @function
+ * @name replaceKeyword
+ * @memberOf RangeHelper.prototype
+ */
+ // eslint-disable-next-line max-params
+ base.replaceKeyword = function (
+ keywords,
+ includeAfter,
+ keywordsSorted,
+ longestKeyword,
+ requireWhitespace,
+ keypressChar
+ ) {
+ if (!keywordsSorted) {
+ keywords.sort(function (a, b) {
+ return a[0].length - b[0].length;
+ });
+ }
+
+ var outerText, match, matchPos, startIndex,
+ leftLen, charsLeft, keyword, keywordLen,
+ whitespaceRegex = '(^|[\\s\xA0\u2002\u2003\u2009])',
+ keywordIdx = keywords.length,
+ whitespaceLen = requireWhitespace ? 1 : 0,
+ maxKeyLen = longestKeyword ||
+ keywords[keywordIdx - 1][0].length;
+
+ if (requireWhitespace) {
+ maxKeyLen++;
+ }
+
+ keypressChar = keypressChar || '';
+ outerText = base.getOuterText(true, maxKeyLen);
+ leftLen = outerText.length;
+ outerText += keypressChar;
+
+ if (includeAfter) {
+ outerText += base.getOuterText(false, maxKeyLen);
+ }
+
+ while (keywordIdx--) {
+ keyword = keywords[keywordIdx][0];
+ keywordLen = keyword.length;
+ startIndex = Math.max(0, leftLen - keywordLen - whitespaceLen);
+ matchPos = -1;
+
+ if (requireWhitespace) {
+ match = outerText
+ .substr(startIndex)
+ .match(new RegExp(whitespaceRegex +
+ regex(keyword) + whitespaceRegex));
+
+ if (match) {
+ // Add the length of the text that was removed by
+ // substr() and also add 1 for the whitespace
+ matchPos = match.index + startIndex + match[1].length;
+ }
+ } else {
+ matchPos = outerText.indexOf(keyword, startIndex);
+ }
+
+ if (matchPos > -1) {
+ // Make sure the match is between before and
+ // after, not just entirely in one side or the other
+ if (matchPos <= leftLen &&
+ matchPos + keywordLen + whitespaceLen >= leftLen) {
+ charsLeft = leftLen - matchPos;
+
+ // If the keypress char is white space then it should
+ // not be replaced, only chars that are part of the
+ // key should be replaced.
+ base.selectOuterText(
+ charsLeft,
+ keywordLen - charsLeft -
+ (/^\S/.test(keypressChar) ? 1 : 0)
+ );
+
+ base.insertHTML(keywords[keywordIdx][1]);
+ return true;
+ }
+ }
+ }
+
+ return false;
+ };
+
+ /**
+ * Compares two ranges.
+ *
+ * If rangeB is undefined it will be set to
+ * the current selected range
+ *
+ * @param {Range} rngA
+ * @param {Range} [rngB]
+ * @return {boolean}
+ * @function
+ * @name compare
+ * @memberOf RangeHelper.prototype
+ */
+ base.compare = function (rngA, rngB) {
+ if (!rngB) {
+ rngB = base.selectedRange();
+ }
+
+ if (!rngA || !rngB) {
+ return !rngA && !rngB;
+ }
+
+ return rngA.compareBoundaryPoints(Range.END_TO_END, rngB) === 0 &&
+ rngA.compareBoundaryPoints(Range.START_TO_START, rngB) === 0;
+ };
+
+ /**
+ * Removes any current selection
+ *
+ * @since 1.4.6
+ * @function
+ * @name clear
+ * @memberOf RangeHelper.prototype
+ */
+ base.clear = function () {
+ var sel = win.getSelection();
+
+ if (sel) {
+ if (sel.removeAllRanges) {
+ sel.removeAllRanges();
+ } else if (sel.empty) {
+ sel.empty();
+ }
+ }
+ };
+ }
+
+ /**
+ * Checks all emoticons are surrounded by whitespace and
+ * replaces any that aren't with with their emoticon code.
+ *
+ * @param {HTMLElement} node
+ * @param {rangeHelper} rangeHelper
+ * @return {void}
+ */
+ function checkWhitespace(node, rangeHelper) {
+ var noneWsRegex = /[^\s\xA0\u2002\u2003\u2009\u00a0]+/;
+ var emoticons = node && find(node, 'img[data-sceditor-emoticon]');
+
+ if (!node || !emoticons.length) {
+ return;
+ }
+
+ for (var i = 0; i < emoticons.length; i++) {
+ var emoticon = emoticons[i];
+ var parent$$1 = emoticon.parentNode;
+ var prev = emoticon.previousSibling;
+ var next = emoticon.nextSibling;
+
+ if ((!prev || !noneWsRegex.test(prev.nodeValue.slice(-1))) &&
+ (!next || !noneWsRegex.test((next.nodeValue || '')[0]))) {
+ continue;
+ }
+
+ var range = rangeHelper.cloneSelected();
+ var rangeStart = -1;
+ var rangeStartContainer = range.startContainer;
+ var previousText = prev.nodeValue;
+
+ // For IE's HTMLPhraseElement
+ if (previousText === null) {
+ previousText = prev.innerText || '';
+ }
+
+ previousText += data(emoticon, 'sceditor-emoticon');
+
+ // If the cursor is after the removed emoticon, add
+ // the length of the newly added text to it
+ if (rangeStartContainer === next) {
+ rangeStart = previousText.length + range.startOffset;
+ }
+
+ // If the cursor is set before the next node, set it to
+ // the end of the new text node
+ if (rangeStartContainer === node &&
+ node.childNodes[range.startOffset] === next) {
+ rangeStart = previousText.length;
+ }
+
+ // If the cursor is set before the removed emoticon,
+ // just keep it at that position
+ if (rangeStartContainer === prev) {
+ rangeStart = range.startOffset;
+ }
+
+ if (!next || next.nodeType !== TEXT_NODE) {
+ next = parent$$1.insertBefore(
+ parent$$1.ownerDocument.createTextNode(''), next
+ );
+ }
+
+ next.insertData(0, previousText);
+ remove(prev);
+ remove(emoticon);
+
+ // Need to update the range starting position if it's been modified
+ if (rangeStart > -1) {
+ range.setStart(next, rangeStart);
+ range.collapse(true);
+ rangeHelper.selectRange(range);
+ }
+ }
+ }
+
+ /**
+ * Replaces any emoticons inside the root node with images.
+ *
+ * emoticons should be an object where the key is the emoticon
+ * code and the value is the HTML to replace it with.
+ *
+ * @param {HTMLElement} root
+ * @param {Object} emoticons
+ * @param {boolean} emoticonsCompat
+ * @return {void}
+ */
+ function replace(root, emoticons, emoticonsCompat) {
+ var doc = root.ownerDocument;
+ var space = '(^|\\s|\xA0|\u2002|\u2003|\u2009|$)';
+ var emoticonCodes = [];
+ var emoticonRegex = {};
+
+ // TODO: Make this tag configurable.
+ if (parent(root, 'code')) {
+ return;
+ }
+
+ each(emoticons, function (key) {
+ emoticonRegex[key] = new RegExp(space + regex(key) + space);
+ emoticonCodes.push(key);
+ });
+
+ // Sort keys longest to shortest so that longer keys
+ // take precedence (avoids bugs with shorter keys partially
+ // matching longer ones)
+ emoticonCodes.sort(function (a, b) {
+ return b.length - a.length;
+ });
+
+ (function convert(node) {
+ node = node.firstChild;
+
+ while (node) {
+ // TODO: Make this tag configurable.
+ if (node.nodeType === ELEMENT_NODE && !is(node, 'code')) {
+ convert(node);
+ }
+
+ if (node.nodeType === TEXT_NODE) {
+ for (var i = 0; i < emoticonCodes.length; i++) {
+ var text = node.nodeValue;
+ var key = emoticonCodes[i];
+ var index = emoticonsCompat ?
+ text.search(emoticonRegex[key]) :
+ text.indexOf(key);
+
+ if (index > -1) {
+ // When emoticonsCompat is enabled this will be the
+ // position after any white space
+ var startIndex = text.indexOf(key, index);
+ var fragment = parseHTML(emoticons[key], doc);
+ var after = text.substr(startIndex + key.length);
+
+ fragment.appendChild(doc.createTextNode(after));
+
+ node.nodeValue = text.substr(0, startIndex);
+ node.parentNode
+ .insertBefore(fragment, node.nextSibling);
+ }
+ }
+ }
+
+ node = node.nextSibling;
+ }
+ }(root));
+ }
+
+ var globalWin = window;
+ var globalDoc = document;
+
+ var IE_VER = 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$2 = IE_VER && IE_VER < 11;
+
+ var IMAGE_MIME_REGEX = /^image\/(p?jpe?g|gif|png|bmp)$/i;
+
+ /**
+ * Wrap inlines that are in the root in paragraphs.
+ *
+ * @param {HTMLBodyElement} body
+ * @param {Document} doc
+ * @private
+ */
+ function wrapInlines(body, doc) {
+ var wrapper;
+
+ traverse(body, function (node) {
+ if (isInline(node, true)) {
+ if (!wrapper) {
+ wrapper = createElement('p', {}, doc);
+ insertBefore(wrapper, node);
+ }
+
+ if (node.nodeType !== TEXT_NODE || node.nodeValue !== '') {
+ appendChild(wrapper, node);
+ }
+ } else {
+ wrapper = null;
+ }
+ }, false, true);
+ }
+
+ /**
+ * SCEditor - A lightweight WYSIWYG editor
+ *
+ * @param {HTMLTextAreaElement} original The textarea to be converted
+ * @param {Object} userOptions
+ * @class SCEditor
+ * @name SCEditor
+ */
+ function SCEditor(original, userOptions) {
+ /**
+ * Alias of this
+ *
+ * @private
+ */
+ var base = this;
+
+ /**
+ * Editor format like BBCode or HTML
+ */
+ var format;
+
+ /**
+ * The div which contains the editor and toolbar
+ *
+ * @type {HTMLDivElement}
+ * @private
+ */
+ var editorContainer;
+
+ /**
+ * Map of events handlers bound to this instance.
+ *
+ * @type {Object}
+ * @private
+ */
+ var eventHandlers = {};
+
+ /**
+ * The editors toolbar
+ *
+ * @type {HTMLDivElement}
+ * @private
+ */
+ var toolbar;
+
+ /**
+ * The editors iframe which should be in design mode
+ *
+ * @type {HTMLIFrameElement}
+ * @private
+ */
+ var wysiwygEditor;
+
+ /**
+ * The editors window
+ *
+ * @type {Window}
+ * @private
+ */
+ var wysiwygWindow;
+
+ /**
+ * The WYSIWYG editors body element
+ *
+ * @type {HTMLBodyElement}
+ * @private
+ */
+ var wysiwygBody;
+
+ /**
+ * The WYSIWYG editors document
+ *
+ * @type {Document}
+ * @private
+ */
+ var wysiwygDocument;
+
+ /**
+ * The editors textarea for viewing source
+ *
+ * @type {HTMLTextAreaElement}
+ * @private
+ */
+ var sourceEditor;
+
+ /**
+ * The current dropdown
+ *
+ * @type {HTMLDivElement}
+ * @private
+ */
+ var dropdown;
+
+ /**
+ * Store the last cursor position. Needed for IE because it forgets
+ *
+ * @type {Range}
+ * @private
+ */
+ var lastRange;
+
+ /**
+ * If the user is currently composing text via IME
+ * @type {boolean}
+ */
+ var isComposing;
+
+ /**
+ * Timer for valueChanged key handler
+ * @type {number}
+ */
+ var valueChangedKeyUpTimer;
+
+ /**
+ * The editors locale
+ *
+ * @private
+ */
+ var locale;
+
+ /**
+ * Stores a cache of preloaded images
+ *
+ * @private
+ * @type {Array.}
+ */
+ var preLoadCache = [];
+
+ /**
+ * The editors rangeHelper instance
+ *
+ * @type {RangeHelper}
+ * @private
+ */
+ var rangeHelper;
+
+ /**
+ * An array of button state handlers
+ *
+ * @type {Array.}
+ * @private
+ */
+ var btnStateHandlers = [];
+
+ /**
+ * Plugin manager instance
+ *
+ * @type {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 {HTMLStyleElement}
+ * @private
+ */
+ var inlineCss;
+
+ /**
+ * Object containing a list of shortcut handlers
+ *
+ * @type {Object}
+ * @private
+ */
+ var shortcutHandlers = {};
+
+ /**
+ * The min and max heights that autoExpand should stay within
+ *
+ * @type {Object}
+ * @private
+ */
+ var autoExpandBounds;
+
+ /**
+ * Timeout for the autoExpand function to throttle calls
+ *
+ * @private
+ */
+ var autoExpandThrottle;
+
+ /**
+ * Cache of the current toolbar buttons
+ *
+ * @type {Object}
+ * @private
+ */
+ var toolbarButtons = {};
+
+ /**
+ * Last scroll position before maximizing so
+ * it can be restored when finished.
+ *
+ * @type {number}
+ * @private
+ */
+ var maximizeScrollPosition;
+
+ /**
+ * Stores the contents while a paste is taking place.
+ *
+ * Needed to support browsers that lack clipboard API support.
+ *
+ * @type {?DocumentFragment}
+ * @private
+ */
+ var pasteContentFragment;
+
+ /**
+ * All the emoticons from dropdown, more and hidden combined
+ * and with the emoticons root set
+ *
+ * @type {!Object}
+ * @private
+ */
+ var allEmoticons = {};
+
+ /**
+ * Current icon set if any
+ *
+ * @type {?Object}
+ * @private
+ */
+ var icons;
+
+ /**
+ * Private functions
+ * @private
+ */
+ var init,
+ replaceEmoticons,
+ handleCommand,
+ saveRange,
+ initEditor,
+ initPlugins,
+ initLocale,
+ initToolBar,
+ initOptions,
+ initEvents,
+ initResize,
+ initEmoticons,
+ handlePasteEvt,
+ handlePasteData,
+ handleKeyDown,
+ handleBackSpace,
+ handleKeyPress,
+ handleFormReset,
+ handleMouseDown,
+ handleComposition,
+ handleEvent,
+ handleDocumentClick,
+ updateToolBar,
+ updateActiveButtons,
+ sourceEditorSelectedText,
+ appendNewLine,
+ checkSelectionChanged,
+ checkNodeChanged,
+ autofocus,
+ emoticonsKeyPress,
+ emoticonsCheckWhitespace,
+ currentStyledBlockNode,
+ triggerValueChanged,
+ valueChangedBlur,
+ valueChangedKeyUp,
+ autoUpdate,
+ autoExpand;
+
+ /**
+ * All the commands supported by the editor
+ * @name commands
+ * @memberOf SCEditor.prototype
+ */
+ base.commands = extend(true, {}, (userOptions.commands || defaultCmds));
+
+ /**
+ * Options for this editor instance
+ * @name opts
+ * @memberOf SCEditor.prototype
+ */
+ var options = base.opts = extend(
+ true, {}, defaultOptions, userOptions
+ );
+
+ // Don't deep extend emoticons (fixes #565)
+ base.opts.emoticons = userOptions.emoticons || defaultOptions.emoticons;
+
+ /**
+ * Creates the editor iframe and textarea
+ * @private
+ */
+ init = function () {
+ original._sceditor = base;
+
+ // Load locale
+ if (options.locale && options.locale !== 'en') {
+ initLocale();
+ }
+
+ editorContainer = createElement('div', {
+ className: 'sceditor-container'
+ });
+
+ insertBefore(editorContainer, original);
+ css(editorContainer, '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) {
+ addClass(editorContainer, 'ie ie' + IE_VER);
+ }
+
+ isRequired = original.required;
+ original.required = false;
+
+ var FormatCtor = SCEditor.formats[options.format];
+ format = FormatCtor ? new FormatCtor() : {};
+ if ('init' in format) {
+ format.init.call(base);
+ }
+
+ // create the editor
+ initPlugins();
+ initEmoticons();
+ initToolBar();
+ initEditor();
+ initOptions();
+ initEvents();
+
+ // force into source mode if is a browser that can't handle
+ // full editing
+ if (!isWysiwygSupported) {
+ base.toggleSourceMode();
+ }
+
+ updateActiveButtons();
+
+ var loaded = function () {
+ off(globalWin, 'load', loaded);
+
+ if (options.autofocus) {
+ autofocus();
+ }
+
+ autoExpand();
+ appendNewLine();
+ // TODO: use editor doc and window?
+ pluginManager.call('ready');
+ if ('onReady' in format) {
+ format.onReady.call(base);
+ }
+ };
+ on(globalWin, 'load', loaded);
+ if (globalDoc.readyState === 'complete') {
+ loaded();
+ }
+ };
+
+ initPlugins = function () {
+ var plugins = options.plugins;
+
+ plugins = plugins ? plugins.toString().split(',') : [];
+ pluginManager = new PluginManager(base);
+
+ plugins.forEach(function (plugin) {
+ pluginManager.register(plugin.trim());
+ });
+ };
+
+ /**
+ * 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
+ * @private
+ */
+ initEditor = function () {
+ sourceEditor = createElement('textarea');
+ wysiwygEditor = createElement('iframe', {
+ frameborder: 0,
+ allowfullscreen: true
+ });
+
+ /* 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 (options.startInSourceMode) {
+ addClass(editorContainer, 'sourceMode');
+ hide(wysiwygEditor);
+ } else {
+ addClass(editorContainer, 'wysiwygMode');
+ hide(sourceEditor);
+ }
+
+ if (!options.spellcheck) {
+ attr(editorContainer, 'spellcheck', 'false');
+ }
+
+ if (globalWin.location.protocol === 'https:') {
+ // eslint-disable-next-line no-script-url
+ attr(wysiwygEditor, 'src', 'javascript:false');
+ }
+
+ // Add the editor to the container
+ appendChild(editorContainer, wysiwygEditor);
+ appendChild(editorContainer, sourceEditor);
+
+ // TODO: make this optional somehow
+ base.dimensions(
+ options.width || width(original),
+ options.height || height(original)
+ );
+
+ // Add IE version class to the HTML element so can apply
+ // conditional styling without CSS hacks
+ var className = IE_VER ? 'ie ie' + IE_VER : '';
+ // Add ios to HTML so can apply CSS fix to only it
+ className += ios ? ' ios' : '';
+
+ wysiwygDocument = wysiwygEditor.contentDocument;
+ wysiwygDocument.open();
+ wysiwygDocument.write(_tmpl('html', {
+ attrs: ' class="' + className + '"',
+ spellcheck: options.spellcheck ? '' : 'spellcheck="false"',
+ charset: options.charset,
+ style: options.style
+ }));
+ wysiwygDocument.close();
+
+ wysiwygBody = wysiwygDocument.body;
+ wysiwygWindow = wysiwygEditor.contentWindow;
+
+ base.readOnly(!!options.readOnly);
+
+ // iframe overflow fix for iOS, also fixes an IE issue with the
+ // editor not getting focus when clicking inside
+ if (ios || edge || IE_VER) {
+ height(wysiwygBody, '100%');
+
+ if (!IE_VER) {
+ on(wysiwygBody, 'touchend', base.focus);
+ }
+ }
+
+ var tabIndex = attr(original, 'tabindex');
+ attr(sourceEditor, 'tabindex', tabIndex);
+ attr(wysiwygEditor, 'tabindex', tabIndex);
+
+ rangeHelper = new RangeHelper(wysiwygWindow);
+
+ // load any textarea value into the editor
+ hide(original);
+ base.val(original.value);
+
+ var placeholder = options.placeholder ||
+ attr(original, 'placeholder');
+
+ if (placeholder) {
+ sourceEditor.placeholder = placeholder;
+ attr(wysiwygBody, 'placeholder', placeholder);
+ }
+ };
+
+ /**
+ * Initialises options
+ * @private
+ */
+ initOptions = function () {
+ // auto-update original textbox on blur if option set to true
+ if (options.autoUpdate) {
+ on(wysiwygBody, 'blur', autoUpdate);
+ on(sourceEditor, 'blur', autoUpdate);
+ }
+
+ if (options.rtl === null) {
+ options.rtl = css(sourceEditor, 'direction') === 'rtl';
+ }
+
+ base.rtl(!!options.rtl);
+
+ if (options.autoExpand) {
+ // Need to update when images (or anything else) loads
+ on(wysiwygBody, 'load', autoExpand, EVENT_CAPTURE);
+ on(wysiwygBody, 'input keyup', autoExpand);
+ }
+
+ if (options.resizeEnabled) {
+ initResize();
+ }
+
+ attr(editorContainer, 'id', options.id);
+ base.emoticons(options.emoticonsEnabled);
+ };
+
+ /**
+ * Initialises events
+ * @private
+ */
+ initEvents = function () {
+ var form = original.form;
+ var compositionEvents = 'compositionstart compositionend';
+ var eventsToForward = 'keydown keyup keypress focus blur contextmenu';
+ var checkSelectionEvents = 'onselectionchange' in wysiwygDocument ?
+ 'selectionchange' :
+ 'keyup focus blur contextmenu mouseup touchend click';
+
+ on(globalDoc, 'click', handleDocumentClick);
+
+ if (form) {
+ on(form, 'reset', handleFormReset);
+ on(form, 'submit', base.updateOriginal, EVENT_CAPTURE);
+ }
+
+ on(wysiwygBody, 'keypress', handleKeyPress);
+ on(wysiwygBody, 'keydown', handleKeyDown);
+ on(wysiwygBody, 'keydown', handleBackSpace);
+ on(wysiwygBody, 'keyup', appendNewLine);
+ on(wysiwygBody, 'blur', valueChangedBlur);
+ on(wysiwygBody, 'keyup', valueChangedKeyUp);
+ on(wysiwygBody, 'paste', handlePasteEvt);
+ on(wysiwygBody, compositionEvents, handleComposition);
+ on(wysiwygBody, checkSelectionEvents, checkSelectionChanged);
+ on(wysiwygBody, eventsToForward, handleEvent);
+
+ if (options.emoticonsCompat && globalWin.getSelection) {
+ on(wysiwygBody, 'keyup', emoticonsCheckWhitespace);
+ }
+
+ on(wysiwygBody, 'blur', function () {
+ if (!base.val()) {
+ addClass(wysiwygBody, 'placeholder');
+ }
+ });
+
+ on(wysiwygBody, 'focus', function () {
+ removeClass(wysiwygBody, 'placeholder');
+ });
+
+ on(sourceEditor, 'blur', valueChangedBlur);
+ on(sourceEditor, 'keyup', valueChangedKeyUp);
+ on(sourceEditor, 'keydown', handleKeyDown);
+ on(sourceEditor, compositionEvents, handleComposition);
+ on(sourceEditor, eventsToForward, handleEvent);
+
+ on(wysiwygDocument, 'mousedown', handleMouseDown);
+ on(wysiwygDocument, checkSelectionEvents, checkSelectionChanged);
+ on(wysiwygDocument, 'beforedeactivate keyup mouseup', saveRange);
+ on(wysiwygDocument, 'keyup', appendNewLine);
+ on(wysiwygDocument, 'focus', function () {
+ lastRange = null;
+ });
+
+ on(editorContainer, 'selectionchanged', checkNodeChanged);
+ on(editorContainer, 'selectionchanged', updateActiveButtons);
+ // Custom events to forward
+ on(
+ editorContainer,
+ 'selectionchanged valuechanged nodechanged pasteraw paste',
+ 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 = createElement('div', {
+ className: 'sceditor-toolbar',
+ unselectable: 'on'
+ });
+
+ if (options.icons in SCEditor.icons) {
+ icons = new SCEditor.icons[options.icons]();
+ }
+
+ each(groups, function (_, menuItems) {
+ group = createElement('div', {
+ className: 'sceditor-group'
+ });
+
+ each(menuItems.split(','), function (_, commandName) {
+ var button, shortcut,
+ command = commands[commandName];
+
+ // The commandName must be a valid command and not excluded
+ if (!command || exclude.indexOf(commandName) > -1) {
+ return;
+ }
+
+ shortcut = command.shortcut;
+ button = _tmpl('toolbarButton', {
+ name: commandName,
+ dispName: base._(command.name ||
+ command.tooltip || commandName)
+ }, true).firstChild;
+
+ if (icons && icons.create) {
+ var icon = icons.create(commandName);
+ if (icon) {
+ insertBefore(icons.create(commandName),
+ button.firstChild);
+ addClass(button, 'has-icon');
+ }
+ }
+
+ button._sceTxtMode = !!command.txtExec;
+ button._sceWysiwygMode = !!command.exec;
+ toggleClass(button, 'disabled', !command.exec);
+ on(button, 'click', function (e) {
+ if (!hasClass(button, 'disabled')) {
+ handleCommand(button, command);
+ }
+
+ updateActiveButtons();
+ e.preventDefault();
+ });
+ // Prevent editor losing focus when button clicked
+ on(button, 'mousedown', function (e) {
+ base.closeDropDown();
+ e.preventDefault();
+ });
+
+ if (command.tooltip) {
+ attr(button, '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 (isString(command.exec)) {
+ btnStateHandlers.push({
+ name: commandName,
+ state: command.exec
+ });
+ }
+
+ appendChild(group, button);
+ toolbarButtons[commandName] = button;
+ });
+
+ // Exclude empty groups
+ if (group.firstChild) {
+ appendChild(toolbar, group);
+ }
+ });
+
+ // Append the toolbar to the toolbarContainer option if given
+ appendChild(options.toolbarContainer || editorContainer, toolbar);
+ };
+
+ /**
+ * Creates the resizer.
+ * @private
+ */
+ initResize = function () {
+ var minHeight, maxHeight, minWidth, maxWidth,
+ mouseMoveFunc, mouseUpFunc,
+ grip = createElement('div', {
+ className: 'sceditor-grip'
+ }),
+ // Cover is used to cover the editor iframe so document
+ // still gets mouse move events
+ cover = createElement('div', {
+ className: 'sceditor-resize-cover'
+ }),
+ moveEvents = 'touchmove mousemove',
+ endEvents = 'touchcancel touchend mouseup',
+ startX = 0,
+ startY = 0,
+ newX = 0,
+ newY = 0,
+ startWidth = 0,
+ startHeight = 0,
+ origWidth = width(editorContainer),
+ origHeight = height(editorContainer),
+ 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);
+ }
+
+ e.preventDefault();
+ };
+
+ mouseUpFunc = function (e) {
+ if (!isDragging) {
+ return;
+ }
+
+ isDragging = false;
+
+ hide(cover);
+ removeClass(editorContainer, 'resizing');
+ off(globalDoc, moveEvents, mouseMoveFunc);
+ off(globalDoc, endEvents, mouseUpFunc);
+
+ e.preventDefault();
+ };
+
+ if (icons && icons.create) {
+ var icon = icons.create('grip');
+ if (icon) {
+ appendChild(grip, icon);
+ addClass(grip, 'has-icon');
+ }
+ }
+
+ appendChild(editorContainer, grip);
+ appendChild(editorContainer, cover);
+ hide(cover);
+
+ on(grip, '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 = width(editorContainer);
+ startHeight = height(editorContainer);
+ isDragging = true;
+
+ addClass(editorContainer, 'resizing');
+ show(cover);
+ on(globalDoc, moveEvents, mouseMoveFunc);
+ on(globalDoc, endEvents, mouseUpFunc);
+
+ e.preventDefault();
+ });
+ };
+
+ /**
+ * Prefixes and preloads the emoticon images
+ * @private
+ */
+ initEmoticons = function () {
+ var emoticons = options.emoticons;
+ var root = options.emoticonsRoot || '';
+
+ if (emoticons) {
+ allEmoticons = extend(
+ {}, emoticons.more, emoticons.dropdown, emoticons.hidden
+ );
+ }
+
+ each(allEmoticons, function (key, url) {
+ allEmoticons[key] = _tmpl('emoticon', {
+ key: key,
+ // Prefix emoticon root to emoticon urls
+ url: root + (url.url || url),
+ tooltip: url.tooltip || key
+ });
+
+ // Preload the emoticon
+ if (options.emoticonsEnabled) {
+ preLoadCache.push(createElement('img', {
+ src: root + (url.url || url)
+ }));
+ }
+ });
+ };
+
+ /**
+ * Autofocus the editor
+ * @private
+ */
+ autofocus = function () {
+ var range, txtPos,
+ node = wysiwygBody.firstChild,
+ focusEnd = !!options.autofocusEnd;
+
+ // Can't focus invisible elements
+ if (!isVisible(editorContainer)) {
+ return;
+ }
+
+ if (base.sourceMode()) {
+ txtPos = focusEnd ? sourceEditor.value.length : 0;
+
+ sourceEditor.setSelectionRange(txtPos, txtPos);
+
+ return;
+ }
+
+ removeWhiteSpace(wysiwygBody);
+
+ if (focusEnd) {
+ if (!(node = wysiwygBody.lastChild)) {
+ node = createElement('p', {}, wysiwygDocument);
+ appendChild(wysiwygBody, 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$2 && is(node, 'br') && node.previousSibling) {
+ node = node.previousSibling;
+ }
+ }
+ }
+
+ range = wysiwygDocument.createRange();
+
+ if (!canHaveChildren(node)) {
+ range.setStartBefore(node);
+
+ if (focusEnd) {
+ range.setStartAfter(node);
+ }
+ } else {
+ range.selectNodeContents(node);
+ }
+
+ range.collapse(!focusEnd);
+ rangeHelper.selectRange(range);
+ currentSelection = range;
+
+ if (focusEnd) {
+ wysiwygBody.scrollTop = wysiwygBody.scrollHeight;
+ }
+
+ base.focus();
+ };
+
+ /**
+ * Gets if the editor is read only
+ *
+ * @since 1.3.5
+ * @function
+ * @memberOf SCEditor.prototype
+ * @name readOnly
+ * @return {boolean}
+ */
+ /**
+ * Sets if the editor is read only
+ *
+ * @param {boolean} readOnly
+ * @since 1.3.5
+ * @function
+ * @memberOf SCEditor.prototype
+ * @name readOnly^2
+ * @return {this}
+ */
+ base.readOnly = function (readOnly) {
+ if (typeof readOnly !== 'boolean') {
+ return !sourceEditor.readonly;
+ }
+
+ wysiwygBody.contentEditable = !readOnly;
+ sourceEditor.readonly = !readOnly;
+
+ updateToolBar(readOnly);
+
+ return base;
+ };
+
+ /**
+ * Gets if the editor is in RTL mode
+ *
+ * @since 1.4.1
+ * @function
+ * @memberOf SCEditor.prototype
+ * @name rtl
+ * @return {boolean}
+ */
+ /**
+ * Sets if the editor is in RTL mode
+ *
+ * @param {boolean} rtl
+ * @since 1.4.1
+ * @function
+ * @memberOf SCEditor.prototype
+ * @name rtl^2
+ * @return {this}
+ */
+ base.rtl = function (rtl) {
+ var dir = rtl ? 'rtl' : 'ltr';
+
+ if (typeof rtl !== 'boolean') {
+ return attr(sourceEditor, 'dir') === 'rtl';
+ }
+
+ attr(wysiwygBody, 'dir', dir);
+ attr(sourceEditor, 'dir', dir);
+
+ removeClass(editorContainer, 'rtl');
+ removeClass(editorContainer, 'ltr');
+ addClass(editorContainer, dir);
+
+ if (icons && icons.rtl) {
+ icons.rtl(rtl);
+ }
+
+ return base;
+ };
+
+ /**
+ * Updates the toolbar to disable/enable the appropriate buttons
+ * @private
+ */
+ updateToolBar = function (disable) {
+ var mode = base.inSourceMode() ? '_sceTxtMode' : '_sceWysiwygMode';
+
+ each(toolbarButtons, function (_, button) {
+ toggleClass(button, 'disabled', disable || !button[mode]);
+ });
+ };
+
+ /**
+ * Gets the width of the editor in pixels
+ *
+ * @since 1.3.5
+ * @function
+ * @memberOf SCEditor.prototype
+ * @name width
+ * @return {number}
+ */
+ /**
+ * Sets the width of the editor
+ *
+ * @param {number} width Width in pixels
+ * @since 1.3.5
+ * @function
+ * @memberOf 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 {number} width Width in pixels
+ * @param {boolean} [saveWidth=true] If to store the width
+ * @since 1.4.1
+ * @function
+ * @memberOf SCEditor.prototype
+ * @name width^3
+ * @return {this}
+ */
+ base.width = function (width$$1, saveWidth) {
+ if (!width$$1 && width$$1 !== 0) {
+ return width(editorContainer);
+ }
+
+ base.dimensions(width$$1, 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 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 {number} width Width in px
+ * @param {number} height Height in px
+ * @since 1.4.1
+ * @function
+ * @memberOf 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 {number} width Width in px
+ * @param {number} height Height in px
+ * @param {boolean} [save=true] If to store the new sizes
+ * @since 1.4.1
+ * @function
+ * @memberOf SCEditor.prototype
+ * @name dimensions^3
+ * @return {this}
+ */
+ base.dimensions = function (width$$1, height$$1, save) {
+ // set undefined width/height to boolean false
+ width$$1 = (!width$$1 && width$$1 !== 0) ? false : width$$1;
+ height$$1 = (!height$$1 && height$$1 !== 0) ? false : height$$1;
+
+ if (width$$1 === false && height$$1 === false) {
+ return { width: base.width(), height: base.height() };
+ }
+
+ if (width$$1 !== false) {
+ if (save !== false) {
+ options.width = width$$1;
+ }
+
+ width(editorContainer, width$$1);
+ }
+
+ if (height$$1 !== false) {
+ if (save !== false) {
+ options.height = height$$1;
+ }
+
+ height(editorContainer, height$$1);
+ }
+
+ return base;
+ };
+
+ /**
+ * Gets the height of the editor in px
+ *
+ * @since 1.3.5
+ * @function
+ * @memberOf SCEditor.prototype
+ * @name height
+ * @return {number}
+ */
+ /**
+ * Sets the height of the editor
+ *
+ * @param {number} height Height in px
+ * @since 1.3.5
+ * @function
+ * @memberOf 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 {number} height Height in px
+ * @param {boolean} [saveHeight=true] If to store the height
+ * @since 1.4.1
+ * @function
+ * @memberOf SCEditor.prototype
+ * @name height^3
+ * @return {this}
+ */
+ base.height = function (height$$1, saveHeight) {
+ if (!height$$1 && height$$1 !== 0) {
+ return height(editorContainer);
+ }
+
+ base.dimensions(null, height$$1, saveHeight);
+
+ return base;
+ };
+
+ /**
+ * Gets if the editor is maximised or not
+ *
+ * @since 1.4.1
+ * @function
+ * @memberOf 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 SCEditor.prototype
+ * @name maximize^2
+ * @return {this}
+ */
+ base.maximize = function (maximize) {
+ var maximizeSize = 'sceditor-maximize';
+
+ if (isUndefined(maximize)) {
+ return hasClass(editorContainer, maximizeSize);
+ }
+
+ maximize = !!maximize;
+
+ if (maximize) {
+ maximizeScrollPosition = globalWin.pageYOffset;
+ }
+
+ toggleClass(globalDoc.documentElement, maximizeSize, maximize);
+ toggleClass(globalDoc.body, maximizeSize, maximize);
+ toggleClass(editorContainer, maximizeSize, maximize);
+ base.width(maximize ? '100%' : options.width, false);
+ base.height(maximize ? '100%' : options.height, false);
+
+ if (!maximize) {
+ globalWin.scrollTo(0, maximizeScrollPosition);
+ }
+
+ autoExpand();
+
+ return base;
+ };
+
+ autoExpand = function () {
+ if (options.autoExpand && !autoExpandThrottle) {
+ autoExpandThrottle = setTimeout(base.expandToContent, 200);
+ }
+ };
+
+ /**
+ * Expands or shrinks 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 SCEditor.prototype
+ * @see #resizeToContent
+ */
+ base.expandToContent = function (ignoreMaxHeight) {
+ if (base.maximize()) {
+ return;
+ }
+
+ clearTimeout(autoExpandThrottle);
+ autoExpandThrottle = false;
+
+ if (!autoExpandBounds) {
+ var height$$1 = options.resizeMinHeight || options.height ||
+ height(original);
+
+ autoExpandBounds = {
+ min: height$$1,
+ max: options.resizeMaxHeight || (height$$1 * 2)
+ };
+ }
+
+ var range = globalDoc.createRange();
+ range.selectNodeContents(wysiwygBody);
+
+ var rect = range.getBoundingClientRect();
+ var current = wysiwygDocument.documentElement.clientHeight - 1;
+ var spaceNeeded = rect.bottom - rect.top;
+ var newHeight = base.height() + 1 + (spaceNeeded - current);
+
+ if (!ignoreMaxHeight && autoExpandBounds.max !== -1) {
+ newHeight = Math.min(newHeight, autoExpandBounds.max);
+ }
+
+ base.height(Math.ceil(Math.max(newHeight, autoExpandBounds.min)));
+ };
+
+ /**
+ * Destroys the editor, removing all elements and
+ * event handlers.
+ *
+ * Leaves only the original textarea.
+ *
+ * @function
+ * @name destroy
+ * @memberOf 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) {
+ remove(dropdown);
+ }
+
+ off(globalDoc, 'click', handleDocumentClick);
+
+ // TODO: make off support null nodes?
+ var form = original.form;
+ if (form) {
+ off(form, 'reset', handleFormReset);
+ off(form, 'submit', base.updateOriginal);
+ }
+
+ remove(sourceEditor);
+ remove(toolbar);
+ remove(editorContainer);
+
+ delete original._sceditor;
+ show(original);
+
+ original.required = isRequired;
+ };
+
+
+ /**
+ * 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 {boolean} 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 SCEditor.prototype
+ */
+ base.createDropDown = function (menuItem, name, content, ieFix) {
+ // first click for create second click for close
+ var dropDownCss,
+ dropDownClass = 'sceditor-' + name;
+
+ // Will re-focus the editor. This is needed for IE
+ // as it has special logic to save/restore the selection
+ base.closeDropDown(true);
+
+ // Only close the dropdown if it was already open
+ if (dropdown && hasClass(dropdown, dropDownClass)) {
+ 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) {
+ each(find(content, ':not(input):not(textarea)'),
+ function (_, node) {
+ if (node.nodeType === ELEMENT_NODE) {
+ attr(node, 'unselectable', 'on');
+ }
+ });
+ }
+
+ dropDownCss = extend({
+ top: menuItem.offsetTop,
+ left: menuItem.offsetLeft,
+ marginTop: menuItem.clientHeight
+ }, options.dropDownCss);
+
+ dropdown = createElement('div', {
+ className: 'sceditor-dropdown ' + dropDownClass
+ });
+
+ css(dropdown, dropDownCss);
+ appendChild(dropdown, content);
+ appendChild(editorContainer, dropdown);
+ on(dropdown, '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) {
+ var first = find(dropdown, 'input,textarea')[0];
+ if (first) {
+ first.focus();
+ }
+ }
+ });
+ };
+
+ /**
+ * Handles any document click and closes the dropdown if open
+ * @private
+ */
+ handleDocumentClick = function (e) {
+ // ignore right clicks
+ if (e.which !== 3 && dropdown && !e.defaultPrevented) {
+ autoUpdate();
+
+ base.closeDropDown();
+ }
+ };
+
+ /**
+ * Handles the WYSIWYG editors paste event
+ * @private
+ */
+ handlePasteEvt = function (e) {
+ var isIeOrEdge = IE_VER || edge;
+ var editable = wysiwygBody;
+ var clipboard = e.clipboardData;
+ var loadImage = function (file) {
+ var reader = new FileReader();
+ reader.onload = function (e) {
+ handlePasteData({
+ html: ' '
+ });
+ };
+ reader.readAsDataURL(file);
+ };
+
+ // Modern browsers with clipboard API - everything other than _very_
+ // old android web views and UC browser which doesn't support the
+ // paste event at all.
+ if (clipboard && !isIeOrEdge) {
+ var data$$1 = {};
+ var types = clipboard.types;
+ var items = clipboard.items;
+
+ e.preventDefault();
+
+ for (var i = 0; i < types.length; i++) {
+ // Normalise image pasting to paste as a data-uri
+ if (globalWin.FileReader && items &&
+ IMAGE_MIME_REGEX.test(items[i].type)) {
+ return loadImage(clipboard.items[i].getAsFile());
+ }
+
+ data$$1[types[i]] = clipboard.getData(types[i]);
+ }
+ // Call plugins here with file?
+ data$$1.text = data$$1['text/plain'];
+ data$$1.html = data$$1['text/html'];
+
+ handlePasteData(data$$1);
+ // If contentsFragment exists then we are already waiting for a
+ // previous paste so let the handler for that handle this one too
+ } else if (!pasteContentFragment) {
+ // Save the scroll position so can be restored
+ // when contents is restored
+ var scrollTop = editable.scrollTop;
+
+ rangeHelper.saveRange();
+
+ pasteContentFragment = globalDoc.createDocumentFragment();
+ while (editable.firstChild) {
+ appendChild(pasteContentFragment, editable.firstChild);
+ }
+
+ setTimeout(function () {
+ var html = editable.innerHTML;
+
+ editable.innerHTML = '';
+ appendChild(editable, pasteContentFragment);
+ editable.scrollTop = scrollTop;
+ pasteContentFragment = false;
+
+ rangeHelper.restoreRange();
+
+ handlePasteData({ html: html });
+ }, 0);
+ }
+ };
+
+ /**
+ * Gets the pasted data, filters it and then inserts it.
+ * @param {Object} data
+ * @private
+ */
+ handlePasteData = function (data$$1) {
+ var pasteArea = createElement('div', {}, wysiwygDocument);
+
+ pluginManager.call('pasteRaw', data$$1);
+ trigger(editorContainer, 'pasteraw', data$$1);
+
+ if (data$$1.html) {
+ pasteArea.innerHTML = data$$1.html;
+
+ // fix any invalid nesting
+ fixNesting(pasteArea);
+ } else {
+ pasteArea.innerHTML = entities(data$$1.text || '');
+ }
+
+ var paste = {
+ val: pasteArea.innerHTML
+ };
+
+ if ('fragmentToSource' in format) {
+ paste.val = format
+ .fragmentToSource(paste.val, wysiwygDocument, currentNode);
+ }
+
+ pluginManager.call('paste', paste);
+ trigger(editorContainer, 'paste', paste);
+
+ if ('fragmentToHtml' in format) {
+ paste.val = format
+ .fragmentToHtml(paste.val, currentNode);
+ }
+
+ pluginManager.call('pasteHtml', paste);
+
+ base.wysiwygEditorInsertHtml(paste.val, null, true);
+ };
+
+ /**
+ * Closes any currently open drop down
+ *
+ * @param {boolean} [focus=false] If to focus the editor
+ * after closing the drop down
+ * @function
+ * @name closeDropDown
+ * @memberOf SCEditor.prototype
+ */
+ base.closeDropDown = function (focus) {
+ if (dropdown) {
+ remove(dropdown);
+ dropdown = null;
+ }
+
+ if (focus === true) {
+ base.focus();
+ }
+ };
+
+
+ /**
+ * 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 SCEditor.prototype
+ */
+ base.wysiwygEditorInsertHtml = function (
+ html, endHtml, overrideCodeBlocking
+ ) {
+ var marker, scrollTop, scrollTo,
+ editorHeight = height(wysiwygEditor);
+
+ 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 && closest(currentBlockNode, 'code')) {
+ 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();
+
+ // Scroll the editor after the end of the selection
+ marker = find(wysiwygBody, '#sceditor-end-marker')[0];
+ show(marker);
+ scrollTop = wysiwygBody.scrollTop;
+ scrollTo = (getOffset(marker).top +
+ (marker.offsetHeight * 1.5)) - editorHeight;
+ hide(marker);
+
+ // Only scroll if marker isn't already visible
+ if (scrollTo > scrollTop || scrollTo + editorHeight < scrollTop) {
+ wysiwygBody.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 SCEditor.prototype
+ */
+ base.wysiwygEditorInsertText = function (text, endText) {
+ base.wysiwygEditorInsertHtml(
+ entities(text), 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 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 SCEditor.prototype
+ */
+ base.sourceEditorInsertText = function (text, endText) {
+ var scrollTop, currentValue,
+ startPos = sourceEditor.selectionStart,
+ endPos = sourceEditor.selectionEnd;
+
+ scrollTop = sourceEditor.scrollTop;
+ sourceEditor.focus();
+ 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;
+
+ sourceEditor.scrollTop = scrollTop;
+ sourceEditor.focus();
+
+ triggerValueChanged();
+ };
+
+ /**
+ * Gets the current instance of the rangeHelper class
+ * for the editor.
+ *
+ * @return {RangeHelper}
+ * @function
+ * @name getRangeHelper
+ * @memberOf SCEditor.prototype
+ */
+ base.getRangeHelper = function () {
+ return rangeHelper;
+ };
+
+ /**
+ * Gets or sets the source editor caret position.
+ *
+ * @param {Object} [position]
+ * @return {this}
+ * @function
+ * @since 1.4.5
+ * @name sourceEditorCaret
+ * @memberOf SCEditor.prototype
+ */
+ base.sourceEditorCaret = function (position) {
+ sourceEditor.focus();
+
+ if (position) {
+ sourceEditor.selectionStart = position.start;
+ sourceEditor.selectionEnd = position.end;
+
+ return this;
+ }
+
+ return {
+ start: sourceEditor.selectionStart,
+ end: sourceEditor.selectionEnd
+ };
+ };
+
+ /**
+ * 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 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 SCEditor.prototype
+ */
+ base.val = function (val, filter) {
+ if (!isString(val)) {
+ return base.inSourceMode() ?
+ base.getSourceEditorValue(false) :
+ base.getWysiwygEditorValue(filter);
+ }
+
+ if (!base.inSourceMode()) {
+ if (filter !== false && 'toHtml' in format) {
+ val = format.toHtml(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 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 SCEditor.prototype
+ */
+ // eslint-disable-next-line max-params
+ base.insert = function (
+ 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();
+
+ if (filter !== false && 'fragmentToSource' in format) {
+ html = format
+ .fragmentToSource(html, wysiwygDocument, currentNode);
+ }
+
+ start += html + end;
+ }
+ // TODO: This filter should allow empty tags as it's inserting.
+ if (filter !== false && 'fragmentToHtml' in format) {
+ start = format.fragmentToHtml(start, currentNode);
+ }
+
+ // 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 {boolean} [filter=true]
+ * @return {string}
+ * @function
+ * @name getWysiwygEditorValue
+ * @memberOf 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 = createElement('div', {}, wysiwygDocument);
+ var childNodes = wysiwygBody.childNodes;
+
+ for (var i = 0; i < childNodes.length; i++) {
+ appendChild(tmp, childNodes[i].cloneNode(true));
+ }
+
+ appendChild(wysiwygBody, tmp);
+ fixNesting(tmp);
+ remove(tmp);
+
+ html = tmp.innerHTML;
+
+ // filter the HTML and DOM through any plugins
+ if (filter !== false && format.hasOwnProperty('toSource')) {
+ html = format.toSource(html, wysiwygDocument);
+ }
+
+ return html;
+ };
+
+ /**
+ * Gets the WYSIWYG editor's iFrame Body.
+ *
+ * @return {HTMLElement}
+ * @function
+ * @since 1.4.3
+ * @name getBody
+ * @memberOf SCEditor.prototype
+ */
+ base.getBody = function () {
+ return wysiwygBody;
+ };
+
+ /**
+ * Gets the WYSIWYG editors container area (whole iFrame).
+ *
+ * @return {HTMLElement}
+ * @function
+ * @since 1.4.3
+ * @name getContentAreaContainer
+ * @memberOf 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 {boolean} [filter=true]
+ * @return {string}
+ * @function
+ * @since 1.4.0
+ * @name getSourceEditorValue
+ * @memberOf SCEditor.prototype
+ */
+ base.getSourceEditorValue = function (filter) {
+ var val = sourceEditor.value;
+
+ if (filter !== false && 'toHtml' in format) {
+ val = format.toHtml(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 SCEditor.prototype
+ */
+ base.setWysiwygEditorValue = function (value) {
+ if (!value) {
+ value = '' + (IE_VER ? '' : ' ') + '
';
+ }
+
+ wysiwygBody.innerHTML = value;
+ replaceEmoticons();
+
+ appendNewLine();
+ triggerValueChanged();
+ autoExpand();
+ };
+
+ /**
+ * Sets the text editor value
+ *
+ * @param {string} value
+ * @function
+ * @name setSourceEditorValue
+ * @memberOf SCEditor.prototype
+ */
+ base.setSourceEditorValue = function (value) {
+ sourceEditor.value = 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 SCEditor.prototype
+ */
+ base.updateOriginal = function () {
+ original.value = base.val();
+ };
+
+ /**
+ * Replaces any emoticon codes in the passed HTML
+ * with their emoticon images
+ * @private
+ */
+ replaceEmoticons = function () {
+ if (options.emoticonsEnabled) {
+ replace(wysiwygBody, allEmoticons, options.emoticonsCompat);
+ }
+ };
+
+ /**
+ * If the editor is in source code mode
+ *
+ * @return {boolean}
+ * @function
+ * @name inSourceMode
+ * @memberOf SCEditor.prototype
+ */
+ base.inSourceMode = function () {
+ return hasClass(editorContainer, 'sourceMode');
+ };
+
+ /**
+ * Gets if the editor is in sourceMode
+ *
+ * @return boolean
+ * @function
+ * @name sourceMode
+ * @memberOf SCEditor.prototype
+ */
+ /**
+ * Sets if the editor is in sourceMode
+ *
+ * @param {boolean} enable
+ * @return {this}
+ * @function
+ * @name sourceMode^2
+ * @memberOf 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 SCEditor.prototype
+ */
+ base.toggleSourceMode = function () {
+ var isInSourceMode = base.inSourceMode();
+
+ // don't allow switching to WYSIWYG if doesn't support it
+ if (!isWysiwygSupported && isInSourceMode) {
+ return;
+ }
+
+ if (!isInSourceMode) {
+ rangeHelper.saveRange();
+ rangeHelper.clear();
+ }
+
+ base.blur();
+
+ if (isInSourceMode) {
+ base.setWysiwygEditorValue(base.getSourceEditorValue());
+ } else {
+ base.setSourceEditorValue(base.getWysiwygEditorValue());
+ }
+
+ lastRange = null;
+ toggle(sourceEditor);
+ toggle(wysiwygEditor);
+
+ toggleClass(editorContainer, 'wysiwygMode', isInSourceMode);
+ toggleClass(editorContainer, 'sourceMode', !isInSourceMode);
+
+ updateToolBar();
+ updateActiveButtons();
+ };
+
+ /**
+ * Gets the selected text of the source editor
+ * @return {string}
+ * @private
+ */
+ sourceEditorSelectedText = function () {
+ sourceEditor.focus();
+
+ return sourceEditor.value.substring(
+ sourceEditor.selectionStart,
+ sourceEditor.selectionEnd
+ );
+ };
+
+ /**
+ * 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 (Array.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 SCEditor.prototype
+ */
+ base.execCommand = function (command, param) {
+ var executed = false,
+ commandObj = base.commands[command];
+
+ base.focus();
+
+ // TODO: make configurable
+ // don't apply any commands to code elements
+ if (closest(rangeHelper.parentNode(), 'code')) {
+ return;
+ }
+
+ try {
+ executed = wysiwygDocument.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() {
+ // Don't create new selection if there isn't one (like after
+ // blur event in iOS)
+ if (wysiwygWindow.getSelection() &&
+ wysiwygWindow.getSelection().rangeCount <= 0) {
+ currentSelection = null;
+ // rangeHelper could be null if editor was destroyed
+ // before the timeout had finished
+ } else if (rangeHelper && !rangeHelper.compare(currentSelection)) {
+ currentSelection = rangeHelper.cloneSelected();
+
+ // If the selection is in an inline wrap it in a block.
+ // Fixes #331
+ if (currentSelection && currentSelection.collapsed) {
+ var parent$$1 = currentSelection.startContainer;
+ var offset = currentSelection.startOffset;
+
+ // Handle if selection is placed before/after an element
+ if (offset && parent$$1.nodeType !== TEXT_NODE) {
+ parent$$1 = parent$$1.childNodes[offset];
+ }
+
+ while (parent$$1 && parent$$1.parentNode !== wysiwygBody) {
+ parent$$1 = parent$$1.parentNode;
+ }
+
+ if (parent$$1 && isInline(parent$$1, true)) {
+ rangeHelper.saveRange();
+ wrapInlines(wysiwygBody, wysiwygDocument);
+ rangeHelper.restoreRange();
+ }
+ }
+
+ trigger(editorContainer, 'selectionchanged');
+ }
+
+ isSelectionCheckPending = false;
+ }
+
+ if (isSelectionCheckPending) {
+ return;
+ }
+
+ isSelectionCheckPending = true;
+
+ // Don't need to limit checking if browser supports the Selection API
+ if ('onselectionchange' in wysiwygDocument) {
+ 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);
+
+ trigger(editorContainer, '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 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 SCEditor.prototype
+ * @since 1.4.4
+ */
+ base.currentBlockNode = function () {
+ return currentBlockNode;
+ };
+
+ /**
+ * Updates if buttons are active or not
+ * @private
+ */
+ updateActiveButtons = function () {
+ var firstBlock, parent$$1;
+ var activeClass = 'active';
+ var doc = wysiwygDocument;
+ var isSource = base.sourceMode();
+
+ if (base.readOnly()) {
+ each(find(toolbar, activeClass), function (_, menuItem) {
+ removeClass(menuItem, activeClass);
+ });
+ return;
+ }
+
+ if (!isSource) {
+ parent$$1 = rangeHelper.parentNode();
+ firstBlock = rangeHelper.getFirstBlockParent(parent$$1);
+ }
+
+ for (var j = 0; j < btnStateHandlers.length; j++) {
+ var state = 0;
+ var btn = toolbarButtons[btnStateHandlers[j].name];
+ var stateFn = btnStateHandlers[j].state;
+ var isDisabled = (isSource && !btn._sceTxtMode) ||
+ (!isSource && !btn._sceWysiwygMode);
+
+ if (isString(stateFn)) {
+ if (!isSource) {
+ try {
+ state = doc.queryCommandEnabled(stateFn) ? 0 : -1;
+
+ // eslint-disable-next-line max-depth
+ if (state > -1) {
+ state = doc.queryCommandState(stateFn) ? 1 : 0;
+ }
+ } catch (ex) {}
+ }
+ } else if (!isDisabled) {
+ state = stateFn.call(base, parent$$1, firstBlock);
+ }
+
+ toggleClass(btn, 'disabled', isDisabled || state < 0);
+ toggleClass(btn, activeClass, state > 0);
+ }
+
+ if (icons && icons.update) {
+ icons.update(isSource, parent$$1, firstBlock);
+ }
+ };
+
+ /**
+ * Handles any key press in the WYSIWYG editor
+ *
+ * @private
+ */
+ handleKeyPress = function (e) {
+ // FF bug: https://bugzilla.mozilla.org/show_bug.cgi?id=501496
+ if (e.defaultPrevented) {
+ return;
+ }
+
+ base.closeDropDown();
+
+ // 13 = enter key
+ if (e.which === 13) {
+ var LIST_TAGS = 'li,ul,ol';
+
+ // "Fix" (cludge) for blocklevel elements being duplicated in some
+ // browsers when enter is pressed instead of inserting a newline
+ if (!is(currentBlockNode, LIST_TAGS) &&
+ hasStyling(currentBlockNode)) {
+ lastRange = null;
+
+ var br = createElement('br', {}, wysiwygDocument);
+ 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$2) {
+ var parent$$1 = br.parentNode;
+ var lastChild = parent$$1.lastChild;
+
+ // Sometimes an empty next node is created after the
+ if (lastChild && lastChild.nodeType === TEXT_NODE &&
+ lastChild.nodeValue === '') {
+ remove(lastChild);
+ lastChild = parent$$1.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 (!isInline(parent$$1, true) && lastChild === br &&
+ isInline(br.previousSibling)) {
+ rangeHelper.insertHTML(' ');
+ }
+ }
+
+ e.preventDefault();
+ }
+ }
+ };
+
+ /**
+ * 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 () {
+ // Check all nodes in reverse until either add a new line
+ // or reach a non-empty textnode or BR at which point can
+ // stop checking.
+ rTraverse(wysiwygBody, function (node) {
+ // Last block, add new line after if has styling
+ if (node.nodeType === ELEMENT_NODE &&
+ !/inline/.test(css(node, 'display'))) {
+
+ // Add line break after if has styling
+ if (!is(node, '.sceditor-nlf') && hasStyling(node)) {
+ var paragraph = createElement('p', {}, wysiwygDocument);
+ paragraph.className = 'sceditor-nlf';
+ paragraph.innerHTML = !IE_BR_FIX$2 ? ' ' : '';
+ appendChild(wysiwygBody, paragraph);
+ return false;
+ }
+ }
+
+ // Last non-empty text node or line break.
+ // No need to add line-break after them
+ if ((node.nodeType === 3 && !/^\s*$/.test(node.nodeValue)) ||
+ is(node, 'br')) {
+ return false;
+ }
+ });
+ };
+
+ /**
+ * Handles form reset event
+ * @private
+ */
+ handleFormReset = function () {
+ base.val(original.value);
+ };
+
+ /**
+ * Handles any mousedown press in the WYSIWYG editor
+ * @private
+ */
+ handleMouseDown = function () {
+ base.closeDropDown();
+ lastRange = null;
+ };
+
+ /**
+ * 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 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) {
+ if (pluginManager) {
+ // Send event to all plugins
+ pluginManager.call(e.type + 'Event', e, base);
+ }
+
+ // convert the event into a custom event to send
+ var name = (e.target === sourceEditor ? 'scesrc' : 'scewys') + e.type;
+
+ if (eventHandlers[name]) {
+ eventHandlers[name].forEach(function (fn) {
+ fn.call(base, e);
+ });
+ }
+ };
+
+ /**
+ * 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 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)) {
+ var wysEvent = 'scewys' + events[i];
+ var srcEvent = 'scesrc' + events[i];
+ // Use custom events to allow passing the instance as the
+ // 2nd argument.
+ // Also allows unbinding without unbinding the editors own
+ // event handlers.
+ if (!excludeWysiwyg) {
+ eventHandlers[wysEvent] = eventHandlers[wysEvent] || [];
+ eventHandlers[wysEvent].push(handler);
+ }
+
+ if (!excludeSource) {
+ eventHandlers[srcEvent] = eventHandlers[srcEvent] || [];
+ eventHandlers[srcEvent].push(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 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) {
+ arrayRemove(
+ eventHandlers['scewys' + events[i]] || [], handler);
+ }
+
+ if (!excludeSource) {
+ arrayRemove(
+ eventHandlers['scesrc' + events[i]] || [], handler);
+ }
+ }
+ }
+
+ return base;
+ };
+
+ /**
+ * Blurs the editors input area
+ *
+ * @return {this}
+ * @function
+ * @name blur
+ * @memberOf 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 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 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 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()) {
+ // Already has focus so do nothing
+ if (find(wysiwygDocument, ':focus').length) {
+ return;
+ }
+
+ var container;
+ var 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) {
+ 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$2 && rng && rng.endOffset === 1 && rng.collapsed) {
+ container = rng.endContainer;
+
+ if (container && container.childNodes.length === 1 &&
+ is(container.firstChild, 'br')) {
+ rng.setStartBefore(container.firstChild);
+ rng.collapse(true);
+ rangeHelper.selectRange(rng);
+ }
+ }
+
+ wysiwygWindow.focus();
+ wysiwygBody.focus();
+
+ // Needed for IE
+ 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 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 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 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 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 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 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 (closest(currentBlockNode, 'code')) {
+ return;
+ }
+
+ if (!emoticonsCache) {
+ emoticonsCache = [];
+
+ each(allEmoticons, function (key, html) {
+ emoticonsCache[cachePos++] = [key, html];
+ });
+
+ 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) {
+ if (!options.emoticonsCompat || !/^\s$/.test(curChar)) {
+ e.preventDefault();
+ }
+ }
+ };
+
+ /**
+ * Makes sure emoticons are surrounded by whitespace
+ * @private
+ */
+ emoticonsCheckWhitespace = function () {
+ checkWhitespace(currentBlockNode, rangeHelper);
+ };
+
+ /**
+ * Gets if emoticons are currently enabled
+ * @return {boolean}
+ * @function
+ * @name emoticons
+ * @memberOf SCEditor.prototype
+ * @since 1.4.2
+ */
+ /**
+ * Enables/disables emoticons
+ *
+ * @param {boolean} enable
+ * @return {this}
+ * @function
+ * @name emoticons^2
+ * @memberOf SCEditor.prototype
+ * @since 1.4.2
+ */
+ base.emoticons = function (enable) {
+ if (!enable && enable !== false) {
+ return options.emoticonsEnabled;
+ }
+
+ options.emoticonsEnabled = enable;
+
+ if (enable) {
+ on(wysiwygBody, 'keypress', emoticonsKeyPress);
+
+ if (!base.sourceMode()) {
+ rangeHelper.saveRange();
+
+ replaceEmoticons();
+ triggerValueChanged(false);
+
+ rangeHelper.restoreRange();
+ }
+ } else {
+ var emoticons =
+ find(wysiwygBody, 'img[data-sceditor-emoticon]');
+
+ each(emoticons, function (_, img) {
+ var text = data(img, 'sceditor-emoticon');
+ var textNode = wysiwygDocument.createTextNode(text);
+ img.parentNode.replaceChild(textNode, img);
+ });
+
+ off(wysiwygBody, 'keypress', emoticonsKeyPress);
+
+ triggerValueChanged();
+ }
+
+ return base;
+ };
+
+ /**
+ * Gets the current WYSIWYG editors inline CSS
+ *
+ * @return {string}
+ * @function
+ * @name css
+ * @memberOf SCEditor.prototype
+ * @since 1.4.3
+ */
+ /**
+ * Sets inline CSS for the WYSIWYG editor
+ *
+ * @param {string} css
+ * @return {this}
+ * @function
+ * @name css^2
+ * @memberOf SCEditor.prototype
+ * @since 1.4.3
+ */
+ base.css = function (css$$1) {
+ if (!inlineCss) {
+ inlineCss = createElement('style', {
+ id: 'inline'
+ }, wysiwygDocument);
+
+ appendChild(wysiwygDocument.head, inlineCss);
+ }
+
+ if (!isString(css$$1)) {
+ return inlineCss.styleSheet ?
+ inlineCss.styleSheet.cssText : inlineCss.innerHTML;
+ }
+
+ if (inlineCss.styleSheet) {
+ inlineCss.styleSheet.cssText = css$$1;
+ } else {
+ inlineCss.innerHTML = css$$1;
+ }
+
+ return base;
+ };
+
+ /**
+ * Handles the keydown event, used for shortcuts
+ * @private
+ */
+ handleKeyDown = function (e) {
+ var shortcut = [],
+ SHIFT_KEYS = {
+ '`': '~',
+ '1': '!',
+ '2': '@',
+ '3': '#',
+ '4': '$',
+ '5': '%',
+ '6': '^',
+ '7': '&',
+ '8': '*',
+ '9': '(',
+ '0': ')',
+ '-': '_',
+ '=': '+',
+ ';': ': ',
+ '\'': '"',
+ ',': '<',
+ '.': '>',
+ '/': '?',
+ '\\': '|',
+ '[': '{',
+ ']': '}'
+ },
+ SPECIAL_KEYS = {
+ 8: 'backspace',
+ 9: 'tab',
+ 13: 'enter',
+ 19: 'pause',
+ 20: 'capslock',
+ 27: 'esc',
+ 32: 'space',
+ 33: 'pageup',
+ 34: 'pagedown',
+ 35: 'end',
+ 36: 'home',
+ 37: 'left',
+ 38: 'up',
+ 39: 'right',
+ 40: 'down',
+ 45: 'insert',
+ 46: 'del',
+ 91: 'win',
+ 92: 'win',
+ 93: 'select',
+ 96: '0',
+ 97: '1',
+ 98: '2',
+ 99: '3',
+ 100: '4',
+ 101: '5',
+ 102: '6',
+ 103: '7',
+ 104: '8',
+ 105: '9',
+ 106: '*',
+ 107: '+',
+ 109: '-',
+ 110: '.',
+ 111: '/',
+ 112: 'f1',
+ 113: 'f2',
+ 114: 'f3',
+ 115: 'f4',
+ 116: 'f5',
+ 117: 'f6',
+ 118: 'f7',
+ 119: 'f8',
+ 120: 'f9',
+ 121: 'f10',
+ 122: 'f11',
+ 123: 'f12',
+ 144: 'numlock',
+ 145: 'scrolllock',
+ 186: ';',
+ 187: '=',
+ 188: ',',
+ 189: '-',
+ 190: '.',
+ 191: '/',
+ 192: '`',
+ 219: '[',
+ 220: '\\',
+ 221: ']',
+ 222: '\''
+ },
+ NUMPAD_SHIFT_KEYS = {
+ 109: '-',
+ 110: 'del',
+ 111: '/',
+ 96: '0',
+ 97: '1',
+ 98: '2',
+ 99: '3',
+ 100: '4',
+ 101: '5',
+ 102: '6',
+ 103: '7',
+ 104: '8',
+ 105: '9'
+ },
+ which = e.which,
+ character = SPECIAL_KEYS[which] ||
+ String.fromCharCode(which).toLowerCase();
+
+ if (e.ctrlKey || e.metaKey) {
+ shortcut.push('ctrl');
+ }
+
+ if (e.altKey) {
+ shortcut.push('alt');
+ }
+
+ if (e.shiftKey) {
+ shortcut.push('shift');
+
+ if (NUMPAD_SHIFT_KEYS[which]) {
+ character = NUMPAD_SHIFT_KEYS[which];
+ } else if (SHIFT_KEYS[character]) {
+ character = SHIFT_KEYS[character];
+ }
+ }
+
+ // Shift is 16, ctrl is 17 and alt is 18
+ if (character && (which < 16 || which > 18)) {
+ shortcut.push(character);
+ }
+
+ shortcut = shortcut.join('+');
+ if (shortcutHandlers[shortcut] &&
+ shortcutHandlers[shortcut].call(base) === false) {
+
+ e.stopPropagation();
+ e.preventDefault();
+ }
+ };
+
+ /**
+ * Adds a shortcut handler to the editor
+ * @param {string} shortcut
+ * @param {String|Function} cmd
+ * @return {sceditor}
+ */
+ base.addShortcut = function (shortcut, cmd) {
+ shortcut = shortcut.toLowerCase();
+
+ if (isString(cmd)) {
+ shortcutHandlers[shortcut] = function () {
+ handleCommand(toolbarButtons[cmd], base.commands[cmd]);
+
+ return false;
+ };
+ } else {
+ shortcutHandlers[shortcut] = cmd;
+ }
+
+ return base;
+ };
+
+ /**
+ * Removes a shortcut handler
+ * @param {string} shortcut
+ * @return {sceditor}
+ */
+ base.removeShortcut = function (shortcut) {
+ delete shortcutHandlers[shortcut.toLowerCase()];
+
+ return base;
+ };
+
+ /**
+ * Handles the backspace key press
+ *
+ * Will remove block styling like quotes/code ect if at the start.
+ * @private
+ */
+ handleBackSpace = function (e) {
+ var node, offset, range, parent$$1;
+
+ // 8 is the backspace key
+ if (options.disableBlockRemove || e.which !== 8 ||
+ !(range = rangeHelper.selectedRange())) {
+ return;
+ }
+
+ node = range.startContainer;
+ offset = range.startOffset;
+
+ if (offset !== 0 || !(parent$$1 = currentStyledBlockNode()) ||
+ is(parent$$1, 'body')) {
+ return;
+ }
+
+ while (node !== parent$$1) {
+ while (node.previousSibling) {
+ node = node.previousSibling;
+
+ // Everything but empty text nodes before the cursor
+ // should prevent the style from being removed
+ if (node.nodeType !== TEXT_NODE || node.nodeValue) {
+ return;
+ }
+ }
+
+ if (!(node = node.parentNode)) {
+ return;
+ }
+ }
+
+ // The backspace was pressed at the start of
+ // the container so clear the style
+ base.clearBlockFormatting(parent$$1);
+ e.preventDefault();
+ };
+
+ /**
+ * Gets the first styled block node that contains the cursor
+ * @return {HTMLElement}
+ */
+ currentStyledBlockNode = function () {
+ var block = currentBlockNode;
+
+ while (!hasStyling(block) || isInline(block, true)) {
+ if (!(block = block.parentNode) || is(block, 'body')) {
+ return;
+ }
+ }
+
+ return block;
+ };
+
+ /**
+ * Clears the formatting of the passed block element.
+ *
+ * If block is false, if will clear the styling of the first
+ * block level element that contains the cursor.
+ * @param {HTMLElement} block
+ * @since 1.4.4
+ */
+ base.clearBlockFormatting = function (block) {
+ block = block || currentStyledBlockNode();
+
+ if (!block || is(block, 'body')) {
+ return base;
+ }
+
+ rangeHelper.saveRange();
+
+ block.className = '';
+ lastRange = null;
+
+ attr(block, 'style', '');
+
+ if (!is(block, 'p,div,td')) {
+ convertElement(block, 'p');
+ }
+
+ rangeHelper.restoreRange();
+ return base;
+ };
+
+ /**
+ * Triggers the valueChanged signal if there is
+ * a plugin that handles it.
+ *
+ * If rangeHelper.saveRange() has already been
+ * called, then saveRange should be set to false
+ * to prevent the range being saved twice.
+ *
+ * @since 1.4.5
+ * @param {boolean} saveRange If to call rangeHelper.saveRange().
+ * @private
+ */
+ triggerValueChanged = function (saveRange) {
+ if (!pluginManager ||
+ (!pluginManager.hasHandler('valuechangedEvent') &&
+ !triggerValueChanged.hasHandler)) {
+ return;
+ }
+
+ var currentHtml,
+ sourceMode = base.sourceMode(),
+ hasSelection = !sourceMode && rangeHelper.hasSelection();
+
+ // Composition end isn't guaranteed to fire but must have
+ // ended when triggerValueChanged() is called so reset it
+ isComposing = false;
+
+ // Don't need to save the range if sceditor-start-marker
+ // is present as the range is already saved
+ saveRange = saveRange !== false &&
+ !wysiwygDocument.getElementById('sceditor-start-marker');
+
+ // Clear any current timeout as it's now been triggered
+ if (valueChangedKeyUpTimer) {
+ clearTimeout(valueChangedKeyUpTimer);
+ valueChangedKeyUpTimer = false;
+ }
+
+ if (hasSelection && saveRange) {
+ rangeHelper.saveRange();
+ }
+
+ currentHtml = sourceMode ? sourceEditor.value : wysiwygBody.innerHTML;
+
+ // Only trigger if something has actually changed.
+ if (currentHtml !== triggerValueChanged.lastVal) {
+ triggerValueChanged.lastVal = currentHtml;
+
+ trigger(editorContainer, 'valuechanged', {
+ rawValue: sourceMode ? base.val() : currentHtml
+ });
+ }
+
+ if (hasSelection && saveRange) {
+ rangeHelper.removeMarkers();
+ }
+ };
+
+ /**
+ * Should be called whenever there is a blur event
+ * @private
+ */
+ valueChangedBlur = function () {
+ if (valueChangedKeyUpTimer) {
+ triggerValueChanged();
+ }
+ };
+
+ /**
+ * Should be called whenever there is a keypress event
+ * @param {Event} e The keypress event
+ * @private
+ */
+ valueChangedKeyUp = function (e) {
+ var which = e.which,
+ lastChar = valueChangedKeyUp.lastChar,
+ lastWasSpace = (lastChar === 13 || lastChar === 32),
+ lastWasDelete = (lastChar === 8 || lastChar === 46);
+
+ valueChangedKeyUp.lastChar = which;
+
+ if (isComposing) {
+ return;
+ }
+
+ // 13 = return & 32 = space
+ if (which === 13 || which === 32) {
+ if (!lastWasSpace) {
+ triggerValueChanged();
+ } else {
+ valueChangedKeyUp.triggerNext = true;
+ }
+ // 8 = backspace & 46 = del
+ } else if (which === 8 || which === 46) {
+ if (!lastWasDelete) {
+ triggerValueChanged();
+ } else {
+ valueChangedKeyUp.triggerNext = true;
+ }
+ } else if (valueChangedKeyUp.triggerNext) {
+ triggerValueChanged();
+ valueChangedKeyUp.triggerNext = false;
+ }
+
+ // Clear the previous timeout and set a new one.
+ clearTimeout(valueChangedKeyUpTimer);
+
+ // Trigger the event 1.5s after the last keypress if space
+ // isn't pressed. This might need to be lowered, will need
+ // to look into what the slowest average Chars Per Min is.
+ valueChangedKeyUpTimer = setTimeout(function () {
+ if (!isComposing) {
+ triggerValueChanged();
+ }
+ }, 1500);
+ };
+
+ handleComposition = function (e) {
+ isComposing = /start/i.test(e.type);
+
+ if (!isComposing) {
+ triggerValueChanged();
+ }
+ };
+
+ autoUpdate = function () {
+ base.updateOriginal();
+ };
+
+ // run the initializer
+ init();
+ }
+
+
+ /**
+ * Map containing the loaded SCEditor locales
+ * @type {Object}
+ * @name locale
+ * @memberOf sceditor
+ */
+ SCEditor.locale = {};
+
+ SCEditor.formats = {};
+ SCEditor.icons = {};
+
+
+ /**
+ * Static command helper class
+ * @class command
+ * @name sceditor.command
+ */
+ SCEditor.command =
+ /** @lends sceditor.command */
+ {
+ /**
+ * Gets a command
+ *
+ * @param {string} name
+ * @return {Object|null}
+ * @since v1.3.5
+ */
+ get: function (name) {
+ return defaultCmds[name] || null;
+ },
+
+ /**
+ * Adds a command to the editor or updates an existing
+ * command if a command with the specified name already exists.
+ *
+ * Once a command is add it can be included in the toolbar by
+ * adding it's name to the toolbar option in the constructor. It
+ * can also be executed manually by calling
+ * {@link sceditor.execCommand}
+ *
+ * @example
+ * SCEditor.command.set("hello",
+ * {
+ * exec: function () {
+ * alert("Hello World!");
+ * }
+ * });
+ *
+ * @param {string} name
+ * @param {Object} cmd
+ * @return {this|false} Returns false if name or cmd is false
+ * @since v1.3.5
+ */
+ set: function (name, cmd) {
+ if (!name || !cmd) {
+ return false;
+ }
+
+ // merge any existing command properties
+ cmd = extend(defaultCmds[name] || {}, cmd);
+
+ cmd.remove = function () {
+ SCEditor.command.remove(name);
+ };
+
+ defaultCmds[name] = cmd;
+ return this;
+ },
+
+ /**
+ * Removes a command
+ *
+ * @param {string} name
+ * @return {this}
+ * @since v1.3.5
+ */
+ remove: function (name) {
+ if (defaultCmds[name]) {
+ delete defaultCmds[name];
+ }
+
+ return this;
+ }
+ };
+
+ /**
+ * SCEditor
+ * http://www.sceditor.com/
+ *
+ * Copyright (C) 2017, 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
+ */
+
+ window.sceditor = {
+ command: SCEditor.command,
+ commands: defaultCmds,
+ defaultOptions: defaultOptions,
+
+ ie: ie,
+ ios: ios,
+ isWysiwygSupported: isWysiwygSupported,
+
+ regexEscape: regex,
+ escapeEntities: entities,
+ escapeUriScheme: uriScheme,
+
+ dom: {
+ css: css,
+ attr: attr,
+ removeAttr: removeAttr,
+ is: is,
+ closest: closest,
+ width: width,
+ height: height,
+ traverse: traverse,
+ rTraverse: rTraverse,
+ parseHTML: parseHTML,
+ hasStyling: hasStyling,
+ convertElement: convertElement,
+ blockLevelList: blockLevelList,
+ canHaveChildren: canHaveChildren,
+ isInline: isInline,
+ copyCSS: copyCSS,
+ fixNesting: fixNesting,
+ findCommonAncestor: findCommonAncestor,
+ getSibling: getSibling,
+ removeWhiteSpace: removeWhiteSpace,
+ extractContents: extractContents,
+ getOffset: getOffset,
+ getStyle: getStyle,
+ hasStyle: hasStyle
+ },
+ locale: SCEditor.locale,
+ icons: SCEditor.icons,
+ utils: {
+ each: each,
+ isEmptyObject: isEmptyObject,
+ extend: extend
+ },
+ plugins: PluginManager.plugins,
+ formats: SCEditor.formats,
+ create: function (textarea, options) {
+ options = options || {};
+
+ // Don't allow the editor to be initialised
+ // on it's own source editor
+ if (parent(textarea, '.sceditor-container')) {
+ return;
+ }
+
+ if (options.runWithoutWysiwygSupport || isWysiwygSupported) {
+ /*eslint no-new: off*/
+ (new SCEditor(textarea, options));
+ }
+ },
+ instance: function (textarea) {
+ return textarea._sceditor;
+ }
+ };
+
+}());
diff --git a/public/assets/development/themes/content/default.css b/public/assets/development/themes/content/default.css
new file mode 100644
index 0000000..4cf0e68
--- /dev/null
+++ b/public/assets/development/themes/content/default.css
@@ -0,0 +1,85 @@
+/*! 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: 14px;
+ color: #111;
+ line-height: 1.25;
+ overflow: visible;
+}
+html {
+ height: 100%;
+}
+.ios {
+ /* Needed for iOS scrolling bug fix */
+ overflow: auto;
+ -webkit-overflow-scrolling: touch;
+}
+.ios body {
+ /* Needed for iOS scrolling bug fix */
+ position: relative;
+ overflow: auto;
+}
+body {
+ /* 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;
+}
+
+body.placeholder::before {
+ content: attr(placeholder);
+ color: #555;
+ font-style: italic;
+}
+
+ul, ol {
+ margin-top: 0;
+ margin-bottom: 0;
+ padding-top: 0;
+ padding-bottom: 0;
+}
+
+table, td {
+ border: 1px dotted #000;
+ empty-cells: show;
+}
+
+table td {
+ min-width: 5px;
+}
+
+code {
+ display: block;
+ background: #f1f1f1;
+ white-space: pre;
+ padding: 1em;
+ text-align: left;
+ margin: .25em 0;
+ direction: ltr;
+}
+
+blockquote {
+ background: #fff7d9;
+ margin: .25em 0;
+ border-left: .3em solid #f4e59f;
+ padding: .5em .5em .5em .75em;
+}
+blockquote cite {
+ font-weight: bold;
+ display: block;
+ font-size: 1em;
+ margin: 0 -.5em .25em -.75em;
+ padding: 0 .5em .15em .75em;
+ border-bottom: 1px solid #f4e59f;
+}
+
+h1, h2, h3, h4, h5, h6 {
+ padding: 0; margin: 0;
+}
+
+/* Prevent empty paragraphs from collapsing */
+div, p {
+ min-height: 1.25em;
+}
diff --git a/public/assets/development/themes/default.css b/public/assets/development/themes/default.css
new file mode 100644
index 0000000..6d0de8b
--- /dev/null
+++ b/public/assets/development/themes/default.css
@@ -0,0 +1,530 @@
+/*! SCEditor | (C) 2011-2016, Sam Clarke | sceditor.com/license */
+/**
+ * Default SCEditor
+ * http://www.sceditor.com/
+ *
+ * Copyright (C) 2011-16, Sam Clarke
+ *
+ * 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;
+}
+/**
+ * SCEditor
+ * http://www.sceditor.com/
+ *
+ * Copyright (C) 2017, 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 {
+ display: -ms-flexbox;
+ display: flex;
+ -ms-flex-direction: column;
+ flex-direction: column;
+ position: relative;
+ background: #fff;
+ border: 1px solid #d9d9d9;
+ font-size: 13px;
+ font-family: Arial, "Helvetica Neue", Helvetica, sans-serif;
+ color: #333;
+ line-height: 1;
+ font-weight: bold;
+ height: 250px;
+ 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 {
+ display: block;
+ -ms-flex: 1 1 0%;
+ flex: 1 1 0%;
+ line-height: 1.25;
+ border: 0;
+ outline: none;
+ font-family: Verdana, Arial, Helvetica, sans-serif;
+ font-size: 14px;
+ color: #111;
+ padding: 0;
+ margin: 5px;
+ resize: none;
+ background: #fff;
+ height: auto !important;
+ width: auto !important;
+ width: calc(100% - 10px) !important;
+ min-height: 1px;
+}
+.sceditor-container textarea {
+ margin: 7px 5px;
+}
+div.sceditor-dnd-cover {
+ position: absolute;
+ top: 0;
+ left: 0;
+ bottom: 0;
+ right: 0;
+ background: rgba(255, 255, 255, 0.2);
+ border: 5px dashed #aaa;
+ z-index: 200;
+ font-size: 2em;
+ text-align: center;
+ color: #aaa;
+}
+div.sceditor-dnd-cover p {
+ position: relative;
+ top: 45%;
+ pointer-events: none;
+}
+div.sceditor-resize-cover {
+ position: absolute;
+ top: 0;
+ left: 0;
+ background: #000;
+ width: 100%;
+ height: 100%;
+ z-index: 10;
+ opacity: 0.3;
+}
+div.sceditor-grip {
+ overflow: hidden;
+ width: 10px;
+ height: 10px;
+ cursor: pointer;
+ position: absolute;
+ bottom: 0;
+ right: 0;
+ z-index: 3;
+ line-height: 0;
+}
+div.sceditor-grip.has-icon {
+ background-image: none;
+}
+.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;
+}
+.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;
+ z-index: 4000;
+ padding: 10px;
+ font-weight: normal;
+ font-size: 15px;
+ border-radius: 2px;
+ background-clip: padding-box;
+ box-shadow: 1px 2px 4px rgba(0, 0, 0, 0.2);
+}
+div.sceditor-dropdown *,
+div.sceditor-dropdown *:before,
+div.sceditor-dropdown *:after {
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+}
+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-color-picker {
+ padding: 4px;
+}
+div.sceditor-emoticons,
+div.sceditor-more-emoticons {
+ 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: 2px solid #fff;
+ height: 18px;
+ width: 18px;
+ overflow: hidden;
+}
+.sceditor-color-option:hover {
+ border: 1px solid #aaa;
+}
+/**
+ * Toolbar styleing
+ */
+div.sceditor-toolbar {
+ flex-shrink: 0;
+ 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;
+}
+.sceditor-button {
+ float: left;
+ cursor: pointer;
+ padding: 3px 5px;
+ width: 16px;
+ height: 20px;
+ border-radius: 3px;
+ background-clip: padding-box;
+}
+.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 svg {
+ display: inline-block;
+ height: 16px;
+ width: 16px;
+ margin: 2px 0;
+ fill: #111;
+ text-decoration: none;
+ pointer-events: none;
+ line-height: 1;
+}
+.sceditor-button.disabled svg {
+ fill: #888;
+}
+.sceditor-button div {
+ display: inline-block;
+ margin: 2px 0;
+ padding: 0;
+ overflow: hidden;
+ line-height: 0;
+ font-size: 0;
+ color: transparent;
+}
+.sceditor-button.has-icon div {
+ display: none;
+}
+.sceditor-button.disabled div {
+ 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 {
+ display: inline-block;
+ width: auto;
+ line-height: 16px;
+ font-size: 1em;
+ color: inherit;
+ text-indent: 0;
+}
+.text-icon .sceditor-button.has-icon div,
+.sceditor-button.has-icon div,
+.text .sceditor-button div,
+.sceditor-button.text div {
+ padding: 0 2px;
+ background: none;
+}
+.text .sceditor-button svg,
+.sceditor-button.text svg {
+ display: 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/public/assets/development/themes/defaultdark.css b/public/assets/development/themes/defaultdark.css
new file mode 100644
index 0000000..09da75d
--- /dev/null
+++ b/public/assets/development/themes/defaultdark.css
@@ -0,0 +1,548 @@
+/*! SCEditor | (C) 2017, Sam Clarke | sceditor.com/license */
+/**
+ * Default SCEditor
+ * http://www.sceditor.com/
+ *
+ * Copyright (C) 2017, Sam Clarke
+ *
+ * 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;
+}
+/**
+ * SCEditor
+ * http://www.sceditor.com/
+ *
+ * Copyright (C) 2017, 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 {
+ display: -ms-flexbox;
+ display: flex;
+ -ms-flex-direction: column;
+ flex-direction: column;
+ position: relative;
+ background: #fff;
+ border: 1px solid #d9d9d9;
+ font-size: 13px;
+ font-family: Arial, "Helvetica Neue", Helvetica, sans-serif;
+ color: #333;
+ line-height: 1;
+ font-weight: bold;
+ height: 250px;
+ 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 {
+ display: block;
+ -ms-flex: 1 1 0%;
+ flex: 1 1 0%;
+ line-height: 1.25;
+ border: 0;
+ outline: none;
+ font-family: Verdana, Arial, Helvetica, sans-serif;
+ font-size: 14px;
+ color: #111;
+ padding: 0;
+ margin: 5px;
+ resize: none;
+ background: #fff;
+ height: auto !important;
+ width: auto !important;
+ width: calc(100% - 10px) !important;
+ min-height: 1px;
+}
+.sceditor-container textarea {
+ margin: 7px 5px;
+}
+div.sceditor-dnd-cover {
+ position: absolute;
+ top: 0;
+ left: 0;
+ bottom: 0;
+ right: 0;
+ background: rgba(255, 255, 255, 0.2);
+ border: 5px dashed #aaa;
+ z-index: 200;
+ font-size: 2em;
+ text-align: center;
+ color: #aaa;
+}
+div.sceditor-dnd-cover p {
+ position: relative;
+ top: 45%;
+ pointer-events: none;
+}
+div.sceditor-resize-cover {
+ position: absolute;
+ top: 0;
+ left: 0;
+ background: #000;
+ width: 100%;
+ height: 100%;
+ z-index: 10;
+ opacity: 0.3;
+}
+div.sceditor-grip {
+ overflow: hidden;
+ width: 10px;
+ height: 10px;
+ cursor: pointer;
+ position: absolute;
+ bottom: 0;
+ right: 0;
+ z-index: 3;
+ line-height: 0;
+}
+div.sceditor-grip.has-icon {
+ background-image: none;
+}
+.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;
+}
+.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;
+ z-index: 4000;
+ padding: 10px;
+ font-weight: normal;
+ font-size: 15px;
+ border-radius: 2px;
+ background-clip: padding-box;
+ box-shadow: 1px 2px 4px rgba(0, 0, 0, 0.2);
+}
+div.sceditor-dropdown *,
+div.sceditor-dropdown *:before,
+div.sceditor-dropdown *:after {
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+}
+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-color-picker {
+ padding: 4px;
+}
+div.sceditor-emoticons,
+div.sceditor-more-emoticons {
+ 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: 2px solid #fff;
+ height: 18px;
+ width: 18px;
+ overflow: hidden;
+}
+.sceditor-color-option:hover {
+ border: 1px solid #aaa;
+}
+/**
+ * Toolbar styleing
+ */
+div.sceditor-toolbar {
+ flex-shrink: 0;
+ 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;
+}
+.sceditor-button {
+ float: left;
+ cursor: pointer;
+ padding: 3px 5px;
+ width: 16px;
+ height: 20px;
+ border-radius: 3px;
+ background-clip: padding-box;
+}
+.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 svg {
+ display: inline-block;
+ height: 16px;
+ width: 16px;
+ margin: 2px 0;
+ fill: #111;
+ text-decoration: none;
+ pointer-events: none;
+ line-height: 1;
+}
+.sceditor-button.disabled svg {
+ fill: #888;
+}
+.sceditor-button div {
+ display: inline-block;
+ margin: 2px 0;
+ padding: 0;
+ overflow: hidden;
+ line-height: 0;
+ font-size: 0;
+ color: transparent;
+}
+.sceditor-button.has-icon div {
+ display: none;
+}
+.sceditor-button.disabled div {
+ 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 {
+ display: inline-block;
+ width: auto;
+ line-height: 16px;
+ font-size: 1em;
+ color: inherit;
+ text-indent: 0;
+}
+.text-icon .sceditor-button.has-icon div,
+.sceditor-button.has-icon div,
+.text .sceditor-button div,
+.sceditor-button.text div {
+ padding: 0 2px;
+ background: none;
+}
+.text .sceditor-button svg,
+.sceditor-button.text svg {
+ display: 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;
+}
+div.sceditor-toolbar {
+ background: #5d5d5d;
+}
+div.sceditor-group {
+ background: #303030;
+ border-bottom: 1px solid #000;
+}
+.sceditor-button:hover,
+.sceditor-button:active,
+.sceditor-button.active {
+ background: #6b6b6b;
+}
+.sceditor-button svg {
+ fill: #fff;
+}
+.sceditor-button.disabled svg {
+ fill: #777;
+}
diff --git a/public/assets/development/themes/famfamfam.png b/public/assets/development/themes/famfamfam.png
new file mode 100644
index 0000000..488c72a
Binary files /dev/null and b/public/assets/development/themes/famfamfam.png differ
diff --git a/public/assets/development/themes/modern.css b/public/assets/development/themes/modern.css
new file mode 100644
index 0000000..e3c4a67
--- /dev/null
+++ b/public/assets/development/themes/modern.css
@@ -0,0 +1,604 @@
+/**
+ * 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-2016, Sam Clarke | sceditor.com/license */
+/**
+ * Default SCEditor
+ * http://www.sceditor.com/
+ *
+ * Copyright (C) 2011-16, Sam Clarke
+ *
+ * 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;
+}
+/**
+ * SCEditor
+ * http://www.sceditor.com/
+ *
+ * Copyright (C) 2017, 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 {
+ display: -ms-flexbox;
+ display: flex;
+ -ms-flex-direction: column;
+ flex-direction: column;
+ position: relative;
+ background: #fff;
+ border: 1px solid #d9d9d9;
+ font-size: 13px;
+ font-family: Arial, "Helvetica Neue", Helvetica, sans-serif;
+ color: #333;
+ line-height: 1;
+ font-weight: bold;
+ height: 250px;
+ 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 {
+ display: block;
+ -ms-flex: 1 1 0%;
+ flex: 1 1 0%;
+ line-height: 1.25;
+ border: 0;
+ outline: none;
+ font-family: Verdana, Arial, Helvetica, sans-serif;
+ font-size: 14px;
+ color: #111;
+ padding: 0;
+ margin: 5px;
+ resize: none;
+ background: #fff;
+ height: auto !important;
+ width: auto !important;
+ width: calc(100% - 10px) !important;
+ min-height: 1px;
+}
+.sceditor-container textarea {
+ margin: 7px 5px;
+}
+div.sceditor-dnd-cover {
+ position: absolute;
+ top: 0;
+ left: 0;
+ bottom: 0;
+ right: 0;
+ background: rgba(255, 255, 255, 0.2);
+ border: 5px dashed #aaa;
+ z-index: 200;
+ font-size: 2em;
+ text-align: center;
+ color: #aaa;
+}
+div.sceditor-dnd-cover p {
+ position: relative;
+ top: 45%;
+ pointer-events: none;
+}
+div.sceditor-resize-cover {
+ position: absolute;
+ top: 0;
+ left: 0;
+ background: #000;
+ width: 100%;
+ height: 100%;
+ z-index: 10;
+ opacity: 0.3;
+}
+div.sceditor-grip {
+ overflow: hidden;
+ width: 10px;
+ height: 10px;
+ cursor: pointer;
+ position: absolute;
+ bottom: 0;
+ right: 0;
+ z-index: 3;
+ line-height: 0;
+}
+div.sceditor-grip.has-icon {
+ background-image: none;
+}
+.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;
+}
+.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;
+ z-index: 4000;
+ padding: 10px;
+ font-weight: normal;
+ font-size: 15px;
+ border-radius: 2px;
+ background-clip: padding-box;
+ box-shadow: 1px 2px 4px rgba(0, 0, 0, 0.2);
+}
+div.sceditor-dropdown *,
+div.sceditor-dropdown *:before,
+div.sceditor-dropdown *:after {
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+}
+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-color-picker {
+ padding: 4px;
+}
+div.sceditor-emoticons,
+div.sceditor-more-emoticons {
+ 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: 2px solid #fff;
+ height: 18px;
+ width: 18px;
+ overflow: hidden;
+}
+.sceditor-color-option:hover {
+ border: 1px solid #aaa;
+}
+/**
+ * Toolbar styleing
+ */
+div.sceditor-toolbar {
+ flex-shrink: 0;
+ 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;
+}
+.sceditor-button {
+ float: left;
+ cursor: pointer;
+ padding: 3px 5px;
+ width: 16px;
+ height: 20px;
+ border-radius: 3px;
+ background-clip: padding-box;
+}
+.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 svg {
+ display: inline-block;
+ height: 16px;
+ width: 16px;
+ margin: 2px 0;
+ fill: #111;
+ text-decoration: none;
+ pointer-events: none;
+ line-height: 1;
+}
+.sceditor-button.disabled svg {
+ fill: #888;
+}
+.sceditor-button div {
+ display: inline-block;
+ margin: 2px 0;
+ padding: 0;
+ overflow: hidden;
+ line-height: 0;
+ font-size: 0;
+ color: transparent;
+}
+.sceditor-button.has-icon div {
+ display: none;
+}
+.sceditor-button.disabled div {
+ 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 {
+ display: inline-block;
+ width: auto;
+ line-height: 16px;
+ font-size: 1em;
+ color: inherit;
+ text-indent: 0;
+}
+.text-icon .sceditor-button.has-icon div,
+.sceditor-button.has-icon div,
+.text .sceditor-button div,
+.sceditor-button.text div {
+ padding: 0 2px;
+ background: none;
+}
+.text .sceditor-button svg,
+.sceditor-button.text svg {
+ display: 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%);
+}
+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,
+.sceditor-button svg {
+ margin: 0;
+}
diff --git a/public/assets/development/themes/office-toolbar.css b/public/assets/development/themes/office-toolbar.css
new file mode 100644
index 0000000..ed3ee3c
--- /dev/null
+++ b/public/assets/development/themes/office-toolbar.css
@@ -0,0 +1,596 @@
+/**
+ * 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-2016, Sam Clarke | sceditor.com/license */
+/**
+ * Default SCEditor
+ * http://www.sceditor.com/
+ *
+ * Copyright (C) 2011-16, Sam Clarke
+ *
+ * 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;
+}
+/**
+ * SCEditor
+ * http://www.sceditor.com/
+ *
+ * Copyright (C) 2017, 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 {
+ display: -ms-flexbox;
+ display: flex;
+ -ms-flex-direction: column;
+ flex-direction: column;
+ position: relative;
+ background: #fff;
+ border: 1px solid #d9d9d9;
+ font-size: 13px;
+ font-family: Arial, "Helvetica Neue", Helvetica, sans-serif;
+ color: #333;
+ line-height: 1;
+ font-weight: bold;
+ height: 250px;
+ 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 {
+ display: block;
+ -ms-flex: 1 1 0%;
+ flex: 1 1 0%;
+ line-height: 1.25;
+ border: 0;
+ outline: none;
+ font-family: Verdana, Arial, Helvetica, sans-serif;
+ font-size: 14px;
+ color: #111;
+ padding: 0;
+ margin: 5px;
+ resize: none;
+ background: #fff;
+ height: auto !important;
+ width: auto !important;
+ width: calc(100% - 10px) !important;
+ min-height: 1px;
+}
+.sceditor-container textarea {
+ margin: 7px 5px;
+}
+div.sceditor-dnd-cover {
+ position: absolute;
+ top: 0;
+ left: 0;
+ bottom: 0;
+ right: 0;
+ background: rgba(255, 255, 255, 0.2);
+ border: 5px dashed #aaa;
+ z-index: 200;
+ font-size: 2em;
+ text-align: center;
+ color: #aaa;
+}
+div.sceditor-dnd-cover p {
+ position: relative;
+ top: 45%;
+ pointer-events: none;
+}
+div.sceditor-resize-cover {
+ position: absolute;
+ top: 0;
+ left: 0;
+ background: #000;
+ width: 100%;
+ height: 100%;
+ z-index: 10;
+ opacity: 0.3;
+}
+div.sceditor-grip {
+ overflow: hidden;
+ width: 10px;
+ height: 10px;
+ cursor: pointer;
+ position: absolute;
+ bottom: 0;
+ right: 0;
+ z-index: 3;
+ line-height: 0;
+}
+div.sceditor-grip.has-icon {
+ background-image: none;
+}
+.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;
+}
+.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;
+ z-index: 4000;
+ padding: 10px;
+ font-weight: normal;
+ font-size: 15px;
+ border-radius: 2px;
+ background-clip: padding-box;
+ box-shadow: 1px 2px 4px rgba(0, 0, 0, 0.2);
+}
+div.sceditor-dropdown *,
+div.sceditor-dropdown *:before,
+div.sceditor-dropdown *:after {
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+}
+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-color-picker {
+ padding: 4px;
+}
+div.sceditor-emoticons,
+div.sceditor-more-emoticons {
+ 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: 2px solid #fff;
+ height: 18px;
+ width: 18px;
+ overflow: hidden;
+}
+.sceditor-color-option:hover {
+ border: 1px solid #aaa;
+}
+/**
+ * Toolbar styleing
+ */
+div.sceditor-toolbar {
+ flex-shrink: 0;
+ 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;
+}
+.sceditor-button {
+ float: left;
+ cursor: pointer;
+ padding: 3px 5px;
+ width: 16px;
+ height: 20px;
+ border-radius: 3px;
+ background-clip: padding-box;
+}
+.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 svg {
+ display: inline-block;
+ height: 16px;
+ width: 16px;
+ margin: 2px 0;
+ fill: #111;
+ text-decoration: none;
+ pointer-events: none;
+ line-height: 1;
+}
+.sceditor-button.disabled svg {
+ fill: #888;
+}
+.sceditor-button div {
+ display: inline-block;
+ margin: 2px 0;
+ padding: 0;
+ overflow: hidden;
+ line-height: 0;
+ font-size: 0;
+ color: transparent;
+}
+.sceditor-button.has-icon div {
+ display: none;
+}
+.sceditor-button.disabled div {
+ 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 {
+ display: inline-block;
+ width: auto;
+ line-height: 16px;
+ font-size: 1em;
+ color: inherit;
+ text-indent: 0;
+}
+.text-icon .sceditor-button.has-icon div,
+.sceditor-button.has-icon div,
+.text .sceditor-button div,
+.sceditor-button.text div {
+ padding: 0 2px;
+ background: none;
+}
+.text .sceditor-button svg,
+.sceditor-button.text svg {
+ display: 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%);
+}
+div.sceditor-group {
+ border: 1px solid #7596bf;
+ background: transparent;
+ padding: 0;
+ background: #cadcf0;
+ background: linear-gradient(to bottom, #cadcf0 24%, #bcd0e9 38%, #d0e1f7 99%);
+}
+.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,
+.sceditor-button svg {
+ margin: 0;
+}
+.sceditor-button.active {
+ background: #fbdbb5;
+ background: linear-gradient(to bottom, #fbdbb5 11%, #feb456 29%, #fdeb9f 99%);
+ box-shadow: inset 0 1px #ebd1b4, inset 0 -1px #ffe47f, inset -1px 0 #b8ceea;
+}
+.sceditor-button:hover {
+ background: #fef7d5;
+ background: linear-gradient(to bottom, #fef7d5 0%, #fae5a9 42%, #ffd048 42%, #ffe59f 100%);
+ box-shadow: inset 0 1px #fffbe8, inset -1px 0 #ffefc4, inset 0 -1px #fff9cc;
+}
+.sceditor-button:active {
+ background: #e7a66d;
+ background: linear-gradient(to bottom, #e7a66d 0%, #fcb16d 1%, #ff8d05 42%, #ffc450 100%);
+ box-shadow: inset 0 1px 1px #7b6645, inset 0 -1px #d19c33;
+}
+.sceditor-button.active:hover {
+ background: #dba368;
+ background: linear-gradient(to bottom, #dba368 0%, #ffbd79 4%, #fea335 34%, #ffc64c 66%, #fee069 100%);
+ box-shadow: inset 0 1px 1px #9e8255, inset 0 -1px #fcce6b;
+}
diff --git a/public/assets/development/themes/office.css b/public/assets/development/themes/office.css
new file mode 100644
index 0000000..8cd4ba9
--- /dev/null
+++ b/public/assets/development/themes/office.css
@@ -0,0 +1,618 @@
+/**
+ * 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-2016, Sam Clarke | sceditor.com/license */
+/**
+ * Default SCEditor
+ * http://www.sceditor.com/
+ *
+ * Copyright (C) 2011-16, Sam Clarke
+ *
+ * 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;
+}
+/**
+ * SCEditor
+ * http://www.sceditor.com/
+ *
+ * Copyright (C) 2017, 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 {
+ display: -ms-flexbox;
+ display: flex;
+ -ms-flex-direction: column;
+ flex-direction: column;
+ position: relative;
+ background: #fff;
+ border: 1px solid #d9d9d9;
+ font-size: 13px;
+ font-family: Arial, "Helvetica Neue", Helvetica, sans-serif;
+ color: #333;
+ line-height: 1;
+ font-weight: bold;
+ height: 250px;
+ 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 {
+ display: block;
+ -ms-flex: 1 1 0%;
+ flex: 1 1 0%;
+ line-height: 1.25;
+ border: 0;
+ outline: none;
+ font-family: Verdana, Arial, Helvetica, sans-serif;
+ font-size: 14px;
+ color: #111;
+ padding: 0;
+ margin: 5px;
+ resize: none;
+ background: #fff;
+ height: auto !important;
+ width: auto !important;
+ width: calc(100% - 10px) !important;
+ min-height: 1px;
+}
+.sceditor-container textarea {
+ margin: 7px 5px;
+}
+div.sceditor-dnd-cover {
+ position: absolute;
+ top: 0;
+ left: 0;
+ bottom: 0;
+ right: 0;
+ background: rgba(255, 255, 255, 0.2);
+ border: 5px dashed #aaa;
+ z-index: 200;
+ font-size: 2em;
+ text-align: center;
+ color: #aaa;
+}
+div.sceditor-dnd-cover p {
+ position: relative;
+ top: 45%;
+ pointer-events: none;
+}
+div.sceditor-resize-cover {
+ position: absolute;
+ top: 0;
+ left: 0;
+ background: #000;
+ width: 100%;
+ height: 100%;
+ z-index: 10;
+ opacity: 0.3;
+}
+div.sceditor-grip {
+ overflow: hidden;
+ width: 10px;
+ height: 10px;
+ cursor: pointer;
+ position: absolute;
+ bottom: 0;
+ right: 0;
+ z-index: 3;
+ line-height: 0;
+}
+div.sceditor-grip.has-icon {
+ background-image: none;
+}
+.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;
+}
+.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;
+ z-index: 4000;
+ padding: 10px;
+ font-weight: normal;
+ font-size: 15px;
+ border-radius: 2px;
+ background-clip: padding-box;
+ box-shadow: 1px 2px 4px rgba(0, 0, 0, 0.2);
+}
+div.sceditor-dropdown *,
+div.sceditor-dropdown *:before,
+div.sceditor-dropdown *:after {
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+}
+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-color-picker {
+ padding: 4px;
+}
+div.sceditor-emoticons,
+div.sceditor-more-emoticons {
+ 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: 2px solid #fff;
+ height: 18px;
+ width: 18px;
+ overflow: hidden;
+}
+.sceditor-color-option:hover {
+ border: 1px solid #aaa;
+}
+/**
+ * Toolbar styleing
+ */
+div.sceditor-toolbar {
+ flex-shrink: 0;
+ 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;
+}
+.sceditor-button {
+ float: left;
+ cursor: pointer;
+ padding: 3px 5px;
+ width: 16px;
+ height: 20px;
+ border-radius: 3px;
+ background-clip: padding-box;
+}
+.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 svg {
+ display: inline-block;
+ height: 16px;
+ width: 16px;
+ margin: 2px 0;
+ fill: #111;
+ text-decoration: none;
+ pointer-events: none;
+ line-height: 1;
+}
+.sceditor-button.disabled svg {
+ fill: #888;
+}
+.sceditor-button div {
+ display: inline-block;
+ margin: 2px 0;
+ padding: 0;
+ overflow: hidden;
+ line-height: 0;
+ font-size: 0;
+ color: transparent;
+}
+.sceditor-button.has-icon div {
+ display: none;
+}
+.sceditor-button.disabled div {
+ 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 {
+ display: inline-block;
+ width: auto;
+ line-height: 16px;
+ font-size: 1em;
+ color: inherit;
+ text-indent: 0;
+}
+.text-icon .sceditor-button.has-icon div,
+.sceditor-button.has-icon div,
+.text .sceditor-button div,
+.sceditor-button.text div {
+ padding: 0 2px;
+ background: none;
+}
+.text .sceditor-button svg,
+.sceditor-button.text svg {
+ display: 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%);
+}
+div.sceditor-group {
+ border: 1px solid #7596bf;
+ background: transparent;
+ padding: 0;
+ background: #cadcf0;
+ background: linear-gradient(to bottom, #cadcf0 24%, #bcd0e9 38%, #d0e1f7 99%);
+}
+.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,
+.sceditor-button svg {
+ margin: 0;
+}
+.sceditor-button.active {
+ background: #fbdbb5;
+ background: linear-gradient(to bottom, #fbdbb5 11%, #feb456 29%, #fdeb9f 99%);
+ box-shadow: inset 0 1px #ebd1b4, inset 0 -1px #ffe47f, inset -1px 0 #b8ceea;
+}
+.sceditor-button:hover {
+ background: #fef7d5;
+ background: linear-gradient(to bottom, #fef7d5 0%, #fae5a9 42%, #ffd048 42%, #ffe59f 100%);
+ box-shadow: inset 0 1px #fffbe8, inset -1px 0 #ffefc4, inset 0 -1px #fff9cc;
+}
+.sceditor-button:active {
+ background: #e7a66d;
+ background: linear-gradient(to bottom, #e7a66d 0%, #fcb16d 1%, #ff8d05 42%, #ffc450 100%);
+ box-shadow: inset 0 1px 1px #7b6645, inset 0 -1px #d19c33;
+}
+.sceditor-button.active:hover {
+ background: #dba368;
+ background: linear-gradient(to bottom, #dba368 0%, #ffbd79 4%, #fea335 34%, #ffc64c 66%, #fee069 100%);
+ box-shadow: inset 0 1px 1px #9e8255, inset 0 -1px #fcce6b;
+}
+.sceditor-container {
+ background: #a3c2ea;
+ background: linear-gradient(to bottom, #a3c2ea 0%, #6d92c1 39%, #577fb3 64%, #6591cc 100%);
+}
+.sceditor-container iframe,
+.sceditor-container textarea {
+ border: 1px solid #646464;
+ background: #fff;
+ margin: 7px 40px;
+ padding: 20px;
+ width: calc(100% - 120px) !important;
+ box-shadow: 1px 1px 5px #293a52;
+}
diff --git a/public/assets/development/themes/square.css b/public/assets/development/themes/square.css
new file mode 100644
index 0000000..a9787a8
--- /dev/null
+++ b/public/assets/development/themes/square.css
@@ -0,0 +1,619 @@
+/**
+ * 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-2016, Sam Clarke | sceditor.com/license */
+/**
+ * Default SCEditor
+ * http://www.sceditor.com/
+ *
+ * Copyright (C) 2011-16, Sam Clarke
+ *
+ * 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;
+}
+/**
+ * SCEditor
+ * http://www.sceditor.com/
+ *
+ * Copyright (C) 2017, 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 {
+ display: -ms-flexbox;
+ display: flex;
+ -ms-flex-direction: column;
+ flex-direction: column;
+ position: relative;
+ background: #fff;
+ border: 1px solid #d9d9d9;
+ font-size: 13px;
+ font-family: Arial, "Helvetica Neue", Helvetica, sans-serif;
+ color: #333;
+ line-height: 1;
+ font-weight: bold;
+ height: 250px;
+ 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 {
+ display: block;
+ -ms-flex: 1 1 0%;
+ flex: 1 1 0%;
+ line-height: 1.25;
+ border: 0;
+ outline: none;
+ font-family: Verdana, Arial, Helvetica, sans-serif;
+ font-size: 14px;
+ color: #111;
+ padding: 0;
+ margin: 5px;
+ resize: none;
+ background: #fff;
+ height: auto !important;
+ width: auto !important;
+ width: calc(100% - 10px) !important;
+ min-height: 1px;
+}
+.sceditor-container textarea {
+ margin: 7px 5px;
+}
+div.sceditor-dnd-cover {
+ position: absolute;
+ top: 0;
+ left: 0;
+ bottom: 0;
+ right: 0;
+ background: rgba(255, 255, 255, 0.2);
+ border: 5px dashed #aaa;
+ z-index: 200;
+ font-size: 2em;
+ text-align: center;
+ color: #aaa;
+}
+div.sceditor-dnd-cover p {
+ position: relative;
+ top: 45%;
+ pointer-events: none;
+}
+div.sceditor-resize-cover {
+ position: absolute;
+ top: 0;
+ left: 0;
+ background: #000;
+ width: 100%;
+ height: 100%;
+ z-index: 10;
+ opacity: 0.3;
+}
+div.sceditor-grip {
+ overflow: hidden;
+ width: 10px;
+ height: 10px;
+ cursor: pointer;
+ position: absolute;
+ bottom: 0;
+ right: 0;
+ z-index: 3;
+ line-height: 0;
+}
+div.sceditor-grip.has-icon {
+ background-image: none;
+}
+.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;
+}
+.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;
+ z-index: 4000;
+ padding: 10px;
+ font-weight: normal;
+ font-size: 15px;
+ border-radius: 2px;
+ background-clip: padding-box;
+ box-shadow: 1px 2px 4px rgba(0, 0, 0, 0.2);
+}
+div.sceditor-dropdown *,
+div.sceditor-dropdown *:before,
+div.sceditor-dropdown *:after {
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+}
+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-color-picker {
+ padding: 4px;
+}
+div.sceditor-emoticons,
+div.sceditor-more-emoticons {
+ 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: 2px solid #fff;
+ height: 18px;
+ width: 18px;
+ overflow: hidden;
+}
+.sceditor-color-option:hover {
+ border: 1px solid #aaa;
+}
+/**
+ * Toolbar styleing
+ */
+div.sceditor-toolbar {
+ flex-shrink: 0;
+ 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;
+}
+.sceditor-button {
+ float: left;
+ cursor: pointer;
+ padding: 3px 5px;
+ width: 16px;
+ height: 20px;
+ border-radius: 3px;
+ background-clip: padding-box;
+}
+.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 svg {
+ display: inline-block;
+ height: 16px;
+ width: 16px;
+ margin: 2px 0;
+ fill: #111;
+ text-decoration: none;
+ pointer-events: none;
+ line-height: 1;
+}
+.sceditor-button.disabled svg {
+ fill: #888;
+}
+.sceditor-button div {
+ display: inline-block;
+ margin: 2px 0;
+ padding: 0;
+ overflow: hidden;
+ line-height: 0;
+ font-size: 0;
+ color: transparent;
+}
+.sceditor-button.has-icon div {
+ display: none;
+}
+.sceditor-button.disabled div {
+ 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 {
+ display: inline-block;
+ width: auto;
+ line-height: 16px;
+ font-size: 1em;
+ color: inherit;
+ text-indent: 0;
+}
+.text-icon .sceditor-button.has-icon div,
+.sceditor-button.has-icon div,
+.text .sceditor-button div,
+.sceditor-button.text div {
+ padding: 0 2px;
+ background: none;
+}
+.text .sceditor-button svg,
+.sceditor-button.text svg {
+ display: 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%);
+}
+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,
+.sceditor-button svg {
+ 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;
+}
diff --git a/public/assets/fonts/ionicons.eot b/public/assets/fonts/ionicons.eot
deleted file mode 100644
index 92a3f20..0000000
Binary files a/public/assets/fonts/ionicons.eot and /dev/null differ
diff --git a/public/assets/fonts/ionicons.svg b/public/assets/fonts/ionicons.svg
deleted file mode 100644
index 49fc8f3..0000000
--- a/public/assets/fonts/ionicons.svg
+++ /dev/null
@@ -1,2230 +0,0 @@
-
-
-
-
-
-Created by FontForge 20120731 at Thu Dec 4 09:51:48 2014
- By Adam Bradley
-Created by Adam Bradley with FontForge 2.0 (http://fontforge.sf.net)
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/public/assets/fonts/ionicons.ttf b/public/assets/fonts/ionicons.ttf
deleted file mode 100644
index c4e4632..0000000
Binary files a/public/assets/fonts/ionicons.ttf and /dev/null differ
diff --git a/public/assets/fonts/ionicons.woff b/public/assets/fonts/ionicons.woff
deleted file mode 100644
index 5f3a14e..0000000
Binary files a/public/assets/fonts/ionicons.woff and /dev/null differ
diff --git a/public/assets/fonts/wysibbiconfont-wb.eot b/public/assets/fonts/wysibbiconfont-wb.eot
new file mode 100644
index 0000000..170c2d3
Binary files /dev/null and b/public/assets/fonts/wysibbiconfont-wb.eot differ
diff --git a/public/assets/fonts/wysibbiconfont-wb.ttf b/public/assets/fonts/wysibbiconfont-wb.ttf
new file mode 100644
index 0000000..5852784
Binary files /dev/null and b/public/assets/fonts/wysibbiconfont-wb.ttf differ
diff --git a/public/assets/fonts/wysibbiconfont-wb.woff b/public/assets/fonts/wysibbiconfont-wb.woff
new file mode 100644
index 0000000..cf2cf31
Binary files /dev/null and b/public/assets/fonts/wysibbiconfont-wb.woff differ
diff --git a/public/assets/img/emoticons/alien.png b/public/assets/img/emoticons/alien.png
new file mode 100644
index 0000000..58a0767
Binary files /dev/null and b/public/assets/img/emoticons/alien.png differ
diff --git a/public/assets/img/emoticons/angel.png b/public/assets/img/emoticons/angel.png
new file mode 100644
index 0000000..4792225
Binary files /dev/null and b/public/assets/img/emoticons/angel.png differ
diff --git a/public/assets/img/emoticons/angry.png b/public/assets/img/emoticons/angry.png
new file mode 100644
index 0000000..7bec8e4
Binary files /dev/null and b/public/assets/img/emoticons/angry.png differ
diff --git a/public/assets/img/emoticons/blink.png b/public/assets/img/emoticons/blink.png
new file mode 100644
index 0000000..ff529f1
Binary files /dev/null and b/public/assets/img/emoticons/blink.png differ
diff --git a/public/assets/img/emoticons/blush.png b/public/assets/img/emoticons/blush.png
new file mode 100644
index 0000000..8ff7d1d
Binary files /dev/null and b/public/assets/img/emoticons/blush.png differ
diff --git a/public/assets/img/emoticons/cheerful.png b/public/assets/img/emoticons/cheerful.png
new file mode 100644
index 0000000..c7c5cb8
Binary files /dev/null and b/public/assets/img/emoticons/cheerful.png differ
diff --git a/public/assets/img/emoticons/cool.png b/public/assets/img/emoticons/cool.png
new file mode 100644
index 0000000..d21c544
Binary files /dev/null and b/public/assets/img/emoticons/cool.png differ
diff --git a/public/assets/img/emoticons/credits.txt b/public/assets/img/emoticons/credits.txt
new file mode 100644
index 0000000..96b3e7a
--- /dev/null
+++ b/public/assets/img/emoticons/credits.txt
@@ -0,0 +1,9 @@
+Presenting, Nomicons: The Full Monty :o
+
+Credits:
+Oscar Gruno, aka Nominell v. 2.0 -> oscargruno@mac.com
+Andy Fedosjeenko, aka Nightwolf -> bobo@animevanguard.com
+
+Copyright (C) 2001-Infinity, Oscar Gruno & Andy Fedosjeenko
+
+You can redistribute these files as much as you like, as long as you keep this file with them and give us the proper credit. You may even rape them if you please, just give us credit for our work.
\ No newline at end of file
diff --git a/public/assets/img/emoticons/cwy.png b/public/assets/img/emoticons/cwy.png
new file mode 100644
index 0000000..58ee08f
Binary files /dev/null and b/public/assets/img/emoticons/cwy.png differ
diff --git a/public/assets/img/emoticons/devil.png b/public/assets/img/emoticons/devil.png
new file mode 100644
index 0000000..7d8226a
Binary files /dev/null and b/public/assets/img/emoticons/devil.png differ
diff --git a/public/assets/img/emoticons/dizzy.png b/public/assets/img/emoticons/dizzy.png
new file mode 100644
index 0000000..8218464
Binary files /dev/null and b/public/assets/img/emoticons/dizzy.png differ
diff --git a/public/assets/img/emoticons/ermm.png b/public/assets/img/emoticons/ermm.png
new file mode 100644
index 0000000..122c10f
Binary files /dev/null and b/public/assets/img/emoticons/ermm.png differ
diff --git a/public/assets/img/emoticons/face.png b/public/assets/img/emoticons/face.png
new file mode 100644
index 0000000..04ad4b7
Binary files /dev/null and b/public/assets/img/emoticons/face.png differ
diff --git a/public/assets/img/emoticons/getlost.png b/public/assets/img/emoticons/getlost.png
new file mode 100644
index 0000000..ac87dce
Binary files /dev/null and b/public/assets/img/emoticons/getlost.png differ
diff --git a/public/assets/img/emoticons/grin.png b/public/assets/img/emoticons/grin.png
new file mode 100644
index 0000000..69cf5a3
Binary files /dev/null and b/public/assets/img/emoticons/grin.png differ
diff --git a/public/assets/img/emoticons/happy.png b/public/assets/img/emoticons/happy.png
new file mode 100644
index 0000000..54b9131
Binary files /dev/null and b/public/assets/img/emoticons/happy.png differ
diff --git a/public/assets/img/emoticons/heart.png b/public/assets/img/emoticons/heart.png
new file mode 100644
index 0000000..451058d
Binary files /dev/null and b/public/assets/img/emoticons/heart.png differ
diff --git a/public/assets/img/emoticons/kissing.png b/public/assets/img/emoticons/kissing.png
new file mode 100644
index 0000000..28d7752
Binary files /dev/null and b/public/assets/img/emoticons/kissing.png differ
diff --git a/public/assets/img/emoticons/laughing.png b/public/assets/img/emoticons/laughing.png
new file mode 100644
index 0000000..d65f35e
Binary files /dev/null and b/public/assets/img/emoticons/laughing.png differ
diff --git a/public/assets/img/emoticons/ninja.png b/public/assets/img/emoticons/ninja.png
new file mode 100644
index 0000000..952f811
Binary files /dev/null and b/public/assets/img/emoticons/ninja.png differ
diff --git a/public/assets/img/emoticons/pinch.png b/public/assets/img/emoticons/pinch.png
new file mode 100644
index 0000000..f6f6e53
Binary files /dev/null and b/public/assets/img/emoticons/pinch.png differ
diff --git a/public/assets/img/emoticons/pouty.png b/public/assets/img/emoticons/pouty.png
new file mode 100644
index 0000000..f3cef70
Binary files /dev/null and b/public/assets/img/emoticons/pouty.png differ
diff --git a/public/assets/img/emoticons/sad.png b/public/assets/img/emoticons/sad.png
new file mode 100644
index 0000000..d08666e
Binary files /dev/null and b/public/assets/img/emoticons/sad.png differ
diff --git a/public/assets/img/emoticons/shocked.png b/public/assets/img/emoticons/shocked.png
new file mode 100644
index 0000000..f3dfb11
Binary files /dev/null and b/public/assets/img/emoticons/shocked.png differ
diff --git a/public/assets/img/emoticons/sick.png b/public/assets/img/emoticons/sick.png
new file mode 100644
index 0000000..3a47381
Binary files /dev/null and b/public/assets/img/emoticons/sick.png differ
diff --git a/public/assets/img/emoticons/sideways.png b/public/assets/img/emoticons/sideways.png
new file mode 100644
index 0000000..b3dd09a
Binary files /dev/null and b/public/assets/img/emoticons/sideways.png differ
diff --git a/public/assets/img/emoticons/silly.png b/public/assets/img/emoticons/silly.png
new file mode 100644
index 0000000..d4497d9
Binary files /dev/null and b/public/assets/img/emoticons/silly.png differ
diff --git a/public/assets/img/emoticons/sleeping.png b/public/assets/img/emoticons/sleeping.png
new file mode 100644
index 0000000..2b97a80
Binary files /dev/null and b/public/assets/img/emoticons/sleeping.png differ
diff --git a/public/assets/img/emoticons/smile.png b/public/assets/img/emoticons/smile.png
new file mode 100644
index 0000000..c11821e
Binary files /dev/null and b/public/assets/img/emoticons/smile.png differ
diff --git a/public/assets/img/emoticons/tongue.png b/public/assets/img/emoticons/tongue.png
new file mode 100644
index 0000000..73da3e3
Binary files /dev/null and b/public/assets/img/emoticons/tongue.png differ
diff --git a/public/assets/img/emoticons/unsure.png b/public/assets/img/emoticons/unsure.png
new file mode 100644
index 0000000..87c7599
Binary files /dev/null and b/public/assets/img/emoticons/unsure.png differ
diff --git a/public/assets/img/emoticons/w00t.png b/public/assets/img/emoticons/w00t.png
new file mode 100644
index 0000000..fa08831
Binary files /dev/null and b/public/assets/img/emoticons/w00t.png differ
diff --git a/public/assets/img/emoticons/wassat.png b/public/assets/img/emoticons/wassat.png
new file mode 100644
index 0000000..201733b
Binary files /dev/null and b/public/assets/img/emoticons/wassat.png differ
diff --git a/public/assets/img/emoticons/whistling.png b/public/assets/img/emoticons/whistling.png
new file mode 100644
index 0000000..3940f0d
Binary files /dev/null and b/public/assets/img/emoticons/whistling.png differ
diff --git a/public/assets/img/emoticons/wink.png b/public/assets/img/emoticons/wink.png
new file mode 100644
index 0000000..3d1a9cc
Binary files /dev/null and b/public/assets/img/emoticons/wink.png differ
diff --git a/public/assets/img/emoticons/wub.png b/public/assets/img/emoticons/wub.png
new file mode 100644
index 0000000..d5faa2b
Binary files /dev/null and b/public/assets/img/emoticons/wub.png differ
diff --git a/public/assets/img/logo-lg-white.png b/public/assets/img/logo-lg-white.png
deleted file mode 100644
index 8af573c..0000000
Binary files a/public/assets/img/logo-lg-white.png and /dev/null differ
diff --git a/public/assets/js/jquery.wysibb.min.js b/public/assets/js/jquery.wysibb.min.js
new file mode 100644
index 0000000..f92b2d8
--- /dev/null
+++ b/public/assets/js/jquery.wysibb.min.js
@@ -0,0 +1,5 @@
+/*! WysiBB v1.5.1 2014-03-26
+ Author: Vadim Dobroskok
+ */
+"undefined"==typeof WBBLANG&&(WBBLANG={}),WBBLANG.en=CURLANG={bold:"Bold",italic:"Italic",underline:"Underline",strike:"Strike",link:"Link",img:"Insert image",sup:"Superscript",sub:"Subscript",justifyleft:"Align left",justifycenter:"Align center",justifyright:"Align right",table:"Insert table",bullist:"• Unordered list",numlist:"1. Ordered list",quote:"Quote",offtop:"Offtop",code:"Code",spoiler:"Spoiler",fontcolor:"Font color",fontsize:"Font size",fontfamily:"Font family",fs_verysmall:"Very small",fs_small:"Small",fs_normal:"Normal",fs_big:"Big",fs_verybig:"Very big",smilebox:"Insert emoticon",video:"Insert YouTube",removeFormat:"Remove Format",modal_link_title:"Insert link",modal_link_text:"Display text",modal_link_url:"URL",modal_email_text:"Display email",modal_email_url:"Email",modal_link_tab1:"Insert URL",modal_img_title:"Insert image",modal_img_tab1:"Insert URL",modal_img_tab2:"Upload image",modal_imgsrc_text:"Enter image URL",modal_img_btn:"Choose file",add_attach:"Add Attachment",modal_video_text:"Enter the URL of the video",close:"Close",save:"Save",cancel:"Cancel",remove:"Delete",validation_err:"The entered data is invalid",error_onupload:"Error during file upload",fileupload_text1:"Drop file here",fileupload_text2:"or",loading:"Loading",auto:"Auto",views:"Views",downloads:"Downloads",sm1:"Smile",sm2:"Laughter",sm3:"Wink",sm4:"Thank you",sm5:"Scold",sm6:"Shock",sm7:"Angry",sm8:"Pain",sm9:"Sick"},wbbdebug=!0,function(a){"use strict";a.wysibb=function(b,c){a(b).data("wbb",this),c&&c.deflang&&"undefined"!=typeof WBBLANG[c.deflang]&&(CURLANG=WBBLANG[c.deflang]),c&&c.lang&&"undefined"!=typeof WBBLANG[c.lang]&&(CURLANG=WBBLANG[c.lang]),this.txtArea=b,this.$txtArea=a(b);this.$txtArea.attr("id")||this.setUID(this.txtArea);this.options={bbmode:!1,onlyBBmode:!1,themeName:"default",bodyClass:"",lang:"ru",tabInsert:!0,imgupload:!1,img_uploadurl:"/iupload.php",img_maxwidth:800,img_maxheight:800,hotkeys:!0,showHotkeys:!0,autoresize:!0,resize_maxheight:800,loadPageStyles:!0,traceTextarea:!0,smileConversion:!0,buttons:"bold,italic,underline,strike,sup,sub,|,img,video,link,|,bullist,numlist,|,fontcolor,fontsize,fontfamily,|,justifyleft,justifycenter,justifyright,|,quote,code,table,removeFormat",allButtons:{bold:{title:CURLANG.bold,buttonHTML:' ',excmd:"bold",hotkey:"ctrl+b",transform:{"{SELTEXT} ":"[b]{SELTEXT}[/b]","{SELTEXT} ":"[b]{SELTEXT}[/b]"}},italic:{title:CURLANG.italic,buttonHTML:' ',excmd:"italic",hotkey:"ctrl+i",transform:{"{SELTEXT} ":"[i]{SELTEXT}[/i]","{SELTEXT} ":"[i]{SELTEXT}[/i]"}},underline:{title:CURLANG.underline,buttonHTML:' ',excmd:"underline",hotkey:"ctrl+u",transform:{"{SELTEXT} ":"[u]{SELTEXT}[/u]"}},strike:{title:CURLANG.strike,buttonHTML:' ',excmd:"strikeThrough",transform:{"{SELTEXT} ":"[s]{SELTEXT}[/s]","{SELTEXT} ":"[s]{SELTEXT}[/s]"}},sup:{title:CURLANG.sup,buttonHTML:' ',excmd:"superscript",transform:{"{SELTEXT} ":"[sup]{SELTEXT}[/sup]"}},sub:{title:CURLANG.sub,buttonHTML:' ',excmd:"subscript",transform:{"{SELTEXT} ":"[sub]{SELTEXT}[/sub]"}},link:{title:CURLANG.link,buttonHTML:' ',hotkey:"ctrl+shift+2",modal:{title:CURLANG.modal_link_title,width:"500px",tabs:[{input:[{param:"SELTEXT",title:CURLANG.modal_link_text,type:"div"},{param:"URL",title:CURLANG.modal_link_url,validation:"^http(s)?://"}]}]},transform:{'{SELTEXT} ':"[url={URL}]{SELTEXT}[/url]",'{URL} ':"[url]{URL}[/url]"}},img:{title:CURLANG.img,buttonHTML:' ',hotkey:"ctrl+shift+1",addWrap:!0,modal:{title:CURLANG.modal_img_title,width:"600px",tabs:[{title:CURLANG.modal_img_tab1,input:[{param:"SRC",title:CURLANG.modal_imgsrc_text,validation:"^http(s)?://.*?.(jpg|png|gif|jpeg)$"}]}],onLoad:this.imgLoadModal},transform:{' ':"[img]{SRC}[/img]",' ':"[img width={WIDTH},height={HEIGHT}]{SRC}[/img]"}},bullist:{title:CURLANG.bullist,buttonHTML:' ',excmd:"insertUnorderedList",transform:{"":"[list]{SELTEXT}[/list]","{SELTEXT} ":"[*]{SELTEXT}[/*]"}},numlist:{title:CURLANG.numlist,buttonHTML:' ',excmd:"insertOrderedList",transform:{"{SELTEXT} ":"[list=1]{SELTEXT}[/list]","{SELTEXT} ":"[*]{SELTEXT}[/*]"}},quote:{title:CURLANG.quote,buttonHTML:' ',hotkey:"ctrl+shift+3",transform:{"{SELTEXT} ":"[quote]{SELTEXT}[/quote]"}},code:{title:CURLANG.code,buttonText:"[code]",hotkey:"ctrl+shift+4",onlyClearText:!0,transform:{"{SELTEXT}
":"[code]{SELTEXT}[/code]"}},offtop:{title:CURLANG.offtop,buttonText:"offtop",transform:{'{SELTEXT} ':"[offtop]{SELTEXT}[/offtop]"}},fontcolor:{type:"colorpicker",title:CURLANG.fontcolor,excmd:"foreColor",valueBBname:"color",subInsert:!0,colors:"#000000,#444444,#666666,#999999,#b6b6b6,#cccccc,#d8d8d8,#efefef,#f4f4f4,#ffffff,-, #ff0000,#980000,#ff7700,#ffff00,#00ff00,#00ffff,#1e84cc,#0000ff,#9900ff,#ff00ff,-, #f4cccc,#dbb0a7,#fce5cd,#fff2cc,#d9ead3,#d0e0e3,#c9daf8,#cfe2f3,#d9d2e9,#ead1dc, #ea9999,#dd7e6b,#f9cb9c,#ffe599,#b6d7a8,#a2c4c9,#a4c2f4,#9fc5e8,#b4a7d6,#d5a6bd, #e06666,#cc4125,#f6b26b,#ffd966,#93c47d,#76a5af,#6d9eeb,#6fa8dc,#8e7cc3,#c27ba0, #cc0000,#a61c00,#e69138,#f1c232,#6aa84f,#45818e,#3c78d8,#3d85c6,#674ea7,#a64d79, #900000,#85200C,#B45F06,#BF9000,#38761D,#134F5C,#1155Cc,#0B5394,#351C75,#741B47, #660000,#5B0F00,#783F04,#7F6000,#274E13,#0C343D,#1C4587,#073763,#20124D,#4C1130",transform:{'{SELTEXT} ':"[color={COLOR}]{SELTEXT}[/color]"}},table:{type:"table",title:CURLANG.table,cols:10,rows:10,cellwidth:20,transform:{"{SELTEXT} ":"[td]{SELTEXT}[/td]","{SELTEXT} ":"[tr]{SELTEXT}[/tr]",'':"[table]{SELTEXT}[/table]"},skipRules:!0},fontsize:{type:"select",title:CURLANG.fontsize,options:"fs_verysmall,fs_small,fs_normal,fs_big,fs_verybig"},fontfamily:{type:"select",title:CURLANG.fontfamily,excmd:"fontName",valueBBname:"font",options:[{title:"Arial",exvalue:"Arial"},{title:"Comic Sans MS",exvalue:"Comic Sans MS"},{title:"Courier New",exvalue:"Courier New"},{title:"Georgia",exvalue:"Georgia"},{title:"Lucida Sans Unicode",exvalue:"Lucida Sans Unicode"},{title:"Tahoma",exvalue:"Tahoma"},{title:"Times New Roman",exvalue:"Times New Roman"},{title:"Trebuchet MS",exvalue:"Trebuchet MS"},{title:"Verdana",exvalue:"Verdana"}],transform:{'{SELTEXT} ':"[font={FONT}]{SELTEXT}[/font]"}},smilebox:{type:"smilebox",title:CURLANG.smilebox,buttonHTML:' '},justifyleft:{title:CURLANG.justifyleft,buttonHTML:' ',groupkey:"align",transform:{'{SELTEXT}
':"[left]{SELTEXT}[/left]"}},justifyright:{title:CURLANG.justifyright,buttonHTML:' ',groupkey:"align",transform:{'{SELTEXT}
':"[right]{SELTEXT}[/right]"}},justifycenter:{title:CURLANG.justifycenter,buttonHTML:' ',groupkey:"align",transform:{'{SELTEXT}
':"[center]{SELTEXT}[/center]"}},video:{title:CURLANG.video,buttonHTML:' ',modal:{title:CURLANG.video,width:"600px",tabs:[{title:CURLANG.video,input:[{param:"SRC",title:CURLANG.modal_video_text}]}],onSubmit:function(a){var b=this.$modal.find('input[name="SRC"]').val();b&&(b=b.replace(/^\s+/,"").replace(/\s+$/,""));var c;if(c=b.match(-1!=b.indexOf("youtu.be")?/^http[s]*:\/\/youtu\.be\/([a-z0-9_-]+)/i:/^http[s]*:\/\/www\.youtube\.com\/watch\?.*?v=([a-z0-9_-]+)/i),c&&2==c.length){var d=c[1];this.insertAtCursor(this.getCodeByCommand(a,{src:d}))}return this.closeModal(),this.updateUI(),!1}},transform:{'VIDEO ':"[video]{SRC}[/video]"}},fs_verysmall:{title:CURLANG.fs_verysmall,buttonText:"fs1",excmd:"fontSize",exvalue:"1",transform:{'{SELTEXT} ':"[size=50]{SELTEXT}[/size]"}},fs_small:{title:CURLANG.fs_small,buttonText:"fs2",excmd:"fontSize",exvalue:"2",transform:{'{SELTEXT} ':"[size=85]{SELTEXT}[/size]"}},fs_normal:{title:CURLANG.fs_normal,buttonText:"fs3",excmd:"fontSize",exvalue:"3",transform:{'{SELTEXT} ':"[size=100]{SELTEXT}[/size]"}},fs_big:{title:CURLANG.fs_big,buttonText:"fs4",excmd:"fontSize",exvalue:"4",transform:{'{SELTEXT} ':"[size=150]{SELTEXT}[/size]"}},fs_verybig:{title:CURLANG.fs_verybig,buttonText:"fs5",excmd:"fontSize",exvalue:"6",transform:{'{SELTEXT} ':"[size=200]{SELTEXT}[/size]"}},removeformat:{title:CURLANG.removeFormat,buttonHTML:' ',excmd:"removeFormat"}},systr:{" ":"\n",'{SELTEXT} ':" {SELTEXT}"},customRules:{td:[["[td]{SELTEXT}[/td]",{seltext:{rgx:!1,attr:!1,sel:!1}}]],tr:[["[tr]{SELTEXT}[/tr]",{seltext:{rgx:!1,attr:!1,sel:!1}}]],table:[["[table]{SELTEXT}[/table]",{seltext:{rgx:!1,attr:!1,sel:!1}}]]},smileList:[],attrWrap:["src","color","href"]},this.inited=this.options.onlyBBmode,this.options.themePrefix||a("link").each(a.proxy(function(b,c){var d=a(c).get(0).href.match(/(.*\/)(.*)\/wbbtheme\.css.*$/);null!==d&&(this.options.themeName=d[2],this.options.themePrefix=d[1])},this)),"undefined"!=typeof WBBPRESET&&(WBBPRESET.allButtons&&a.each(WBBPRESET.allButtons,a.proxy(function(a,b){b.transform&&this.options.allButtons[a]&&delete this.options.allButtons[a].transform},this)),a.extend(!0,this.options,WBBPRESET)),c&&c.allButtons&&a.each(c.allButtons,a.proxy(function(a,b){b.transform&&this.options.allButtons[a]&&delete this.options.allButtons[a].transform},this)),a.extend(!0,this.options,c),this.init()},a.wysibb.prototype={lastid:1,init:function(){a.log("Init",this),this.isMobile=function(a){/android|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|meego.+mobile|midp|mmp|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows (ce|phone)|xda|xiino/i.test(a)}(navigator.userAgent||navigator.vendor||window.opera),this.options.onlyBBmode===!0&&(this.options.bbmode=!0),this.controllers=[],this.options.buttons=this.options.buttons.toLowerCase(),this.options.buttons=this.options.buttons.split(","),this.options.allButtons._systr={},this.options.allButtons._systr.transform=this.options.systr,this.smileFind(),this.initTransforms(),this.build(),this.initModal(),this.options.hotkeys!==!0||this.isMobile||this.initHotkeys(),this.options.smileList&&this.options.smileList.length>0&&this.options.smileList.sort(function(a,b){return b.bbcode.length-a.bbcode.length}),this.$txtArea.parents("form").bind("submit",a.proxy(function(){return this.sync(),!0},this)),this.$txtArea.parents("form").find("input[id*='preview'],input[id*='submit'],input[class*='preview'],input[class*='submit'],input[name*='preview'],input[name*='submit']").bind("mousedown",a.proxy(function(){this.sync(),setTimeout(a.proxy(function(){this.options.bbmode===!1&&this.$txtArea.removeAttr("wbbsync").val("")},this),1e3)},this)),this.options.initCallback&&this.options.initCallback.call(this),a.log(this)},initTransforms:function(){a.log("Create rules for transform HTML=>BB");var b=this.options;b.rules||(b.rules={}),b.groups||(b.groups={});var c=b.buttons.slice();c.push("_systr");for(var d=0;d-1||a(d).parent().contents().size()>1){var n=a("").html("{"+i+"}");this.setUID(n,"wbb");var p=f.indexOf(i)+i.length+1,q=f.substr(p,f.length-p);d.data=f.substr(0,f.indexOf(i)-1),a(d).after(this.elFromString(q,document)).after(n),m=(m?m+" ":"")+this.filterByNode(n),k=!1}o[i.toLowerCase()]={sel:m,attr:!1,rgx:k},e[e.length]=m}},this)),e=null},this));var p=k.html();p=this.unwrapAttrs(p),i!=p&&(delete e.transform[i],e.transform[p]=j,h=p)}b.rules[l].push([j,o]),e.onlyClearText===!0&&(this.cleartext||(this.cleartext={}),this.cleartext[l]=c[d]),e.groupkey&&(b.groups[e.groupkey]||(b.groups[e.groupkey]=[]),b.groups[e.groupkey].push(l))}}e.rootSelector&&this.sortArray(e.rootSelector,-1);var q=a.map(e.transform,function(a,b){return b}).sort(function(a,b){return(b[0]||"").length-(a[0]||"").length});e.bbcode=e.transform[q[0]],e.html=q[0]}}}this.options.btnlist=c,a.extend(b.rules,this.options.customRules),b.srules={},this.options.smileList&&a.each(b.smileList,a.proxy(function(c,d){var e=a(this.strf(d.img,b)),f=this.filterByNode(e);b.srules[f]=[d.bbcode,d.img]},this));for(var r in b.rules)this.options.rules[r].sort(function(a,b){return b[0].length-a[0].length});this.rsellist=[];for(var r in this.options.rules)this.rsellist.push(r);this.sortArray(this.rsellist,-1)},build:function(){if(a.log("Build editor"),this.$editor=a("").addClass("wysibb"),this.isMobile&&this.$editor.addClass("wysibb-mobile"),this.options.direction&&this.$editor.css("direction",this.options.direction),this.$editor.insertAfter(this.txtArea).append(this.txtArea),this.startHeight=this.$txtArea.outerHeight(),this.$txtArea.addClass("wysibb-texarea"),this.buildToolbar(),this.$txtArea.wrap('
'),this.options.onlyBBmode===!1){var b=this.options.minheight||this.$txtArea.outerHeight(),c=(this.options.resize_maxheight,this.options.autoresize===!0?this.options.resize_maxheight:b);if(this.$body=a(this.strf('
',{maxheight:c,height:b})).insertAfter(this.$txtArea),this.body=this.$body[0],this.$txtArea.hide(),b>32&&this.$toolbar.css("max-height",b),a.log("WysiBB loaded"),this.$body.addClass("wysibb-body").addClass(this.options.bodyClass),this.options.direction&&this.$body.css("direction",this.options.direction),"contentEditable"in this.body){this.body.contentEditable=!0;try{document.execCommand("StyleWithCSS",!1,!1),this.$body.append("
")}catch(d){}}else this.options.onlyBBmode=this.options.bbmode=!0;this.txtArea.value.length>0&&this.txtAreaInitContent(),this.$body.bind("keydown",a.proxy(function(b){return 86==b.which&&(1==b.ctrlKey||1==b.metaKey)||45==b.which&&(1==b.shiftKey||1==b.metaKey)?(this.$pasteBlock||(this.saveRange(),this.$pasteBlock=a(this.elFromString('
')),this.$pasteBlock.appendTo(this.body),setTimeout(a.proxy(function(){this.clearPaste(this.$pasteBlock);var b="
"+this.$pasteBlock.html()+" ";this.$body.attr("contentEditable","true"),this.$pasteBlock.blur().remove(),this.body.focus(),this.cleartext&&(a.log("Check if paste to clearText Block"),this.isInClearTextBlock()&&(b=this.toBB(b).replace(/\n/g,"
").replace(/\s{3}/g,'
'))),b=b.replace(/\t/g,'
'),this.selectRange(this.lastRange),this.insertAtCursor(b,!1),this.lastRange=!1,this.$pasteBlock=!1},this),1),this.selectNode(this.$pasteBlock[0])),!0):void 0},this)),this.$body.bind("keydown",a.proxy(function(a){if(13==a.which){var b=this.isContain(this.getSelectNode(),"li");b||(a.preventDefault&&a.preventDefault(),this.checkForLastBR(this.getSelectNode()),this.insertAtCursor("
",!1))}},this)),this.options.tabInsert===!0&&this.$body.bind("keydown",a.proxy(this.pressTab,this)),this.$body.bind("mouseup keyup",a.proxy(this.updateUI,this)),this.$body.bind("mousedown",a.proxy(function(a){this.clearLastRange(),this.checkForLastBR(a.target)},this)),this.options.traceTextarea===!0&&(a(document).bind("mousedown",a.proxy(this.traceTextareaEvent,this)),this.$txtArea.val("")),this.options.hotkeys===!0&&this.$body.bind("keydown",a.proxy(this.presskey,this)),this.options.smileConversion===!0&&this.$body.bind("keyup",a.proxy(this.smileConversion,this)),this.inited=!0,this.options.autoresize===!0&&(this.$bresize=a(this.elFromString('
')).appendTo(this.$editor).wdrag({scope:this,axisY:!0,height:b})),this.imgListeners()}this.$txtArea.bind("mouseup keyup",a.proxy(function(){clearTimeout(this.uitimer),this.uitimer=setTimeout(a.proxy(this.updateUI,this),100)},this)),this.options.hotkeys===!0&&a(document).bind("keydown",a.proxy(this.presskey,this))},buildToolbar:function(){if(this.options.toolbar===!1)return!1;this.$toolbar=a("
").addClass("wysibb-toolbar").prependTo(this.$editor);var b;a.each(this.options.buttons,a.proxy(function(c,d){var e=this.options.allButtons[d];(0==c||"|"==d||"-"==d)&&("-"==d&&this.$toolbar.append("
"),b=a('
').appendTo(this.$toolbar)),e&&("colorpicker"==e.type?this.buildColorpicker(b,d,e):"table"==e.type?this.buildTablepicker(b,d,e):"select"==e.type?this.buildSelect(b,d,e):"smilebox"==e.type?this.buildSmilebox(b,d,e):this.buildButton(b,d,e))},this)),this.$toolbar.find(".btn-tooltip").hover(function(){a(this).parent().css("overflow","hidden")},function(){a(this).parent().css("overflow","visible")});var c=a(document.createElement("div")).addClass("wysibb-toolbar-container modeSwitch").html('
[bbcode]
').appendTo(this.$toolbar);1==this.options.bbmode&&c.children(".wysibb-toolbar-btn").addClass("on"),this.options.onlyBBmode===!1&&c.children(".wysibb-toolbar-btn").click(a.proxy(function(b){a(b.currentTarget).toggleClass("on"),this.modeSwitch()},this))},buildButton:function(b,c,d){"object"!=typeof b&&(b=this.$toolbar);var e=d.buttonHTML?a(this.strf(d.buttonHTML,this.options)).addClass("btn-inner"):this.strf('
{text} ',{text:d.buttonText.replace(/['+d.hotkey+"]":"",g=a('