jQuery.trumbowyg = { langs: { en: { viewHTML: 'View HTML', undo: 'Undo', redo: 'Redo', formatting: 'Formatting', p: 'Paragraph', blockquote: 'Quote', code: 'Code', header: 'Header', bold: 'Bold', italic: 'Italic', strikethrough: 'Stroke', underline: 'Underline', strong: 'Strong', em: 'Emphasis', del: 'Deleted', superscript: 'Superscript', subscript: 'Subscript', unorderedList: 'Unordered list', orderedList: 'Ordered list', insertImage: 'Insert Image', link: 'Link', createLink: 'Insert link', unlink: 'Remove link', justifyLeft: 'Align Left', justifyCenter: 'Align Center', justifyRight: 'Align Right', justifyFull: 'Align Justify', horizontalRule: 'Insert horizontal rule', removeformat: 'Remove format', fullscreen: 'Fullscreen', close: 'Close', submit: 'Confirm', reset: 'Cancel', required: 'Required', description: 'Description', title: 'Title', text: 'Text', target: 'Target' } }, // Plugins plugins: {}, // SVG Path globally svgPath: null }; (function (navigator, window, document, $) { 'use strict'; $.fn.trumbowyg = function (options, params) { var trumbowygDataName = 'trumbowyg'; if (options === Object(options) || !options) { return this.each(function () { if (!$(this).data(trumbowygDataName)) { $(this).data(trumbowygDataName, new Trumbowyg(this, options)); } }); } if (this.length === 1) { try { var t = $(this).data(trumbowygDataName); switch (options) { // Exec command case 'execCmd': return t.execCmd(params.cmd, params.param, params.forceCss); // Modal box case 'openModal': return t.openModal(params.title, params.content); case 'closeModal': return t.closeModal(); case 'openModalInsert': return t.openModalInsert(params.title, params.fields, params.callback); // Range case 'saveRange': return t.saveRange(); case 'getRange': return t.range; case 'getRangeText': return t.getRangeText(); case 'restoreRange': return t.restoreRange(); // Enable/disable case 'enable': return t.toggleDisable(false); case 'disable': return t.toggleDisable(true); // Destroy case 'destroy': return t.destroy(); // Empty case 'empty': return t.empty(); // HTML case 'html': return t.html(params); } } catch (c) { } } return false; }; // @param: editorElem is the DOM element var Trumbowyg = function (editorElem, options) { var t = this, trumbowygIconsId = 'trumbowyg-icons'; // Get the document of the element. It use to makes the plugin // compatible on iframes. t.doc = editorElem.ownerDocument || document; // jQuery object of the editor t.$ta = $(editorElem); // $ta : Textarea t.$c = $(editorElem); // $c : creator options = options || {}; // Localization management if (options.lang != null || $.trumbowyg.langs[options.lang] != null) { t.lang = $.extend(true, {}, $.trumbowyg.langs.en, $.trumbowyg.langs[options.lang]); } else { t.lang = $.trumbowyg.langs.en; } // SVG path var svgPathOption = $.trumbowyg.svgPath != null ? $.trumbowyg.svgPath : options.svgPath; t.hasSvg = svgPathOption !== false; t.svgPath = !!t.doc.querySelector('base') ? window.location : ''; if ($('#' + trumbowygIconsId, t.doc).length === 0 && svgPathOption !== false) { if (svgPathOption == null) { try { throw new Error(); } catch (e) { var stackLines = e.stack.split('\n'); for (var i in stackLines) { if (!stackLines[i].match(/http[s]?:\/\//)) { continue; } svgPathOption = stackLines[Number(i)].match(/((http[s]?:\/\/.+\/)([^\/]+\.js)):/)[1].split('/'); svgPathOption.pop(); svgPathOption = svgPathOption.join('/') + '/ui/icons.svg'; break; } } } var div = t.doc.createElement('div'); div.id = trumbowygIconsId; t.doc.body.insertBefore(div, t.doc.body.childNodes[0]); $.get(svgPathOption, function (data) { div.innerHTML = new XMLSerializer().serializeToString(data.documentElement); }); } /** * When the button is associated to a empty object * fn and title attributs are defined from the button key value * * For example * foo: {} * is equivalent to : * foo: { * fn: 'foo', * title: this.lang.foo * } */ var h = t.lang.header, // Header translation isBlinkFunction = function () { return (window.chrome || (window.Intl && Intl.v8BreakIterator)) && 'CSS' in window; }; t.btnsDef = { viewHTML: { fn: 'toggle' }, undo: { isSupported: isBlinkFunction, key: 'Z' }, redo: { isSupported: isBlinkFunction, key: 'Y' }, p: { fn: 'formatBlock' }, blockquote: { fn: 'formatBlock' }, h1: { fn: 'formatBlock', title: h + ' 1' }, h2: { fn: 'formatBlock', title: h + ' 2' }, h3: { fn: 'formatBlock', title: h + ' 3' }, h4: { fn: 'formatBlock', title: h + ' 4' }, subscript: { tag: 'sub' }, superscript: { tag: 'sup' }, bold: { key: 'B' }, italic: { key: 'I' }, underline: { tag: 'u' }, strikethrough: { tag: 'strike' }, strong: { fn: 'bold', key: 'B' }, em: { fn: 'italic', key: 'I' }, del: { fn: 'strikethrough' }, createLink: { key: 'K', tag: 'a' }, unlink: {}, insertImage: {}, justifyLeft: { tag: 'left', forceCss: true }, justifyCenter: { tag: 'center', forceCss: true }, justifyRight: { tag: 'right', forceCss: true }, justifyFull: { tag: 'justify', forceCss: true }, unorderedList: { fn: 'insertUnorderedList', tag: 'ul' }, orderedList: { fn: 'insertOrderedList', tag: 'ol' }, horizontalRule: { fn: 'insertHorizontalRule' }, removeformat: {}, fullscreen: { class: 'trumbowyg-not-disable' }, close: { fn: 'destroy', class: 'trumbowyg-not-disable' }, // Dropdowns formatting: { dropdown: ['p', 'blockquote', 'h1', 'h2', 'h3', 'h4'], ico: 'p' }, link: { dropdown: ['createLink', 'unlink'] } }; // Defaults Options t.o = $.extend(true, {}, { lang: 'en', fixedBtnPane: false, fixedFullWidth: false, autogrow: false, prefix: 'trumbowyg-', semantic: true, resetCss: false, removeformatPasted: false, tagsToRemove: [], btnsGrps: { design: ['bold', 'italic', 'underline', 'strikethrough'], semantic: ['strong', 'em', 'del'], justify: ['justifyLeft', 'justifyCenter', 'justifyRight', 'justifyFull'], lists: ['unorderedList', 'orderedList'] }, btns: [ ['viewHTML'], ['undo', 'redo'], ['formatting'], 'btnGrp-semantic', ['superscript', 'subscript'], ['link'], ['insertImage'], 'btnGrp-justify', 'btnGrp-lists', ['horizontalRule'], ['removeformat'], ['fullscreen'] ], // For custom button definitions btnsDef: {}, inlineElementsSelector: 'a,abbr,acronym,b,caption,cite,code,col,dfn,dir,dt,dd,em,font,hr,i,kbd,li,q,span,strikeout,strong,sub,sup,u', pasteHandlers: [], imgDblClickHandler: function () { var $img = $(this), src = $img.attr('src'), base64 = '(Base64)'; if (src.indexOf('data:image') === 0) { src = base64; } t.openModalInsert(t.lang.insertImage, { url: { label: 'URL', value: src, required: true }, alt: { label: t.lang.description, value: $img.attr('alt') } }, function (v) { if (v.src !== base64) { $img.attr({ src: v.src }); } $img.attr({ alt: v.alt }); return true; }); return false; }, plugins: {} }, options); t.disabled = t.o.disabled || (editorElem.nodeName === 'TEXTAREA' && editorElem.disabled); if (options.btns) { t.o.btns = options.btns; } else if (!t.o.semantic) { t.o.btns[4] = 'btnGrp-design'; } $.each(t.o.btnsDef, function (btnName, btnDef) { t.addBtnDef(btnName, btnDef); }); // Keyboard shortcuts are load in this array t.keys = []; // Tag to button dynamically hydrated t.tagToButton = {}; t.tagHandlers = []; // Admit multiple paste handlers t.pasteHandlers = [].concat(t.o.pasteHandlers); t.init(); }; Trumbowyg.prototype = { init: function () { var t = this; t.height = t.$ta.height(); t.initPlugins(); // Disable image resize in Firefox t.doc.execCommand('enableObjectResizing', false, false); t.doc.execCommand('defaultParagraphSeparator', false, 'p'); t.buildEditor(); t.buildBtnPane(); t.fixedBtnPaneEvents(); t.buildOverlay(); setTimeout(function () { if (t.disabled) { t.toggleDisable(true); } t.$c.trigger('tbwinit'); }); }, addBtnDef: function (btnName, btnDef) { this.btnsDef[btnName] = btnDef; }, buildEditor: function () { var t = this, prefix = t.o.prefix, html = ''; t.$box = $('
', { class: prefix + 'box ' + prefix + 'editor-visible ' + prefix + t.o.lang + ' trumbowyg' }); // $ta = Textarea // $ed = Editor t.isTextarea = t.$ta.is('textarea'); if (t.isTextarea) { html = t.$ta.val(); t.$ed = $(''); t.$box .insertAfter(t.$ta) .append(t.$ed, t.$ta); } else { t.$ed = t.$ta; html = t.$ed.html(); t.$ta = $('', { name: t.$ta.attr('id'), height: t.height }).val(html); t.$box .insertAfter(t.$ed) .append(t.$ta, t.$ed); t.syncCode(); } t.$ta .addClass(prefix + 'textarea') .attr('tabindex', -1) ; t.$ed .addClass(prefix + 'editor') .attr({ contenteditable: true, dir: t.lang._dir || 'ltr' }) .html(html) ; if (t.o.tabindex) { t.$ed.attr('tabindex', t.o.tabindex); } if (t.$c.is('[placeholder]')) { t.$ed.attr('placeholder', t.$c.attr('placeholder')); } if (t.o.resetCss) { t.$ed.addClass(prefix + 'reset-css'); } if (!t.o.autogrow) { t.$ta.add(t.$ed).css({ height: t.height }); } t.semanticCode(); t._ctrl = false; t.$ed .on('dblclick', 'img', t.o.imgDblClickHandler) .on('keydown', function (e) { t._composition = (e.which === 229); if (e.ctrlKey) { t._ctrl = true; var k = t.keys[String.fromCharCode(e.which).toUpperCase()]; try { t.execCmd(k.fn, k.param); return false; } catch (c) { } } }) .on('keyup', function (e) { if (e.which >= 37 && e.which <= 40) { return; } if (e.ctrlKey && (e.which === 89 || e.which === 90)) { t.$c.trigger('tbwchange'); } else if (!t._ctrl && e.which !== 17 && !t._composition) { t.semanticCode(false, e.which === 13); t.$c.trigger('tbwchange'); } setTimeout(function () { t._ctrl = false; }, 200); }) .on('mouseup keydown keyup', function () { t.updateButtonPaneStatus(); }) .on('focus blur', function (e) { t.$c.trigger('tbw' + e.type); if (e.type === 'blur') { $('.' + prefix + 'active-button', t.$btnPane).removeClass(prefix + 'active-button ' + prefix + 'active'); } }) .on('cut', function () { t.$c.trigger('tbwchange'); }) .on('paste', function (e) { if (t.o.removeformatPasted) { e.preventDefault(); try { // IE var text = window.clipboardData.getData('Text'); try { // <= IE10 t.doc.selection.createRange().pasteHTML(text); } catch (c) { // IE 11 t.doc.getSelection().getRangeAt(0).insertNode(t.doc.createTextNode(text)); } } catch (d) { // Not IE t.execCmd('insertText', (e.originalEvent || e).clipboardData.getData('text/plain')); } } // Call pasteHandlers $.each(t.pasteHandlers, function (i, pasteHandler) { pasteHandler(e); }); setTimeout(function () { if (t.o.semantic) { t.semanticCode(false, true); } else { t.syncCode(); } t.$c.trigger('tbwpaste', e); }, 0); }); t.$ta.on('keyup paste', function () { t.$c.trigger('tbwchange'); }); $(t.doc).on('keydown', function (e) { if (e.which === 27) { t.closeModal(); return false; } }); }, // Build button pane, use o.btns option buildBtnPane: function () { var t = this, prefix = t.o.prefix; var $btnPane = t.$btnPane = $('', { class: prefix + 'button-pane' }); $.each(t.o.btns, function (i, btnGrps) { // Managment of group of buttons try { var b = btnGrps.split('btnGrp-'); if (b[1] != null) { btnGrps = t.o.btnsGrps[b[1]]; } } catch (c) { } if (!$.isArray(btnGrps)) { btnGrps = [btnGrps]; } var $btnGroup = $('', { class: prefix + 'button-group ' + ((btnGrps.indexOf('fullscreen') >= 0) ? prefix + 'right' : '') }); $.each(btnGrps, function (i, btn) { try { // Prevent buildBtn error var $item; if (t.isSupportedBtn(btn)) { // It's a supported button $item = t.buildBtn(btn); } $btnGroup.append($item); } catch (c) { } }); $btnPane.append($btnGroup); }); t.$box.prepend($btnPane); }, // Build a button and his action buildBtn: function (btnName) { // btnName is name of the button var t = this, prefix = t.o.prefix, btn = t.btnsDef[btnName], isDropdown = btn.dropdown, textDef = t.lang[btnName] || btnName, $btn = $('', { type: 'button', class: prefix + btnName + '-button ' + (btn.class || ''), html: t.hasSvg ? '' : '', title: (btn.title || btn.text || textDef) + ((btn.key) ? ' (Ctrl + ' + btn.key + ')' : ''), tabindex: -1, mousedown: function () { if (!isDropdown || $('.' + btnName + '-' + prefix + 'dropdown', t.$box).is(':hidden')) { $('body', t.doc).trigger('mousedown'); } if (t.$btnPane.hasClass(prefix + 'disable') && !$(this).hasClass(prefix + 'active') && !$(this).hasClass(prefix + 'not-disable')) { return false; } t.execCmd((isDropdown ? 'dropdown' : false) || btn.fn || btnName, btn.param || btnName, btn.forceCss || false); return false; } }); if (isDropdown) { $btn.addClass(prefix + 'open-dropdown'); var dropdownPrefix = prefix + 'dropdown', $dropdown = $('', { // the dropdown class: dropdownPrefix + '-' + btnName + ' ' + dropdownPrefix + ' ' + prefix + 'fixed-top', 'data-dropdown': btnName }); $.each(isDropdown, function (i, def) { if (t.btnsDef[def] && t.isSupportedBtn(def)) { $dropdown.append(t.buildSubBtn(def)); } }); t.$box.append($dropdown.hide()); } else if (btn.key) { t.keys[btn.key] = { fn: btn.fn || btnName, param: btn.param || btnName }; } if (!isDropdown) { t.tagToButton[(btn.tag || btnName).toLowerCase()] = btnName; } return $btn; }, // Build a button for dropdown menu // @param n : name of the subbutton buildSubBtn: function (btnName) { var t = this, prefix = t.o.prefix, btn = t.btnsDef[btnName]; if (btn.key) { t.keys[btn.key] = { fn: btn.fn || btnName, param: btn.param || btnName }; } t.tagToButton[(btn.tag || btnName).toLowerCase()] = btnName; return $('', { type: 'button', class: prefix + btnName + '-dropdown-button' + (btn.ico ? ' ' + prefix + btn.ico + '-button' : ''), html: t.hasSvg ? '' + (btn.text || btn.title || t.lang[btnName] || btnName) : '', title: ((btn.key) ? ' (Ctrl + ' + btn.key + ')' : null), style: btn.style || null, mousedown: function () { $('body', t.doc).trigger('mousedown'); t.execCmd(btn.fn || btnName, btn.param || btnName, btn.forceCss || false); return false; } }); }, // Check if button is supported isSupportedBtn: function (b) { try { return this.btnsDef[b].isSupported(); } catch (c) { } return true; }, // Build overlay for modal box buildOverlay: function () { var t = this; t.$overlay = $('', { class: t.o.prefix + 'overlay' }).css({ top: t.$btnPane.outerHeight(), height: (t.$ed.outerHeight() + 1) + 'px' }).appendTo(t.$box); return t.$overlay; }, showOverlay: function () { var t = this; $(window).trigger('scroll'); t.$overlay.fadeIn(200); t.$box.addClass(t.o.prefix + 'box-blur'); }, hideOverlay: function () { var t = this; t.$overlay.fadeOut(50); t.$box.removeClass(t.o.prefix + 'box-blur'); }, // Management of fixed button pane fixedBtnPaneEvents: function () { var t = this, fixedFullWidth = t.o.fixedFullWidth, $box = t.$box; if (!t.o.fixedBtnPane) { return; } t.isFixed = false; $(window) .on('scroll resize', function () { if (!$box) { return; } t.syncCode(); var scrollTop = $(window).scrollTop(), offset = $box.offset().top + 1, bp = t.$btnPane, oh = bp.outerHeight() - 2; if ((scrollTop - offset > 0) && ((scrollTop - offset - t.height) < 0)) { if (!t.isFixed) { t.isFixed = true; bp.css({ position: 'fixed', top: 0, left: fixedFullWidth ? '0' : 'auto', zIndex: 7 }); $([t.$ta, t.$ed]).css({marginTop: bp.height()}); } bp.css({ width: fixedFullWidth ? '100%' : (($box.width() - 1) + 'px') }); $('.' + t.o.prefix + 'fixed-top', $box).css({ position: fixedFullWidth ? 'fixed' : 'absolute', top: fixedFullWidth ? oh : oh + (scrollTop - offset) + 'px', zIndex: 15 }); } else if (t.isFixed) { t.isFixed = false; bp.removeAttr('style'); $([t.$ta, t.$ed]).css({marginTop: 0}); $('.' + t.o.prefix + 'fixed-top', $box).css({ position: 'absolute', top: oh }); } }); }, // Disable editor toggleDisable: function (disable) { var t = this, prefix = t.o.prefix; t.disabled = disable; if (disable) { t.$ta.attr('disabled', true); } else { t.$ta.removeAttr('disabled'); } t.$box.toggleClass(prefix + 'disabled', disable); t.$ed.attr('contenteditable', !disable); }, // Destroy the editor destroy: function () { var t = this, prefix = t.o.prefix, height = t.height; if (t.isTextarea) { t.$box.after( t.$ta .css({height: height}) .val(t.html()) .removeClass(prefix + 'textarea') .show() ); } else { t.$box.after( t.$ed .css({height: height}) .removeClass(prefix + 'editor') .removeAttr('contenteditable') .html(t.html()) .show() ); } t.$ed.off('dblclick', 'img'); t.destroyPlugins(); t.$box.remove(); t.$c.removeData('trumbowyg'); $('body').removeClass(prefix + 'body-fullscreen'); }, // Empty the editor empty: function () { this.$ta.val(''); this.syncCode(true); }, // Function call when click on viewHTML button toggle: function () { var t = this, prefix = t.o.prefix; t.semanticCode(false, true); setTimeout(function () { t.doc.activeElement.blur(); t.$box.toggleClass(prefix + 'editor-hidden ' + prefix + 'editor-visible'); t.$btnPane.toggleClass(prefix + 'disable'); $('.' + prefix + 'viewHTML-button', t.$btnPane).toggleClass(prefix + 'active'); if (t.$box.hasClass(prefix + 'editor-visible')) { t.$ta.attr('tabindex', -1); } else { t.$ta.removeAttr('tabindex'); } }, 0); }, // Open dropdown when click on a button which open that dropdown: function (name) { var t = this, d = t.doc, prefix = t.o.prefix, $dropdown = $('[data-dropdown=' + name + ']', t.$box), $btn = $('.' + prefix + name + '-button', t.$btnPane), show = $dropdown.is(':hidden'); $('body', d).trigger('mousedown'); if (show) { var o = $btn.offset().left; $btn.addClass(prefix + 'active'); $dropdown.css({ position: 'absolute', top: $btn.offset().top - t.$btnPane.offset().top + $btn.outerHeight(), left: (t.o.fixedFullWidth && t.isFixed) ? o + 'px' : (o - t.$btnPane.offset().left) + 'px' }).show(); $(window).trigger('scroll'); $('body', d).on('mousedown', function () { $('.' + prefix + 'dropdown', d).hide(); $('.' + prefix + 'active', d).removeClass(prefix + 'active'); $('body', d).off('mousedown'); }); } }, // HTML Code management html: function (html) { var t = this; if (html != null) { t.$ta.val(html); t.syncCode(true); return t; } return t.$ta.val(); }, syncTextarea: function () { var t = this; t.$ta.val(t.$ed.text().trim().length > 0 || t.$ed.find('hr,img,embed,input').length > 0 ? t.$ed.html() : ''); }, syncCode: function (force) { var t = this; if (!force && t.$ed.is(':visible')) { t.syncTextarea(); } else { t.$ed.html(t.$ta.val()); } if (t.o.autogrow) { t.height = t.$ed.height(); if (t.height !== t.$ta.css('height')) { t.$ta.css({height: t.height}); t.$c.trigger('tbwresize'); } } }, // Analyse and update to semantic code // @param force : force to sync code from textarea // @param full : wrap text nodes insemanticCode: function (force, full) { var t = this; t.saveRange(); t.syncCode(force); $(t.o.tagsToRemove.join(','), t.$ed).remove(); if (t.o.semantic) { t.semanticTag('b', 'strong'); t.semanticTag('i', 'em'); t.semanticTag('strike', 'del'); if (full) { var inlineElementsSelector = t.o.inlineElementsSelector, blockElementsSelector = ':not(' + inlineElementsSelector + ')'; // Wrap text nodes in span for easier processing t.$ed.contents().filter(function () { return this.nodeType === 3 && this.nodeValue.trim().length > 0; }).wrap(''); // Wrap groups of inline elements in paragraphs (recursive) var wrapInlinesInParagraphsFrom = function ($from) { if ($from.length !== 0) { var $finalParagraph = $from.nextUntil(blockElementsSelector).andSelf().wrapAll('
').parent(), $nextElement = $finalParagraph.nextAll(inlineElementsSelector).first(); $finalParagraph.next('br').remove(); wrapInlinesInParagraphsFrom($nextElement); } }; wrapInlinesInParagraphsFrom(t.$ed.children(inlineElementsSelector).first()); t.semanticTag('div', 'p', true); // Unwrap paragraphs content, containing nothing usefull t.$ed.find('p').filter(function () { // Don't remove currently being edited element if (t.range && this === t.range.startContainer) { return false; } return $(this).text().trim().length === 0 && $(this).children().not('br,span').length === 0; }).contents().unwrap(); // Get rid of temporial span's $('[data-tbw]', t.$ed).contents().unwrap(); // Remove emptyt.$ed.find('p:empty').remove(); } t.restoreRange(); t.syncTextarea(); } }, semanticTag: function (oldTag, newTag, copyAttributes) { $(oldTag, this.$ed).each(function () { var $oldTag = $(this); $oldTag.wrap('<' + newTag + '/>'); if (copyAttributes) { $.each($oldTag.prop('attributes'), function () { $oldTag.parent().attr(this.name, this.value); }); } $oldTag.contents().unwrap(); }); }, // Function call when user click on "Insert Link" createLink: function () { var t = this, documentSelection = t.doc.getSelection(), node = documentSelection.focusNode, url, title, target; while (['A', 'DIV'].indexOf(node.nodeName) < 0) { node = node.parentNode; } if (node && node.nodeName === 'A') { var $a = $(node); url = $a.attr('href'); title = $a.attr('title'); target = $a.attr('target'); var range = t.doc.createRange(); range.selectNode(node); documentSelection.addRange(range); } t.saveRange(); t.openModalInsert(t.lang.createLink, { url: { label: 'URL', required: true, value: url }, title: { label: t.lang.title, value: title }, text: { label: t.lang.text, value: t.getRangeText() }, target: { label: t.lang.target, value: target } }, function (v) { // v is value var link = $(['', v.text, ''].join('')); if (v.title.length > 0) { link.attr('title', v.title); } if (v.target.length > 0) { link.attr('target', v.target); } t.range.deleteContents(); t.range.insertNode(link[0]); return true; }); }, unlink: function () { var t = this, documentSelection = t.doc.getSelection(), node = documentSelection.focusNode; if (documentSelection.isCollapsed) { while (['A', 'DIV'].indexOf(node.nodeName) < 0) { node = node.parentNode; } if (node && node.nodeName === 'A') { var range = t.doc.createRange(); range.selectNode(node); documentSelection.addRange(range); } } t.execCmd('unlink', undefined, undefined, true); }, insertImage: function () { var t = this; t.saveRange(); t.openModalInsert(t.lang.insertImage, { url: { label: 'URL', required: true }, alt: { label: t.lang.description, value: t.getRangeText() } }, function (v) { // v are values t.execCmd('insertImage', v.url); $('img[src="' + v.url + '"]:not([alt])', t.$box).attr('alt', v.alt); return true; }); }, fullscreen: function () { var t = this, prefix = t.o.prefix, fullscreenCssClass = prefix + 'fullscreen', isFullscreen; t.$box.toggleClass(fullscreenCssClass); isFullscreen = t.$box.hasClass(fullscreenCssClass); $('body').toggleClass(prefix + 'body-fullscreen', isFullscreen); $(window).trigger('scroll'); t.$c.trigger('tbw' + (isFullscreen ? 'open' : 'close') + 'fullscreen'); }, /* * Call method of trumbowyg if exist * else try to call anonymous function * and finaly native execCommand */ execCmd: function (cmd, param, forceCss, skipTrumbowyg) { var t = this; skipTrumbowyg = !!skipTrumbowyg || ''; if (cmd !== 'dropdown') { t.$ed.focus(); } t.doc.execCommand('styleWithCSS', false, forceCss || false); try { t[cmd + skipTrumbowyg](param); } catch (c) { try { cmd(param); } catch (e2) { if (cmd === 'insertHorizontalRule') { param = undefined; } else if (cmd === 'formatBlock' && (navigator.userAgent.indexOf('MSIE') !== -1 || navigator.appVersion.indexOf('Trident/') !== -1)) { param = '<' + param + '>'; } t.doc.execCommand(cmd, false, param); t.syncCode(); t.semanticCode(false, true); } if (cmd !== 'dropdown') { t.updateButtonPaneStatus(); t.$c.trigger('tbwchange'); } } }, // Open a modal box openModal: function (title, content) { var t = this, prefix = t.o.prefix; // No open a modal box when exist other modal box if ($('.' + prefix + 'modal-box', t.$box).length > 0) { return false; } t.saveRange(); t.showOverlay(); // Disable all btnPane btns t.$btnPane.addClass(prefix + 'disable'); // Build out of ModalBox, it's the mask for animations var $modal = $('
', { class: prefix + 'modal ' + prefix + 'fixed-top' }).css({ top: t.$btnPane.height() }).appendTo(t.$box); // Click on overlay close modal by cancelling them t.$overlay.one('click', function () { $modal.trigger('tbwcancel'); return false; }); // Build the form var $form = $('', { action: '', html: content }) .on('submit', function () { $modal.trigger('tbwconfirm'); return false; }) .on('reset', function () { $modal.trigger('tbwcancel'); return false; }); // Build ModalBox and animate to show them var $box = $('', { class: prefix + 'modal-box', html: $form }) .css({ top: '-' + t.$btnPane.outerHeight() + 'px', opacity: 0 }) .appendTo($modal) .animate({ top: 0, opacity: 1 }, 100); // Append title $('', { text: title, class: prefix + 'modal-title' }).prependTo($box); $modal.height($box.outerHeight() + 10); // Focus in modal box $('input:first', $box).focus(); // Append Confirm and Cancel buttons t.buildModalBtn('submit', $box); t.buildModalBtn('reset', $box); $(window).trigger('scroll'); return $modal; }, // @param n is name of modal buildModalBtn: function (n, $modal) { var t = this, prefix = t.o.prefix; return $('', { class: prefix + 'modal-button ' + prefix + 'modal-' + n, type: n, text: t.lang[n] || n }).appendTo($('form', $modal)); }, // close current modal box closeModal: function () { var t = this, prefix = t.o.prefix; t.$btnPane.removeClass(prefix + 'disable'); t.$overlay.off(); // Find the modal box var $mb = $('.' + prefix + 'modal-box', t.$box); $mb.animate({ top: '-' + $mb.height() }, 100, function () { $mb.parent().remove(); t.hideOverlay(); }); t.restoreRange(); }, // Preformated build and management modal openModalInsert: function (title, fields, cmd) { var t = this, prefix = t.o.prefix, lg = t.lang, html = '', CONFIRM_EVENT = 'tbwconfirm'; $.each(fields, function (fieldName, field) { var l = field.label, n = field.name || fieldName; html += ''; }); return t.openModal(title, html) .on(CONFIRM_EVENT, function () { var $form = $('form', $(this)), valid = true, values = {}; $.each(fields, function (fieldName, field) { var $field = $('input[name="' + fieldName + '"]', $form); values[fieldName] = $.trim($field.val()); // Validate value if (field.required && values[fieldName] === '') { valid = false; t.addErrorOnModalField($field, t.lang.required); } else if (field.pattern && !field.pattern.test(values[fieldName])) { valid = false; t.addErrorOnModalField($field, field.patternError); } }); if (valid) { t.restoreRange(); if (cmd(values, fields)) { t.syncCode(); t.$c.trigger('tbwchange'); t.closeModal(); $(this).off(CONFIRM_EVENT); } } }) .one('tbwcancel', function () { $(this).off(CONFIRM_EVENT); t.closeModal(); }); }, addErrorOnModalField: function ($field, err) { var prefix = this.o.prefix, $label = $field.parent(); $field .on('change keyup', function () { $label.removeClass(prefix + 'input-error'); }); $label .addClass(prefix + 'input-error') .find('input+span') .append( $('', { class: prefix + 'msg-error', text: err }) ); }, // Range management saveRange: function () { var t = this, documentSelection = t.doc.getSelection(); t.range = null; if (documentSelection.rangeCount) { var savedRange = t.range = documentSelection.getRangeAt(0), range = t.doc.createRange(), rangeStart; range.selectNodeContents(t.$ed[0]); range.setEnd(savedRange.startContainer, savedRange.startOffset); rangeStart = (range + '').length; t.metaRange = { start: rangeStart, end: rangeStart + (savedRange + '').length }; } }, restoreRange: function () { var t = this, metaRange = t.metaRange, savedRange = t.range, documentSelection = t.doc.getSelection(), range; if (!savedRange) { return; } if (metaRange && metaRange.start !== metaRange.end) { // Algorithm from http://jsfiddle.net/WeWy7/3/ var charIndex = 0, nodeStack = [t.$ed[0]], node, foundStart = false, stop = false; range = t.doc.createRange(); while (!stop && (node = nodeStack.pop())) { if (node.nodeType === 3) { var nextCharIndex = charIndex + node.length; if (!foundStart && metaRange.start >= charIndex && metaRange.start <= nextCharIndex) { range.setStart(node, metaRange.start - charIndex); foundStart = true; } if (foundStart && metaRange.end >= charIndex && metaRange.end <= nextCharIndex) { range.setEnd(node, metaRange.end - charIndex); stop = true; } charIndex = nextCharIndex; } else { var cn = node.childNodes, i = cn.length; while (i > 0) { i -= 1; nodeStack.push(cn[i]); } } } } documentSelection.removeAllRanges(); documentSelection.addRange(range || savedRange); }, getRangeText: function () { return this.range + ''; }, updateButtonPaneStatus: function () { var t = this, prefix = t.o.prefix, tags = t.getTagsRecursive(t.doc.getSelection().focusNode.parentNode), activeClasses = prefix + 'active-button ' + prefix + 'active'; $('.' + prefix + 'active-button', t.$btnPane).removeClass(activeClasses); $.each(tags, function (i, tag) { var btnName = t.tagToButton[tag.toLowerCase()], $btn = $('.' + prefix + btnName + '-button', t.$btnPane); if ($btn.length > 0) { $btn.addClass(activeClasses); } else { try { $btn = $('.' + prefix + 'dropdown .' + prefix + btnName + '-dropdown-button', t.$box); var dropdownBtnName = $btn.parent().data('dropdown'); $('.' + prefix + dropdownBtnName + '-button', t.$box).addClass(activeClasses); } catch (e) { } } }); }, getTagsRecursive: function (element, tags) { var t = this; tags = tags || []; var tag = element.tagName; if (tag === 'DIV') { return tags; } if (tag === 'P' && element.style.textAlign !== '') { tags.push(element.style.textAlign); } $.each(t.tagHandlers, function (i, tagHandler) { tags = tags.concat(tagHandler(element, t)); }); tags.push(tag); return t.getTagsRecursive(element.parentNode, tags); }, // Plugins initPlugins: function () { var t = this; t.loadedPlugins = []; $.each($.trumbowyg.plugins, function (name, plugin) { if (!plugin.shouldInit || plugin.shouldInit(t)) { plugin.init(t); if (plugin.tagHandler) { t.tagHandlers.push(plugin.tagHandler); } t.loadedPlugins.push(plugin); } }); }, destroyPlugins: function () { $.each(this.loadedPlugins, function (i, plugin) { if (plugin.destroy) { plugin.destroy(); } }); } }; })(navigator, window, document, jQuery);