/** * Trumbowyg v1.1.6 - A lightweight WYSIWYG editor * Trumbowyg core file * ------------------------ * @link http://alex-d.github.io/Trumbowyg * @license MIT * @author Alexandre Demode (Alex-D) * Twitter : @AlexandreDemode * Website : alex-d.fr */ jQuery.trumbowyg = { langs: { en: { viewHTML: "View HTML", formatting: "Formatting", p: "Paragraph", blockquote: "Quote", code: "Code", header: "Header", bold: "Bold", italic: "Italic", strikethrough: "Stroke", underline: "Underline", strong: "Strong", em: "Emphasis", del: "Deleted", unorderedList: "Unordered list", orderedList: "Ordered list", insertImage: "Insert Image", insertVideo: "Insert Video", link: "Link", createLink: "Insert link", unlink: "Remove link", justifyLeft: "Align Left", justifyCenter: "Align Center", justifyRight: "Align Right", justifyFull: "Align Justify", horizontalRule: "Insert horizontal rule", fullscreen: "fullscreen", close: "Close", submit: "Confirm", reset: "Cancel", invalidUrl: "Invalid URL", required: "Required", description: "Description", title: "Title", text: "Text" } }, // User default options opts: {}, btnsGrps: { design: ['bold', 'italic', 'underline', 'strikethrough'], semantic: ['strong', 'em', 'del'], justify: ['justifyLeft', 'justifyCenter', 'justifyRight', 'justifyFull'], lists: ['unorderedList', 'orderedList'] } }; (function(window, document, $, undefined){ 'use strict'; // @param : o are options // @param : p are params $.fn.trumbowyg = function(o, p){ if(o === Object(o) || !o){ return this.each(function(){ if(!$(this).data('trumbowyg')) $(this).data('trumbowyg', new Trumbowyg(this, o)); }); } else if(this.length === 1){ try { var t = $(this).data('trumbowyg'); switch(o){ // Modal box case 'openModal': return t.openModal(p.title, p.content); case 'closeModal': return t.closeModal(); case 'openModalInsert': return t.openModalInsert(p.title, p.fields, p.callback); // Selection case 'saveSelection': return t.saveSelection(); case 'getSelection': return t.selection; case 'getSelectedText': return t.selection+''; case 'restoreSelection': return t.restoreSelection(); // Destroy case 'destroy': return t.destroy(); // Empty case 'empty': return t.empty(); // Public options case 'lang': return t.lang; case 'duration': return t.o.duration; // HTML case 'html': return t.html(p); } } catch(e){} } return false; }; var Trumbowyg = function(editorElem, opts){ var t = this; // 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.$e = $(editorElem); t.$creator = $(editorElem); // Extend with options opts = $.extend(true, {}, opts, $.trumbowyg.opts); // Localization management if(typeof opts.lang === 'undefined' || typeof $.trumbowyg.langs[opts.lang] === 'undefined') t.lang = $.trumbowyg.langs.en; else t.lang = $.extend(true, {}, $.trumbowyg.langs.en, $.trumbowyg.langs[opts.lang]); // Defaults Options t.o = $.extend(true, {}, { lang: 'en', dir: 'ltr', duration: 200, // Duration of modal box animations mobile: false, tablet: true, closable: false, fullscreenable: true, fixedBtnPane: false, fixedFullWidth: false, autogrow: false, prefix: 'trumbowyg-', // WYSIWYG only convertLink: true, // TODO semantic: false, resetCss: false, btns: [ 'viewHTML', '|', 'formatting', '|', $.trumbowyg.btnsGrps.design, '|', 'link', '|', 'insertImage', '|', $.trumbowyg.btnsGrps.justify, '|', $.trumbowyg.btnsGrps.lists, '|', 'horizontalRule' ], btnsAdd: [], /** * When the button is associated to a empty object * func and title attributs are defined from the button key value * * For example * foo: {} * is equivalent to : * foo: { * func: 'foo', * title: this.lang.foo * } */ btnsDef: { viewHTML: { func: 'toggle' }, p: { func: 'formatBlock' }, blockquote: { func: 'formatBlock' }, h1: { func: 'formatBlock', title: t.lang.header + ' 1' }, h2: { func: 'formatBlock', title: t.lang.header + ' 2' }, h3: { func: 'formatBlock', title: t.lang.header + ' 3' }, h4: { func: 'formatBlock', title: t.lang.header + ' 4' }, bold: {}, italic: {}, underline: {}, strikethrough: {}, strong: { func: 'bold' }, em: { func: 'italic' }, del: { func: 'strikethrough' }, createLink: {}, unlink: {}, insertImage: {}, justifyLeft: {}, justifyCenter: {}, justifyRight: {}, justifyFull: {}, unorderedList: { func: 'insertUnorderedList' }, orderedList: { func: 'insertOrderedList' }, horizontalRule: { func: 'insertHorizontalRule' }, // Dropdowns formatting: { dropdown: ['p', 'blockquote', 'h1', 'h2', 'h3', 'h4'] }, link: { dropdown: ['createLink', 'unlink'] } } }, opts); if(t.o.semantic && !opts.btns) t.o.btns = [ 'viewHTML', '|', 'formatting', '|', $.trumbowyg.btnsGrps.semantic, '|', 'link', '|', 'insertImage', '|', $.trumbowyg.btnsGrps.justify, '|', $.trumbowyg.btnsGrps.lists, '|', 'horizontalRule' ]; else if(opts && opts.btns) t.o.btns = opts.btns; t.init(); }; Trumbowyg.prototype = { init: function(){ var t = this; t.height = t.$e.css('height'); if(t.isEnabled()){ t.buildEditor(true); return; } t.buildEditor(); t.buildBtnPane(); t.fixedBtnPaneEvents(); t.buildOverlay(); }, buildEditor: function(disable){ var t = this, pfx = t.o.prefix, html = ''; if(disable === true){ if(!t.$e.is('textarea')){ var textarea = t.buildTextarea().val(t.$e.val()); t.$e.hide().after(textarea); } return; } t.$box = $('
', { class: pfx + 'box ' + pfx + t.o.lang + ' trumbowyg' }); t.isTextarea = true; if(t.$e.is('textarea')) t.$editor = $(''); else { t.$editor = t.$e; t.$e = t.buildTextarea().val(t.$e.val()); t.isTextarea = false; } if(t.$creator.is('[placeholder]')) t.$editor.attr('placeholder', t.$creator.attr('placeholder')); t.$e.hide() .addClass(pfx + 'textarea'); if(t.isTextarea){ html = t.$e.val(); t.$box.insertAfter(t.$e) .append(t.$editor) .append(t.$e); } else { html = t.$editor.html(); t.$box.insertAfter(t.$editor) .append(t.$e) .append(t.$editor); t.syncCode(); } t.$editor.addClass(pfx + 'editor') .attr('contenteditable', true) .attr('dir', t.lang._dir || t.o.dir) .html(html); if(t.o.resetCss) t.$editor.addClass(pfx + 'reset-css'); if(!t.o.autogrow){ $.each([t.$editor, t.$e], function(i, $el){ $el.css({ height: t.height, overflow: 'auto' }); }); } if(t.o.semantic){ t.$editor.html( t.$editor.html() .replace('') .replace(' ', '') ); t.semanticCode(); } t.$editor .on('dblclick', 'img', function(e){ var $img = $(this); t.openModalInsert(t.lang.insertImage, { url: { label: 'URL', value: $img.attr('src'), required: true }, alt: { label: 'description', value: $img.attr('alt') } }, function(v){ $img.attr({ src: v.url, alt: v.alt }); }); e.stopPropagation(); }) .on('keyup', function(e){ t.semanticCode(false, e.which === 13); }) .on('focus', function(){ t.$creator.trigger('tbwfocus'); }) .on('blur', function(){ t.syncCode(); t.$creator.trigger('tbwblur'); }); }, // Build the Textarea which contain HTML generated code buildTextarea: function(){ return $('', { name: this.$e.attr('id'), height: this.height }); }, // Build button pane, use o.btns and o.btnsAdd options buildBtnPane: function(){ var t = this, pfx = t.o.prefix; if(t.o.btns === false) return; t.$btnPane = $('
semanticCode: function(force, full){ var t = this; t.syncCode(force); if(t.o.semantic){ t.semanticTag('b', 'strong'); t.semanticTag('i', 'em'); t.semanticTag('strike', 'del'); if(full){ // Wrap text nodes in p t.$editor.contents() .filter(function(){ // Only non-empty text nodes return this.nodeType === 3 && $.trim(this.nodeValue).length > 0; }).wrap('
').end() // Remove all br .filter('br').remove(); t.saveSelection(); t.semanticTag('div', 'p'); t.restoreSelection(); } t.$e.val(t.$editor.html()); } }, semanticTag: function(oldTag, newTag){ $(oldTag, this.$editor).each(function(){ $(this).replaceWith(function(){ return '<'+newTag+'>' + $(this).html() + ''+newTag+'>'; }); }); }, // Function call when user click on "Insert Link" createLink: function(){ var t = this; t.saveSelection(); t.openModalInsert(t.lang.createLink, { url: { label: 'URL', value: 'http://', required: true }, title: { label: t.lang.title, value: t.selection }, text: { label: t.lang.text, value: t.selection } }, function(v){ // v is value t.execCmd('createLink', v.url); var l = $('a[href="'+v.url+'"]:not([title])', t.$box); if(v.text.length > 0) l.text(v.text); if(v.title.length > 0) l.attr('title', v.title); return true; }); }, insertImage: function(){ var t = this; t.saveSelection(); t.openModalInsert(t.lang.insertImage, { url: { label: 'URL', value: 'http://', required: true }, alt: { label: t.lang.description, value: t.selection } }, function(v){ // v are values t.execCmd('insertImage', v.url); $('img[src="'+v.url+'"]:not([alt])', t.$box).attr('alt', v.alt); return true; }); }, /* * Call method of trumbowyg if exist * else try to call anonymous function * and finaly native execCommand */ execCmd: function(cmd, param){ var t = this; if(cmd != 'dropdown') t.$editor.focus(); try { t[cmd](param); } catch(e){ try { cmd(param, t); } catch(e2){ //t.$editor.focus(); if(cmd == 'insertHorizontalRule') param = null; else if(cmd == 'formatBlock' && (navigator.userAgent.indexOf('MSIE') !== -1 || navigator.appVersion.indexOf('Trident/') > 0)) param = '<' + param + '>'; t.doc.execCommand(cmd, false, param); } } t.syncCode(); }, // Open a modal box openModal: function(title, content){ var t = this, pfx = t.o.prefix; // No open a modal box when exist other modal box if($('.' + pfx + 'modal-box', t.$box).size() > 0) return false; t.saveSelection(); t.showOverlay(); // Disable all btnPane btns t.$btnPane.addClass(pfx + 'disable'); // Build out of ModalBox, it's the mask for animations var $modal = $('', { class: pfx + 'modal ' + pfx + 'fixed-top' }).css({ top: (parseInt(t.$btnPane.css('height')) + 1) + 'px' }).appendTo(t.$box); // Click on overflay close modal by cancelling them t.$overlay.one('click', function(e){ e.preventDefault(); $modal.trigger(pfx + 'cancel'); }); // Build the form var $form = $('', { action: '', html: content }) .on('submit', function(e){ e.preventDefault(); $modal.trigger(pfx + 'confirm'); }) .on('reset', function(e){ e.preventDefault(); $modal.trigger(pfx + 'cancel'); }); // Build ModalBox and animate to show them var $box = $('', { class: pfx + 'modal-box', html: $form }) .css({ top: '-' + parseInt(t.$btnPane.outerHeight()) + 'px', opacity: 0 }) .appendTo($modal) .animate({ top: 0, opacity: 1 }, t.o.duration / 2); // Append title $('', { text: title, class: pfx + 'modal-title' }).prependTo($box); // Focus in modal box $box.find('input:first').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, pfx = t.o.prefix; return $('', { class: pfx + 'modal-button ' + pfx + 'modal-' + n, type: n, text: t.lang[n] || n }).appendTo(modal.find('form')); }, // close current modal box closeModal: function(){ var t = this, pfx = t.o.prefix; t.$btnPane.removeClass(pfx + 'disable'); t.$overlay.off(); var $modalBox = $('.' + pfx + 'modal-box', t.$box); $modalBox.animate({ top: '-' + $modalBox.css('height') }, t.o.duration/2, function(){ $(this).parent().remove(); t.hideOverlay(); }); }, // Preformated build and management modal openModalInsert: function(title, fields, cmd){ var t = this, pfx = t.o.prefix, lg = t.lang, html = ''; for(var f in fields){ var fd = fields[f], // field definition label = (fd.label === undefined) ? (lg[f] ? lg[f] : f) : (lg[fd.label] ? lg[fd.label] : fd.label); if(fd.name === undefined) fd.name = f; if(!fd.pattern && f === 'url'){ fd.pattern = /^(http|https):\/\/([\w~#!:.?+=&%@!\-\/]+)$/; fd.patternError = lg.invalidUrl; } html += ''; } return t.openModal(title, html) .on(pfx + 'confirm', function(){ var $form = $(this).find('form'), valid = true, v = {}; // values for(var f in fields){ var $field = $('input[name="'+f+'"]', $form); v[f] = $.trim($field.val()); // Validate value if(fields[f].required && v[f] === ''){ valid = false; t.addErrorOnModalField($field, t.lang.required); } else if(fields[f].pattern && !fields[f].pattern.test(v[f])){ valid = false; t.addErrorOnModalField($field, fields[f].patternError); } } if(valid){ t.restoreSelection(); if(cmd(v, fields)){ t.syncCode(); t.closeModal(); $(this).off(pfx + 'confirm'); } } }) .one(pfx + 'cancel', function(){ $(this).off(pfx + 'confirm'); t.closeModal(); t.restoreSelection(); }); }, addErrorOnModalField: function($field, err){ var pfx = this.o.prefix, $label = $field.parent(); $field.on('change keyup', function(){ $label.removeClass(pfx + 'input-error'); }); $label .addClass(pfx + 'input-error') .find('input+span').append( $('', { class: pfx +'msg-error', text: err }) ); }, // Selection management saveSelection: function(){ var t = this, d = t.doc; t.selection = null; if(window.getSelection){ var s = window.getSelection(); if(s.getRangeAt && s.rangeCount) t.selection = s.getRangeAt(0); } else if(d.selection && d.selection.createRange) t.selection = d.selection.createRange(); }, restoreSelection: function(){ var t = this, range = t.selection; if(range){ if(window.getSelection){ var s = window.getSelection(); s.removeAllRanges(); s.addRange(range); } else if(t.doc.selection && range.select) range.select(); } }, // Return true if must enable Trumbowyg on this mobile device isEnabled: function(){ var exprTablet = new RegExp("(iPad|webOS)"), exprMobile = new RegExp("(iPhone|iPod|Android|BlackBerry|Windows Phone|ZuneWP7)"), ua = navigator.userAgent; return (this.o.tablet === true && exprTablet.test(ua)) || (this.o.mobile === true && exprMobile.test(ua)); } }; })(window, document, jQuery);