/* =========================================================== * trumbowyg.js v1.0 * Core code of Trumbowyg plugin * http://alex-d.github.com/Trumbowyg * =========================================================== * Author : Alexandre Demode (Alex-D) * Twitter : @AlexandreDemode * Website : alex-d.fr */ $.trumbowyg = { langs: { en: { viewHTML: "Voir le HTML", formatting: "Format", p: "Paragraphe", blockquote: "Citation", code: "Code", header: "Titre", bold: "Gras", italic: "Italique", strikethrough: "Rayé", underline: "Souligné", strong: "Fort", em: "Emphase", del: "Supprimé", unorderedList: "Liste à puces", orderedList: "Liste ordonnée", insertImage: "Insérer une image", insertVideo: "Insérer une video", link: "Lien", createLink: "Insérer un lien", unlink: "Supprimer le lien", justifyLeft: "Aligner à gauche", justifyCenter: "Centrer", justifyRight: "Aligner à droite", justifyFull: "Justifier", horizontalRule: "Insérer un séparateur horizontal", fullscreen: "Plein écran", close: "Fermer", submit: "Valider", reset: "Annuler", invalidUrl: "URL invalide", required: "Obligatoire", description: "Description", title: "Title", text: "Text" } }, // User default options opts: {}, btnsGrps: { design: ['bold', 'italic', 'underline', 'strikethrough', 'upload'], semantic: ['strong', 'em', 'del'], justify: ['justifyLeft', 'justifyCenter', 'justifyRight', 'justifyFull'], lists: ['unorderedList', 'orderedList'] } }; (function($){ $.fn.trumbowyg = function(opts, params){ if($.isObject(opts) || opts == null){ return this.each(function(){ if(!$(this).data('trumbowyg')) $(this).data('trumbowyg', new Trumbowyg(this, opts)); }); } else if(this.length == 1){ try { var t = $(this).data('trumbowyg'); switch(opts){ // 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); // 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(params); } } catch(e){} } return false; }; var Trumbowyg = function(editorElem, opts){ // jQuery object of the editor this.$e = $(editorElem); this.$creator = $(editorElem); // Extend with options opts = $.extend(true, {}, opts, $.trumbowyg.opts); // Localization management if(typeof opts.lang === 'undefined' || typeof $.trumbowyg.langs[opts.lang] === 'undefined') this.lang = $.trumbowyg.langs['en']; else this.lang = $.extend(true, {}, $.trumbowyg.langs['en'], $.trumbowyg.langs[opts.lang]); // Defaults Options this.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, semantic: false, resetCss: false, autogrow: false, prefix: 'trumbowyg-', convertLink: true, btns: [//'viewHTML', //'|', 'formatting', '|', $.trumbowyg.btnsGrps.design, '|', 'link', '|', $.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: this.lang.header + ' 1' }, h2: { func: 'formatBlock', title: this.lang.header + ' 2' }, h3: { func: 'formatBlock', title: this.lang.header + ' 3' }, h4: { func: 'formatBlock', title: this.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(this.o.semantic && !opts.btns) this.o.btns = [ 'viewHTML', '|', 'formatting', '|', $.trumbowyg.btnsGrps.semantic, '|', 'link', '|', 'insertImage', '|', $.trumbowyg.btnsGrps.justify, '|', $.trumbowyg.btnsGrps.lists, '|', 'horizontalRule' ]; else if(opts && opts.btns) this.o.btns = opts.btns; this.init(); } Trumbowyg.prototype = { init: function(){ this.height = this.$e.css('height'); if(this.isEnabled()){ this.buildEditor(true); return; } this.buildEditor(); this.buildBtnPane(); this.fixedBtnPaneEvents(); this.buildOverlay(); }, buildEditor: function(disable){ if(disable === true){ if(!this.$e.is('textarea')){ var textarea = this.buildTextarea().val(this.$e.val()); this.$e.hide().after(textarea); } return; } this.$box = $('
', { 'class': this.o.prefix + 'box ' + this.o.prefix + this.o.lang + ' trumbowyg' }); this.isTextarea = true; if(this.$e.is('textarea')) this.$editor = $(''); else { this.$editor = this.$e; this.$e = this.buildTextarea().val(this.$e.val()); this.isTextarea = false; } this.$e.hide() .addClass(this.o.prefix + 'textarea'); var html = ''; if(this.isTextarea){ html = this.$e.val(); this.$box.insertAfter(this.$e) .append(this.$editor) .append(this.$e); } else { html = this.$editor.html(); this.$box.insertAfter(this.$editor) .append(this.$e) .append(this.$editor); this.syncCode(); } this.$editor.addClass(this.o.prefix + 'editor') .attr('contenteditable', true) .attr('dir', this.o.dir) .html(html); if(this.o.resetCss) this.$editor.addClass(this.o.prefix + 'reset-css'); if(!this.o.autogrow){ $.each([this.$editor, this.$e], $.proxy(function(i, $el){ $el.css({ height: this.height, overflow: 'auto' }); }, this)); } if(this.o.semantic){ this.$editor.html( this.$editor.html() .replace("") .replace("&nsbp;", "") ); this.semanticCode(); } var that = this; this.$editor .on('dblclick', 'img', function(){ var $img = $(this); that.openModalInsert(that.lang.insertImage, { url: { label: 'URL', value: $img.attr('src'), required: true }, alt: { label: 'description', value: $img.attr('alt') } }, function(values){ $img.attr('src', values['url']); $img.attr('alt', values['alt']); }); return false; }) .on('keyup', function(e){ that.semanticCode(false, e.which === 13); }) .on('blur', function(){ that.syncCode(); }); }, // 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; if(t.o.btns === false) return; var pfx = t.o.prefix; t.$btnPane = $('
semanticCode: function(force, full){ this.syncCode(force); if(this.o.semantic){ this.semanticTag('b', 'strong'); this.semanticTag('i', 'em'); this.semanticTag('strike', 'del'); if(full){ // Wrap text nodes in p this.$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(); this.saveSelection(); this.semanticTag('div', 'p'); this.restoreSelection(); } this.$e.val(this.$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 that = this; this.saveSelection(); this.openModalInsert(this.lang.createLink, { url: { label: 'URL', value: 'http://', required: true }, title: { label: this.lang.title, value: this.selection }, text: { label: this.lang.text, value: this.selection } }, function(values){ that.execCommand('createLink', values['url']); var link = $(['a[href="', values['url'], '"]:not([title])'].join(''), that.$box); if($.trim(values['text']).length !== 0) link.text(values['text']); if($.trim(values['title']).length !== 0) link.attr('title', values['title']); return true; }); }, insertImage: function(){ var that = this; this.saveSelection(); this.openModalInsert(this.lang.insertImage, { url: { label: 'URL', value: 'http://', required: true }, alt: { label: 'description', value: this.selection } }, function(values){ that.execCommand('insertImage', values['url']); $(['img[src="', values['url'], '"]:not([alt])'].join(''), that.$box).attr('alt', values['alt']); return true; }); }, /* * Call method of trumbowyg if exist * else try to call anonymous function * and finaly native execCommand */ execCommand: function(cmd, param){ if(cmd != 'dropdown') this.$editor.focus(); try { this[cmd](param); } catch(e){ try { cmd(param, this); } catch(e){ this.$editor.focus(); if(cmd == 'insertHorizontalRule') param = null; document.execCommand(cmd, false, param); } } this.syncCode(); }, formatBlock: function(param){ if($.browser.msie) param = '<' + param + '>'; document.execCommand('formatBlock', false, param); }, // Open a modal box openModal: function(title, content){ var pfx = this.o.prefix; // No open a modal box when exist other modal box if($('.' + pfx + 'modal-box', this.$box).size() > 0) return false; this.saveSelection(); this.showOverlay(); // Disable all btnPane btns this.$btnPane.addClass(pfx + 'disable'); $('.' + pfx + 'not-disable', this.$btnPane) .not('.' + pfx + 'buttons-right') .removeClass(pfx + 'not-disable') .addClass(pfx + 'not-disable-old'); // Build out of ModalBox, it's the mask for animations var $modal = $('', { 'class': pfx + 'modal ' + pfx + 'fixed-top' }).css({ top: (parseInt(this.$btnPane.css('height')) + 1) + 'px' }).appendTo(this.$box); // Click on overflay close modal by cancelling them this.$overlay.one('click', function(e){ e.preventDefault(); $modal.trigger(pfx + 'cancel'); }); $e = this.$editor; // Build the form $form = $('', { action: 'javascript:void(null);', 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 $modalBox = $('', { 'class': pfx + 'modal-box', html: $form }) .css({ top: '-' + parseInt(this.$btnPane.outerHeight()) + 'px', opacity: 0 }) .appendTo($modal) .animate({ top: 0, opacity: 1 }, this.o.duration / 2); // Append title $('', { text: title, 'class': pfx + 'modal-title' }).prependTo($modalBox); // Focus in modal box $modalBox.find('input:first').focus(); // Append Confirm and Cancel buttons this.buildModalBtn('submit', $modalBox); this.buildModalBtn('reset', $modalBox); $('body').trigger('scroll'); return $modal; }, buildModalBtn: function(name, modal){ return $('', { 'class': this.o.prefix + 'modal-button ' + this.o.prefix + 'modal-' + name, 'type': name, 'text': this.lang[name] || name }).appendTo(modal.find('form')); }, // close current modal box closeModal: function(){ var pfx = this.o.prefix; this.$btnPane.removeClass(pfx + 'disable'); this.$overlay.off(); $('.' + this.o.prefix + 'not-disable-old', this.$btnPane) .removeClass(pfx + 'not-disable-old') .addClass(pfx + 'not-disable'); var that = this, $modalBox = $('.' + pfx + 'modal-box', this.$box); $modalBox.animate({ top: '-' + $modalBox.css('height') }, this.o.duration/2, function(){ $(this).parent().remove(); that.hideOverlay(); }); }, // Preformated build and management modal openModalInsert: function(title, fields, cmd){ var html = '', pfx = this.o.prefix; for(f in fields){ var fd = fields[f]; label = (fd.label == undefined) ? (this.lang[f] ? this.lang[f] : f.charAt(0).toUpperCase() + f.slice(1)) : (this.lang[fd.label] ? this.lang[fd.label] : fd.label) ; if(fd.name == undefined) fd.name = f; if(!fd.pattern && f == 'url'){ fd.pattern = /^(http|https):\/\/([\w~#!:.?+=&%@!\-\/]+)$/; fd.patternError = this.lang.invalidUrl; } html += ''; } var modBox = this.openModal(title, html), that = this; modBox .on(pfx + 'confirm', function(){ var $form = $(this).find('form'), valid = true, values = {}; for(f in fields) { var $field = $('input[name="'+f+'"]', $form); values[f] = $field.val(); // Validate value if(fields[f].required && (values[f] == null || values[f] == undefined || $.trim(values[f]) == "")) { valid = false; that.addErrorOnModalField($field, that.lang.required); } else if(fields[f].pattern && !fields[f].pattern.test(values[f])) { valid = false; that.addErrorOnModalField($field, fields[f].patternError); } } if(valid) { that.restoreSelection(); if(cmd(values, fields)) { that.syncCode(); that.closeModal(); modBox.off(pfx + 'confirm'); } } }) .one(pfx + 'cancel', function(){ modBox.off(pfx + 'confirm'); that.closeModal(); that.restoreSelection(); }); return modBox; }, addErrorOnModalField: function($field, err){ var $label = $field.parent(), pfx = this.o.prefix; $label.addClass(pfx + 'input-error'); $field.on('change keyup', function(){ $label.removeClass(pfx + 'input-error'); }); $label.find('input+span').append(''+ err +''); }, // Selection management saveSelection: function(){ this.selection = null; if(window.getSelection){ var sel = window.getSelection(); if(sel.getRangeAt && sel.rangeCount) this.selection = sel.getRangeAt(0); } else if(document.selection && document.selection.createRange){ this.selection = document.selection.createRange(); } }, restoreSelection: function(){ range = this.selection; if(range){ if(window.getSelection){ var sel = window.getSelection(); sel.removeAllRanges(); sel.addRange(range); } else if(document.selection && range.select){ range.select(); } } }, // Return true if must enable Trumbowyg on this mobile device isEnabled: function(){ var mobile = "iPhone|iPod|Android|BlackBerry|Windows\sPhone|ZuneWP7"; var exprTablet = new RegExp("(iPad|webOS)"); var exprMobile = new RegExp("("+mobile+")"); return (this.o.tablet === true && exprTablet.test(navigator.userAgent)) || (this.o.mobile === true && exprMobile.test(navigator.userAgent)); } }; /* isObject */ var toString = Object.prototype.toString, hasOwnProp = Object.prototype.hasOwnProperty; $.isObject = function(obj) { if(toString.call(obj) !== "[object Object]") return false; var key; for(key in obj){} return !key || hasOwnProp.call(obj, key); }; $.isString = function(str){ return typeof(str) === 'string' }; })(jQuery); /* =========================================================== * trumbowyg.upload.js v1.0 * Upload plugin for Trumbowyg * http://alex-d.github.com/Trumbowyg * =========================================================== * Author : Alexandre Demode (Alex-D) * Twitter : @AlexandreDemode * Website : alex-d.fr */ (function($){ addXhrProgressEvent(); $.extend(true, $.trumbowyg, { langs: { en: { upload: "Envoi d'un fichier", file: "Fichier", uploadError: "Erreur" } }, upload: { serverPath: './trumbowyg.upload.php' }, opts: { btnsDef: { upload: { func: function(params, tbw){ var file, pfx = tbw.o.prefix; var $modal = tbw.openModalInsert( // Title tbw.lang['upload'], // Fields { file: { type: 'file', required: true } }, // Callback function(values, fields){ var data = new FormData(); data.append('file', file); if($('.' + pfx +'progress', $modal).length == 0) $('.' + pfx + 'modal-title', $modal) .after( $('', { 'class': pfx +'progress' }) .append( $('', { 'class': pfx +'progress-bar' }) ) ); $.ajax({ url: $.trumbowyg.upload.serverPath, type: 'POST', data: data, cache: false, dataType: 'json', processData: false, contentType: false, progressUpload: function(e){ $('.' + pfx + 'progress-bar').stop().animate({ width: Math.round(e.loaded * 100 / e.total) + '%' }, 200); }, success: function(data){ if(data.error.length == 0) { if(data.file.ext == 'jpg' || data.file.ext == 'jpeg' || data.file.ext == 'gif' || data.file.ext == 'png' || data.file.ext == 'bmp'){ tbw.execCommand('insertImage', data.file.url); }else{ tbw.execCommand("InsertHTML", ''+data.file.name+''); } setTimeout(function(){ tbw.closeModal(); }, 250); } else { alert(data.error.join('\n-')); tbw.addErrorOnModalField( $('input[type=file]', $modal),'Erreur' ); } }, error: function(data){ tbw.addErrorOnModalField( $('input[type=file]', $modal), tbw.lang['uploadError'] ); } }); } ); $('input[type=file]').on('change', function(e){ file = e.target.files[0]; }); } } } } }); function addXhrProgressEvent(){ var originalXhr = $.ajaxSettings.xhr; $.ajaxSetup({ xhr: function() { var req = originalXhr(), that = this; if(req && typeof req.upload == "object" && that.progressUpload !== undefined) req.upload.addEventListener("progress", function(e){ that.progressUpload(e); }, false); return req; } }); } })(jQuery);