/*! * Sir Trevor JS v0.4.0 * * Released under the MIT license * www.opensource.org/licenses/MIT * * 2014-05-15 */ (function ($, _){ var root = this, SirTrevor; SirTrevor = root.SirTrevor = {}; SirTrevor.DEBUG = false; SirTrevor.SKIP_VALIDATION = false; SirTrevor.version = "0.3.0"; SirTrevor.LANGUAGE = "en"; function $element(el) { return el instanceof $ ? el : $(el); } /* Define default attributes that can be extended through an object passed to the initialize function of SirTrevor */ SirTrevor.DEFAULTS = { defaultType: false, spinner: { className: 'st-spinner', lines: 9, length: 8, width: 3, radius: 6, color: '#000', speed: 1.4, trail: 57, shadow: false, left: '50%', top: '50%' }, blockLimit: 0, blockTypeLimits: {}, required: [], uploadUrl: '/attachments', baseImageUrl: '/sir-trevor-uploads/', errorsContainer: undefined }; SirTrevor.BlockMixins = {}; SirTrevor.Blocks = {}; SirTrevor.Formatters = {}; SirTrevor.instances = []; SirTrevor.Events = Eventable; var formBound = false; // Flag to tell us once we've bound our submit event /* Generic function binding utility, used by lots of our classes */ var FunctionBind = { bound: [], _bindFunctions: function(){ if (this.bound.length > 0) { _.bindAll.apply(null, _.union([this], this.bound)); } } }; var Renderable = { tagName: 'div', className: 'sir-trevor__view', attributes: {}, $: function(selector) { return this.$el.find(selector); }, render: function() { return this; }, destroy: function() { if (!_.isUndefined(this.stopListening)) { this.stopListening(); } this.$el.remove(); }, _ensureElement: function() { if (!this.el) { var attrs = _.extend({}, _.result(this, 'attributes')), html; if (this.id) { attrs.id = this.id; } if (this.className) { attrs['class'] = this.className; } if (attrs.html) { html = attrs.html; delete attrs.html; } var $el = $('<' + this.tagName + '>').attr(attrs); if (html) { $el.html(html); } this._setElement($el); } else { this._setElement(this.el); } }, _setElement: function(element) { this.$el = $element(element); this.el = this.$el[0]; return this; } }; function diffText(before, after) { var pos1 = -1, pos2 = -1, after_len = after.length, before_len = before.length; for (var i = 0; i < after_len; i++) { if (pos1 == -1 && before.substr(i, 1) != after.substr(i, 1)) { pos1 = i - 1; } if (pos2 == -1 && before.substr(before_len - i - 1, 1) != after.substr(after_len - i - 1, 1) ) { pos2 = i; } } return { result: after.substr(pos1, after_len - pos2 - pos1 + 1), pos1: pos1, pos2: pos2 }; } /* Drop Area Plugin from @maccman http://blog.alexmaccaw.com/svbtle-image-uploading -- Tweaked so we use the parent class of dropzone */ (function($){ function dragEnter(e) { e.preventDefault(); } function dragOver(e) { e.originalEvent.dataTransfer.dropEffect = "copy"; $(e.currentTarget).addClass('st-drag-over'); e.preventDefault(); } function dragLeave(e) { $(e.currentTarget).removeClass('st-drag-over'); e.preventDefault(); } $.fn.dropArea = function(){ this.bind("dragenter", dragEnter). bind("dragover", dragOver). bind("dragleave", dragLeave); return this; }; $.fn.noDropArea = function(){ this.unbind("dragenter"). unbind("dragover"). unbind("dragleave"); return this; }; $.fn.caretToEnd = function(){ var range,selection; range = document.createRange(); range.selectNodeContents(this[0]); range.collapse(false); selection = window.getSelection(); selection.removeAllRanges(); selection.addRange(range); return this; }; })(jQuery); /* Backbone Inheritence -- From: https://github.com/documentcloud/backbone/blob/master/backbone.js Backbone.js 0.9.2 (c) 2010-2012 Jeremy Ashkenas, DocumentCloud Inc. */ var extend = function(protoProps, staticProps) { var parent = this; var child; // The constructor function for the new subclass is either defined by you // (the "constructor" property in your `extend` definition), or defaulted // by us to simply call the parent's constructor. if (protoProps && _.has(protoProps, 'constructor')) { child = protoProps.constructor; } else { child = function(){ return parent.apply(this, arguments); }; } // Add static properties to the constructor function, if supplied. _.extend(child, parent, staticProps); // Set the prototype chain to inherit from `parent`, without calling // `parent`'s constructor function. var Surrogate = function(){ this.constructor = child; }; Surrogate.prototype = parent.prototype; child.prototype = new Surrogate; // Add prototype properties (instance properties) to the subclass, // if supplied. if (protoProps) _.extend(child.prototype, protoProps); // Set a convenience property in case the parent's prototype is needed // later. child.__super__ = parent.prototype; return child; }; /* * Ultra simple logging */ SirTrevor.log = function(message) { if (!_.isUndefined(console) && SirTrevor.DEBUG) { console.log(message); } }; SirTrevor.Locales = { en: { general: { 'delete': 'Delete?', 'drop': 'Drag __block__ here', 'paste': 'Or paste URL here', 'upload': '...or choose a file', 'close': 'close', 'position': 'Position', 'wait': 'Please wait...', 'link': 'Enter a link' }, errors: { 'title': "You have the following errors:", 'validation_fail': "__type__ block is invalid", 'block_empty': "__name__ must not be empty", 'type_missing': "You must have a block of type __type__", 'required_type_empty': "A required block type __type__ is empty", 'load_fail': "There was a problem loading the contents of the document" }, blocks: { text: { 'title': "Text" }, columns: { 'title': "Columns" }, list: { 'title': "List" }, quote: { 'title': "Quote", 'credit_field': "Credit" }, image: { 'title': "Image", 'upload_error': "There was a problem with your upload" }, video: { 'title': "Video" }, tweet: { 'title': "Tweet", 'fetch_error': "There was a problem fetching your tweet" }, embedly: { 'title': "Embedly", 'fetch_error': "There was a problem fetching your embed", 'key_missing': "An Embedly API key must be present" }, heading: { 'title': "Heading" } } } }; if (window.i18n === undefined || window.i18n.init === undefined) { // Minimal i18n stub that only reads the English strings SirTrevor.log("Using i18n stub"); window.i18n = { t: function(key, options) { var parts = key.split(':'), str, obj, part, i; obj = SirTrevor.Locales[SirTrevor.LANGUAGE]; for(i = 0; i < parts.length; i++) { part = parts[i]; if(!_.isUndefined(obj[part])) { obj = obj[part]; } } str = obj; if (!_.isString(str)) { return ""; } if (str.indexOf('__') >= 0) { _.each(options, function(value, opt) { str = str.replace('__' + opt + '__', value); }); } return str; } }; } else { SirTrevor.log("Using i18next"); // Only use i18next when the library has been loaded by the user, keeps // dependencies slim i18n.init({ resStore: SirTrevor.Locales, fallbackLng: SirTrevor.LANGUAGE, ns: { namespaces: ['general', 'blocks'], defaultNs: 'general' } }); } //fgnass.github.com/spin.js#v1.2.5 (function(a,b,c){function g(a,c){var d=b.createElement(a||"div"),e;for(e in c)d[e]=c[e];return d}function h(a){for(var b=1,c=arguments.length;b>1):c.left+e)+"px",top:(c.top=="auto"?i.y-h.y+(a.offsetHeight>>1):c.top+e)+"px"})),d.setAttribute("aria-role","progressbar"),b.lines(d,b.opts);if(!f){var j=0,k=c.fps,m=k/c.speed,o=(1-c.opacity)/(m*c.trail/100),p=m/c.lines;!function q(){j++;for(var a=c.lines;a;a--){var e=Math.max(1-(j+a*p)%m*o,c.opacity);b.opacity(d,c.lines-a,e,c)}b.timeout=b.el&&setTimeout(q,~~(1e3/k))}()}return b},stop:function(){var a=this.el;return a&&(clearTimeout(this.timeout),a.parentNode&&a.parentNode.removeChild(a),this.el=c),this},lines:function(a,b){function e(a,d){return l(g(),{position:"absolute",width:b.length+b.width+"px",height:b.width+"px",background:a,boxShadow:d,transformOrigin:"left",transform:"rotate("+~~(360/b.lines*c+b.rotate)+"deg) translate("+b.radius+"px"+",0)",borderRadius:(b.width>>1)+"px"})}var c=0,d;for(;c',b)}var b=l(g("group"),{behavior:"url(#default#VML)"});!k(b,"transform")&&b.adj?(i.addRule(".spin-vml","behavior:url(#default#VML)"),p.prototype.lines=function(b,c){function f(){return l(a("group",{coordsize:e+" "+e,coordorigin:-d+" "+ -d}),{width:e,height:e})}function k(b,e,g){h(i,h(l(f(),{rotation:360/c.lines*b+"deg",left:~~e}),h(l(a("roundrect",{arcsize:1}),{width:d,height:c.width,left:c.radius,top:-c.width>>1,filter:g}),a("fill",{color:c.color,opacity:c.opacity}),a("stroke",{opacity:0}))))}var d=c.length+c.width,e=2*d,g=-(c.width+c.length)*2+"px",i=l(f(),{position:"absolute",top:g,left:g}),j;if(c.shadow)for(j=1;j<=c.lines;j++)k(j,-2,"progid:DXImageTransform.Microsoft.Blur(pixelradius=2,makeshadow=1,shadowopacity=.3)");for(j=1;j<=c.lines;j++)k(j);return h(b,i)},p.prototype.opacity=function(a,b,c,d){var e=a.firstChild;d=d.shadow&&d.lines||0,e&&b+d 0) { try { // Ensure the JSON string has a data element that's an array var str = JSON.parse(content); if (!_.isUndefined(str.data)) { // Set it editor.dataStore = str; } } catch(e) { editor.errors.push({ text: i18n.t("errors:load_fail") }); editor.renderErrors(); SirTrevor.log('Sorry there has been a problem with parsing the JSON'); SirTrevor.log(e); } } break; case "reset": editor.dataStore = { data: [] }; break; case "add": if (options.data) { editor.dataStore.data.push(options.data); resp = editor.dataStore; } break; case "save": // Store to our element editor.$el.val((editor.dataStore.data.length > 0) ? JSON.stringify(editor.dataStore) : ''); break; case "read": resp = editor.dataStore; break; } if(resp) { return resp; } }; /* SirTrevor.Submittable -- We need a global way of setting if the editor can and can't be submitted, and a way to disable the submit button and add messages (when appropriate) We also need this to be highly extensible so it can be overridden. This will be triggered *by anything* so it needs to subscribe to events. */ SirTrevor.Submittable = function($form){ this.$form = $form; this.intialize(); }; _.extend(SirTrevor.Submittable.prototype, { intialize: function(){ this.$submitBtn = this.$form.find("input[type='submit']"); var btnTitles = []; _.each(this.$submitBtn, function(btn){ btnTitles.push($(btn).attr('value')); }); this.submitBtnTitles = btnTitles; this.canSubmit = true; this.globalUploadCount = 0; this._bindEvents(); }, setSubmitButton: function(e, message) { this.$submitBtn.attr('value', message); }, resetSubmitButton: function(){ _.each(this.$submitBtn, function(item, index){ $(item).attr('value', this.submitBtnTitles[index]); }, this); }, onUploadStart: function(e){ this.globalUploadCount++; SirTrevor.log('onUploadStart called ' + this.globalUploadCount); if(this.globalUploadCount === 1) { this._disableSubmitButton(); } }, onUploadStop: function(e) { this.globalUploadCount = (this.globalUploadCount <= 0) ? 0 : this.globalUploadCount - 1; SirTrevor.log('onUploadStop called ' + this.globalUploadCount); if(this.globalUploadCount === 0) { this._enableSubmitButton(); } }, onError: function(e){ SirTrevor.log('onError called'); this.canSubmit = false; }, _disableSubmitButton: function(message){ this.setSubmitButton(null, message || i18n.t("general:wait")); this.$submitBtn .attr('disabled', 'disabled') .addClass('disabled'); }, _enableSubmitButton: function(){ this.resetSubmitButton(); this.$submitBtn .removeAttr('disabled') .removeClass('disabled'); }, _events : { "disableSubmitButton" : "_disableSubmitButton", "enableSubmitButton" : "_enableSubmitButton", "setSubmitButton" : "setSubmitButton", "resetSubmitButton" : "resetSubmitButton", "onError" : "onError", "onUploadStart" : "onUploadStart", "onUploadStop" : "onUploadStop" }, _bindEvents: function(){ _.forEach(this._events, function(callback, type) { SirTrevor.EventBus.on(type, this[callback], this); }, this); } }); /* * Sir Trevor Uploader * Generic Upload implementation that can be extended for blocks */ SirTrevor.fileUploader = function(block, file, success, error) { var uid = [block.blockID, (new Date()).getTime(), 'raw'].join('-'); var data = new FormData(); data.append('attachment[name]', file.name); data.append('attachment[file]', file); data.append('attachment[uid]', uid); block.resetMessages(); var callbackSuccess = function(data){ SirTrevor.log('Upload callback called'); if (!_.isUndefined(success) && _.isFunction(success)) { _.bind(success, block)(data); } }; var callbackError = function(jqXHR, status, errorThrown){ SirTrevor.log('Upload callback error called'); if (!_.isUndefined(error) && _.isFunction(error)) { _.bind(error, block)(status); } }; var xhr = $.ajax({ url: SirTrevor.DEFAULTS.uploadUrl, data: data, cache: false, contentType: false, processData: false, dataType: 'json', type: 'POST' }); block.addQueuedItem(uid, xhr); xhr.done(callbackSuccess) .fail(callbackError) .always(_.bind(block.removeQueuedItem, block, uid)); return xhr; }; /* Underscore helpers */ var url_regex = /^(?:([A-Za-z]+):)?(\/{0,3})([0-9.\-A-Za-z]+)(?::(\d+))?(?:\/([^?#]*))?(?:\?([^#]*))?(?:#(.*))?$/; _.mixin({ isURI : function(string) { return (url_regex.test(string)); }, titleize: function(str){ if (str === null) return ''; str = String(str).toLowerCase(); return str.replace(/(?:^|\s|-)\S/g, function(c){ return c.toUpperCase(); }); }, classify: function(str){ return _.titleize(String(str).replace(/[\W_]/g, ' ')).replace(/\s/g, ''); }, classifyList: function(a){ return _.map(a, function(i){ return _.classify(i); }); }, capitalize : function(string) { return string.charAt(0).toUpperCase() + string.substring(1).toLowerCase(); }, underscored: function(str){ return _.trim(str).replace(/([a-z\d])([A-Z]+)/g, '$1_$2') .replace(/[-\s]+/g, '_').toLowerCase(); }, trim : function(string) { return string.replace(/^\s\s*/, '').replace(/\s\s*$/, ''); }, reverse: function(str) { return str.split("").reverse().join(""); }, flattern: function(obj) { var x = {}; _.each(obj, function(a,b) { x[(_.isArray(obj)) ? a : b] = true; }); return x; }, to_slug: function(str) { return str .toLowerCase() .replace(/[^\w ]+/g,'') .replace(/ +/g,'-'); } }); SirTrevor.toHTML = function(markdown, type) { // MD -> HTML type = _.classify(type); var html = markdown, shouldWrap = type === "Text"; if(_.isUndefined(shouldWrap)) { shouldWrap = false; } if (shouldWrap) { html = "
" + html; } html = html.replace(/\[([^\]]+)\]\(([^\)]+)\)/gm,function(match, p1, p2){ return ""+p1.replace(/\r?\n/g, '')+""; }); // This may seem crazy, but because JS doesn't have a look behind, // we reverse the string to regex out the italic items (and bold) // and look for something that doesn't start (or end in the reversed strings case) // with a slash. html = _.reverse( _.reverse(html) .replace(/_(?!\\)((_\\|[^_])*)_(?=$|[^\\])/gm, function(match, p1) { return ">i/<"+ p1.replace(/\r?\n/g, '').replace(/[\s]+$/,'') +">i<"; }) .replace(/\*\*(?!\\)((\*\*\\|[^\*\*])*)\*\*(?=$|[^\\])/gm, function(match, p1){ return ">b/<"+ p1.replace(/\r?\n/g, '').replace(/[\s]+$/,'') +">b<"; }) ); html = html.replace(/^\> (.+)$/mg,"$1"); // Use custom formatters toHTML functions (if any exist) var formatName, format; for(formatName in SirTrevor.Formatters) { if (SirTrevor.Formatters.hasOwnProperty(formatName)) { format = SirTrevor.Formatters[formatName]; // Do we have a toHTML function? if (!_.isUndefined(format.toHTML) && _.isFunction(format.toHTML)) { html = format.toHTML(html); } } } // Use custom block toHTML functions (if any exist) var block; if (SirTrevor.Blocks.hasOwnProperty(type)) { block = SirTrevor.Blocks[type]; // Do we have a toHTML function? if (!_.isUndefined(block.prototype.toHTML) && _.isFunction(block.prototype.toHTML)) { html = block.prototype.toHTML(html); } } if (shouldWrap) { html = html.replace(/\r?\n\r?\n/gm, "

"); html = html.replace(/\r?\n/gm, "
"); } html = html.replace(/\t/g, "    ") .replace(/\r?\n/g, "
") .replace(/\*\*/, "") .replace(/__/, ""); // Cleanup any markdown characters left // Replace escaped html = html.replace(/\\\*/g, "*") .replace(/\\\[/g, "[") .replace(/\\\]/g, "]") .replace(/\\\_/g, "_") .replace(/\\\(/g, "(") .replace(/\\\)/g, ")") .replace(/\\\-/g, "-"); if (shouldWrap) { html += "
"; } return html; }; SirTrevor.toMarkdown = function(content, type) { type = _.classify(type); var markdown = content; //Normalise whitespace markdown = markdown.replace(/ /g," "); // First of all, strip any additional formatting // MSWord, I'm looking at you, punk. markdown = markdown.replace(/( class=(")?Mso[a-zA-Z]+(")?)/g, '') .replace(//g, '') .replace(/\/\*(.*?)\*\//g, '') .replace(/<(\/)*(meta|link|span|\\?xml:|st1:|o:|font)(.*?)>/gi, ''); var badTags = ['style', 'script', 'applet', 'embed', 'noframes', 'noscript'], tagStripper, i; for (i = 0; i< badTags.length; i++) { tagStripper = new RegExp('<'+badTags[i]+'.*?'+badTags[i]+'(.*?)>', 'gi'); markdown = markdown.replace(tagStripper, ''); } // Escape anything in here that *could* be considered as MD // Markdown chars we care about: * [] _ () - markdown = markdown.replace(/\*/g, "\\*") .replace(/\[/g, "\\[") .replace(/\]/g, "\\]") .replace(/\_/g, "\\_") .replace(/\(/g, "\\(") .replace(/\)/g, "\\)") .replace(/\-/g, "\\-"); var inlineTags = ["em", "i", "strong", "b"]; for (i = 0; i< inlineTags.length; i++) { tagStripper = new RegExp('<'+inlineTags[i]+'>
', 'gi'); markdown = markdown.replace(tagStripper, '
'); } function replaceBolds(match, p1, p2){ if(_.isUndefined(p2)) { p2 = ''; } return "**" + p1.replace(/<(.)?br(.)?>/g, '') + "**" + p2; } function replaceItalics(match, p1, p2){ if(_.isUndefined(p2)) { p2 = ''; } return "_" + p1.replace(/<(.)?br(.)?>/g, '') + "_" + p2; } markdown = markdown.replace(/<(\w+)(?:\s+\w+="[^"]+(?:"\$[^"]+"[^"]+)?")*>\s*<\/\1>/gim, '') //Empty elements .replace(/\n/mg,"") .replace(/(.*?)<\/a>/gim, function(match, p1, p2){ return "[" + p2.trim().replace(/<(.)?br(.)?>/g, '') + "]("+ p1 +")"; }) // Hyperlinks .replace(/(?:\s*)(.*?)(\s)*?<\/strong>/gim, replaceBolds) .replace(/(?:\s*)(.*?)(\s*)?<\/b>/gim, replaceBolds) .replace(/(?:\s*)(.*?)(\s*)?<\/em>/gim, replaceItalics) .replace(/(?:\s*)(.*?)(\s*)?<\/i>/gim, replaceItalics); // Use custom formatters toMarkdown functions (if any exist) var formatName, format; for(formatName in SirTrevor.Formatters) { if (SirTrevor.Formatters.hasOwnProperty(formatName)) { format = SirTrevor.Formatters[formatName]; // Do we have a toMarkdown function? if (!_.isUndefined(format.toMarkdown) && _.isFunction(format.toMarkdown)) { markdown = format.toMarkdown(markdown); } } } // Do our generic stripping out markdown = markdown.replace(/([^<>]+)(
)/g,"$1\n$2") // Divitis style line breaks (handle the first line) .replace(/
/g,'\n
') // ^ (double opening divs with one close from Chrome) .replace(/(?:
)([^<>]+)(?:
)/g,"$1\n") // ^ (handle nested divs that start with content) .replace(/(?:
)(?:
)?([^<>]+)(?:
)?(?:<\/div>)/g,"$1\n") // ^ (handle content inside divs) .replace(/<\/p>/g,"\n\n") // P tags as line breaks .replace(/<(.)?br(.)?>/g,"\n") // Convert normal line breaks .replace(/</g,"<").replace(/>/g,">"); // Encoding // Use custom block toMarkdown functions (if any exist) var block; if (SirTrevor.Blocks.hasOwnProperty(type)) { block = SirTrevor.Blocks[type]; // Do we have a toMarkdown function? if (!_.isUndefined(block.prototype.toMarkdown) && _.isFunction(block.prototype.toMarkdown)) { markdown = block.prototype.toMarkdown(markdown); } } // Strip remaining HTML markdown = markdown.replace(/<\/?[^>]+(>|$)/g, ""); return markdown; }; SirTrevor.EventBus = _.extend({}, SirTrevor.Events); /* Block Mixins */ SirTrevor.BlockMixins.Ajaxable = { mixinName: "Ajaxable", ajaxable: true, initializeAjaxable: function(){ this._queued = []; }, addQueuedItem: function(name, deffered) { SirTrevor.log("Adding queued item for " + this.blockID + " called " + name); SirTrevor.EventBus.trigger("onUploadStart"); this._queued.push({ name: name, deffered: deffered }); }, removeQueuedItem: function(name) { SirTrevor.log("Removing queued item for " + this.blockID + " called " + name); SirTrevor.EventBus.trigger("onUploadStop"); this._queued = _.reject(this._queued, function(queued){ return queued.name == name; }); }, hasItemsInQueue: function() { return this._queued.length > 0; }, resolveAllInQueue: function() { _.each(this._queued, function(item){ SirTrevor.log("Aborting queued request: " + item.name); item.deffered.abort(); }, this); } }; SirTrevor.BlockMixins.Controllable = { mixinName: "Controllable", initializeControllable: function() { SirTrevor.log("Adding controllable to block " + this.blockID); this.$control_ui = $('
', {'class': 'st-block__control-ui'}); _.each( this.controls, function(handler, cmd) { // Bind configured handler to current block context this.addUiControl(cmd, _.bind(handler, this)); }, this ); this.$inner.append(this.$control_ui); }, getControlTemplate: function(cmd) { return $("", { 'data-icon': cmd, 'class': 'st-icon st-block-control-ui-btn st-block-control-ui-btn--' + cmd }); }, addUiControl: function(cmd, handler) { this.$control_ui.append(this.getControlTemplate(cmd)); this.$control_ui.on('click', '.st-block-control-ui-btn--' + cmd, handler); } }; /* Adds drop functionaltiy to this block */ SirTrevor.BlockMixins.Droppable = { mixinName: "Droppable", valid_drop_file_types: ['File', 'Files', 'text/plain', 'text/uri-list'], initializeDroppable: function() { SirTrevor.log("Adding droppable to block " + this.blockID); this.drop_options = _.extend({}, SirTrevor.DEFAULTS.Block.drop_options, this.drop_options); var drop_html = $(_.template(this.drop_options.html, { block: this })); this.$editor.hide(); this.$inputs.append(drop_html); this.$dropzone = drop_html; // Bind our drop event this.$dropzone.dropArea() .bind('drop', _.bind(this._handleDrop, this)); this.$inner.addClass('st-block__inner--droppable'); }, _handleDrop: function(e) { e.preventDefault(); e = e.originalEvent; var el = $(e.target), types = e.dataTransfer.types, type, data = []; el.removeClass('st-dropzone--dragover'); /* Check the type we just received, delegate it away to our blockTypes to process */ if (!_.isUndefined(types) && _.some(types, function(type){ return _.include(this.valid_drop_file_types, type); }, this)) { this.onDrop(e.dataTransfer); } SirTrevor.EventBus.trigger('block:content:dropped'); } }; SirTrevor.BlockMixins.Fetchable = { mixinName: "Fetchable", initializeFetchable: function(){ this.withMixin(SirTrevor.BlockMixins.Ajaxable); }, fetch: function(options, success, failure){ var uid = _.uniqueId(this.blockID + "_fetch"), xhr = $.ajax(options); this.resetMessages(); this.addQueuedItem(uid, xhr); if(!_.isUndefined(success)) { xhr.done(_.bind(success, this)); } if(!_.isUndefined(failure)) { xhr.fail(_.bind(failure, this)); } xhr.always(_.bind(this.removeQueuedItem, this, uid)); return xhr; } }; SirTrevor.BlockMixins.Pastable = { mixinName: "Pastable", initializePastable: function() { SirTrevor.log("Adding pastable to block " + this.blockID); this.paste_options = _.extend({}, SirTrevor.DEFAULTS.Block.paste_options, this.paste_options); this.$inputs.append(_.template(this.paste_options.html, this)); this.$('.st-paste-block') .bind('click', function(){ $(this).select(); }) .bind('paste', this._handleContentPaste) .bind('submit', this._handleContentPaste); } }; SirTrevor.BlockMixins.Uploadable = { mixinName: "Uploadable", uploadsCount: 0, initializeUploadable: function() { SirTrevor.log("Adding uploadable to block " + this.blockID); this.withMixin(SirTrevor.BlockMixins.Ajaxable); this.upload_options = _.extend({}, SirTrevor.DEFAULTS.Block.upload_options, this.upload_options); this.$inputs.append(_.template(this.upload_options.html, this)); }, uploader: function(file, success, failure){ return SirTrevor.fileUploader(this, file, success, failure); } }; SirTrevor.BlockPositioner = (function(){ var template = [ "
", "", "", "
" ].join("\n"); var BlockPositioner = function(block_element, instance_id) { this.$block = block_element; this.instanceID = instance_id; this.total_blocks = 0; this._ensureElement(); this._bindFunctions(); this.initialize(); }; _.extend(BlockPositioner.prototype, FunctionBind, Renderable, { bound: ['onBlockCountChange', 'onSelectChange', 'toggle', 'show', 'hide'], className: 'st-block-positioner', visibleClass: 'st-block-positioner--is-visible', initialize: function(){ this.$el.append(template); this.$select = this.$('.st-block-positioner__select'); this.$select.on('change', this.onSelectChange); SirTrevor.EventBus.on(this.instanceID + ":blocks:count_update", this.onBlockCountChange); }, onBlockCountChange: function(new_count) { // count only siblings blocks new_count = this.$block.siblings('.st-block').length + 1; if (new_count != this.total_blocks) { this.total_blocks = new_count; this.renderPositionList(); } }, onSelectChange: function() { var val = this.$select.val(); if (val !== 0) { SirTrevor.EventBus.trigger(this.instanceID + ":blocks:change_position", this.$block, val, (val == 1 ? 'before' : 'after')); this.toggle(); } }, renderPositionList: function() { var inner = ""; for(var i = 1; i <= this.total_blocks; i++) { inner += ""; } this.$select.html(inner); }, toggle: function() { this.$select.val(0); this.$el.toggleClass(this.visibleClass); }, show: function(){ this.$el.addClass(this.visibleClass); }, hide: function(){ this.$el.removeClass(this.visibleClass); } }); return BlockPositioner; })(); SirTrevor.BlockReorder = (function(){ var BlockReorder = function(block_element) { this.$block = block_element; this._ensureElement(); this._bindFunctions(); this.initialize(); }; _.extend(BlockReorder.prototype, FunctionBind, Renderable, { bound: ['onMouseDown', 'onClick', 'onDragStart', 'onDragEnd', 'onDrag', 'onDrop'], className: 'st-block-ui-btn st-block-ui-btn--reorder st-icon', tagName: 'a', attributes: function() { return { 'html': 'reorder', 'draggable': 'true', 'data-icon': 'move' }; }, initialize: function() { this.$el.bind('mousedown touchstart', this.onMouseDown) .bind('click', this.onClick) .bind('dragstart', this.onDragStart) .bind('dragend touchend', this.onDragEnd) .bind('drag touchmove', this.onDrag); this.$block.dropArea() .bind('drop', this.onDrop); }, onMouseDown: function() { SirTrevor.EventBus.trigger("block:reorder:down"); }, onDrop: function(ev) { ev.preventDefault(); ev.stopPropagation(); // to prevent event handling on outer blocks var dropped_on = this.$block, item_id = ev.originalEvent.dataTransfer.getData("text/plain"); if (!_.isUndefined(item_id) && item_id.substr(0,9) == 'st-block-') { var block = $('#' + item_id); if (!_.isEmpty(block) && dropped_on.attr('id') != item_id && dropped_on.attr('data-instance') == block.attr('data-instance') && !$.contains(block[0], dropped_on[0]) // additional check for nested blocks ) { dropped_on.after(block); } SirTrevor.EventBus.trigger("block:reorder:dropped", item_id); } }, onDragStart: function(ev) { var btn = $(ev.currentTarget).parent(); ev.originalEvent.dataTransfer.setDragImage(this.$block[0], btn.position().left, btn.position().top); ev.originalEvent.dataTransfer.setData('Text', this.$block.attr('id')); SirTrevor.EventBus.trigger("block:reorder:dragstart"); this.$block.addClass('st-block--dragging'); }, onDragEnd: function(ev) { SirTrevor.EventBus.trigger("block:reorder:dragend"); this.$block.removeClass('st-block--dragging'); }, onDrag: function(ev){}, onClick: function() { }, render: function() { return this; } }); return BlockReorder; })(); SirTrevor.BlockDeletion = (function(){ var BlockDeletion = function() { this._ensureElement(); this._bindFunctions(); }; _.extend(BlockDeletion.prototype, FunctionBind, Renderable, { tagName: 'a', className: 'st-block-ui-btn st-block-ui-btn--delete st-icon', attributes: { html: 'delete', 'data-icon': 'bin' } }); return BlockDeletion; })(); var bestNameFromField = function(field) { var msg = field.attr("data-st-name") || field.attr("name"); if (!msg) { msg = 'Field'; } return _.capitalize(msg); }; SirTrevor.BlockValidations = { errors: [], valid: function(){ this.performValidations(); return this.errors.length === 0; }, // This method actually does the leg work // of running our validators and custom validators performValidations: function() { this.resetErrors(); var required_fields = this.$('.st-required'); _.each(required_fields, this.validateField, this); _.each(this.validations, this.runValidator, this); this.$el.toggleClass('st-block--with-errors', this.errors.length > 0); }, // Everything in here should be a function that returns true or false validations: [], validateField: function(field) { field = $(field); var content = field.attr('contenteditable') ? field.text() : field.val(); if (content.length === 0) { this.setError(field, i18n.t("errors:block_empty", { name: bestNameFromField(field) })); } }, runValidator: function(validator) { if (!_.isUndefined(this[validator])) { this[validator].call(this); } }, setError: function(field, reason) { var $msg = this.addMessage(reason, "st-msg--error"); field.addClass('st-error'); this.errors.push({ field: field, reason: reason, msg: $msg }); }, resetErrors: function() { _.each(this.errors, function(error){ error.field.removeClass('st-error'); error.msg.remove(); }); this.$messages.removeClass("st-block__messages--is-visible"); this.errors = []; } }; SirTrevor.BlockStore = { blockStorage: {}, createStore: function(blockData) { this.blockStorage = { type: _.underscored(this.type), data: blockData || {} }; }, save: function() { this.toData(); }, saveAndReturnData: function() { this.save(); return this.blockStorage; }, saveAndGetData: function() { var store = this.saveAndReturnData(); return store.data || store; }, getData: function() { return this.blockStorage.data; }, setData: function(blockData) { SirTrevor.log("Setting data for block " + this.blockID); _.extend(this.blockStorage.data, blockData || {}); }, setAndRetrieveData: function(blockData) { this.setData(blockData); return this.getData(); }, setAndLoadData: function(blockData) { this.setData(blockData); this.beforeLoadingData(); }, toData: function() {}, loadData: function() {}, beforeLoadingData: function() { SirTrevor.log("loadData for " + this.blockID); SirTrevor.EventBus.trigger("editor/block/loadData"); this.loadData(this.getData()); }, _loadData: function() { SirTrevor.log("_loadData is deprecated and will be removed in the future. Please use beforeLoadingData instead."); this.beforeLoadingData(); }, checkAndLoadData: function() { if (!_.isEmpty(this.getData())) { this.beforeLoadingData(); } } }; SirTrevor.SimpleBlock = (function(){ var SimpleBlock = function(data, instance_id, sirTrevor) { this.createStore(data); this.blockID = _.uniqueId('st-block-'); this.instanceID = instance_id; this.sirTrevor = sirTrevor; this._ensureElement(); this._bindFunctions(); this.initialize.apply(this, arguments); }; _.extend(SimpleBlock.prototype, FunctionBind, SirTrevor.Events, Renderable, SirTrevor.BlockStore, { focus : function() {}, valid : function() { return true; }, className: 'st-block', block_template: _.template( "
<%= editor_html %>
" ), attributes: function() { return { 'id': this.blockID, 'data-type': this.type, 'data-instance': this.instanceID }; }, title: function() { return _.titleize(this.type.replace(/[\W_]/g, ' ')); }, blockCSSClass: function() { this.blockCSSClass = _.to_slug(this.type); return this.blockCSSClass; }, type: '', 'class': function() { return _.classify(this.type); }, editorHTML: '', initialize: function() {}, onBlockRender: function(){}, beforeBlockRender: function(){}, _setBlockInner : function() { var editor_html = _.result(this, 'editorHTML'); this.$el.append( this.block_template({ editor_html: editor_html }) ); this.$inner = this.$el.find('.st-block__inner'); this.$inner.bind('click mouseover', function(e){ e.stopPropagation(); }); }, render: function() { this.beforeBlockRender(); this._setBlockInner(); this._blockPrepare(); return this; }, _blockPrepare : function() { this._initUI(); this._initMessages(); this.checkAndLoadData(); this.$el.addClass('st-item-ready'); this.on("onRender", this.onBlockRender); this.save(); }, _withUIComponent: function(component, className, callback) { this.$ui.append(component.render().$el); (className && callback) && this.$ui.on('click', className, callback); }, _initUI : function() { var ui_element = $("
", { 'class': 'st-block__ui' }); this.$inner.append(ui_element); this.$ui = ui_element; this._initUIComponents(); }, _initMessages: function() { var msgs_element = $("
", { 'class': 'st-block__messages' }); this.$inner.prepend(msgs_element); this.$messages = msgs_element; }, addMessage: function(msg, additionalClass) { var $msg = $("", { html: msg, 'class': "st-msg " + additionalClass }); this.$messages.append($msg) .addClass('st-block__messages--is-visible'); return $msg; }, resetMessages: function() { this.$messages.html('') .removeClass('st-block__messages--is-visible'); }, _initUIComponents: function() { this._withUIComponent(new SirTrevor.BlockReorder(this.$el)); } }); SimpleBlock.fn = SimpleBlock.prototype; SimpleBlock.extend = extend; // Allow our Block to be extended. return SimpleBlock; })(); SirTrevor.Block = (function(){ var Block = function(data, instance_id, sirTrevor) { SirTrevor.SimpleBlock.apply(this, arguments); }; var delete_template = [ "" ].join("\n"); var drop_options = { html: ['
', '<%= _.result(block, "icon_name") %>', '

<%= i18n.t("general:drop", { block: "" + _.result(block, "title") + "" }) %>', '

'].join('\n'), re_render_on_reorder: false }; var paste_options = { html: ['"', ' class="st-block__paste-input st-paste-block">'].join('') }; var upload_options = { html: [ '
', '', '', '
' ].join('\n') }; SirTrevor.DEFAULTS.Block = { drop_options: drop_options, paste_options: paste_options, upload_options: upload_options }; _.extend(Block.prototype, SirTrevor.SimpleBlock.fn, SirTrevor.BlockValidations, { bound: ["_handleContentPaste", "_onFocus", "_onBlur", "onDrop", "onDeleteClick", "clearInsertedStyles", "getSelectionForFormatter", "onBlockRender"], className: 'st-block st-icon--add', attributes: function() { return _.extend(SirTrevor.SimpleBlock.fn.attributes.call(this), { 'data-icon-after' : "add" }); }, icon_name: 'default', validationFailMsg: function() { return i18n.t('errors:validation_fail', { type: _.result(this,'title') }); }, editorHTML: '
', toolbarEnabled: true, droppable: false, pastable: false, uploadable: false, fetchable: false, ajaxable: false, drop_options: {}, paste_options: {}, upload_options: {}, formattable: true, _previousSelection: '', initialize: function() {}, toMarkdown: function(markdown){ return markdown; }, toHTML: function(html){ return html; }, withMixin: function(mixin) { if (!_.isObject(mixin)) { return; } var initializeMethod = "initialize" + mixin.mixinName; if (_.isUndefined(this[initializeMethod])) { _.extend(this, mixin); this[initializeMethod](); } }, render: function() { this.beforeBlockRender(); this._setBlockInner(); this.$editor = this.$inner.children().first(); if(this.droppable || this.pastable || this.uploadable) { var input_html = $("
", { 'class': 'st-block__inputs' }); this.$inner.append(input_html); this.$inputs = input_html; } if (this.hasTextBlock) { this._initTextBlocks(); } if (this.droppable) { this.withMixin(SirTrevor.BlockMixins.Droppable); } if (this.pastable) { this.withMixin(SirTrevor.BlockMixins.Pastable); } if (this.uploadable) { this.withMixin(SirTrevor.BlockMixins.Uploadable); } if (this.fetchable) { this.withMixin(SirTrevor.BlockMixins.Fetchable); } if (this.controllable) { this.withMixin(SirTrevor.BlockMixins.Controllable); } if (this.formattable) { this._initFormatting(); } this._blockPrepare(); return this; }, remove: function() { if (this.ajaxable) { this.resolveAllInQueue(); } this.$el.remove(); }, loading: function() { if(!_.isUndefined(this.spinner)) { this.ready(); } this.spinner = new Spinner(SirTrevor.DEFAULTS.spinner); this.spinner.spin(this.$el[0]); this.$el.addClass('st--is-loading'); }, ready: function() { this.$el.removeClass('st--is-loading'); if (!_.isUndefined(this.spinner)) { this.spinner.stop(); delete this.spinner; } }, /* Generic toData implementation. Can be overwritten, although hopefully this will cover most situations */ toData: function() { SirTrevor.log("toData for " + this.blockID); var bl = this.$el, dataObj = {}; /* Simple to start. Add conditions later */ if (this.hasTextBlock()) { var content = this.getTextBlock().html(); if (content.length > 0) { dataObj.text = SirTrevor.toMarkdown(content, this.type); } } // Add any inputs to the data attr if(this.$(':input').not('.st-paste-block').length > 0) { this.$(':input').each(function(index,input){ if (input.getAttribute('name')) { dataObj[input.getAttribute('name')] = input.value; } }); } // Set if(!_.isEmpty(dataObj)) { this.setData(dataObj); } }, /* Generic implementation to tell us when the block is active */ focus: function() { this.getTextBlock().focus(); }, blur: function() { this.getTextBlock().blur(); }, onFocus: function() { this.getTextBlock().bind('focus', this._onFocus); }, onBlur: function() { this.getTextBlock().bind('blur', this._onBlur); }, /* * Event handlers */ _onFocus: function() { this.trigger('blockFocus', this.$el); }, _onBlur: function() {}, onDrop: function(dataTransferObj) {}, onDeleteClick: function(ev) { ev.preventDefault(); var onDeleteConfirm = function(e) { e.preventDefault(); this.trigger('removeBlock', this.blockID); }; var onDeleteDeny = function(e) { e.preventDefault(); this.$el.removeClass('st-block--delete-active'); $delete_el.remove(); }; if (this.isEmpty()) { onDeleteConfirm.call(this, new Event('click')); return; } this.$inner.append(_.template(delete_template)); this.$el.addClass('st-block--delete-active'); var $delete_el = this.$inner.find('.st-block__ui-delete-controls'); this.$inner.on('click', '.st-block-ui-btn--confirm-delete', _.bind(onDeleteConfirm, this)) .on('click', '.st-block-ui-btn--deny-delete', _.bind(onDeleteDeny, this)); }, pastedMarkdownToHTML: function(content) { return SirTrevor.toHTML(SirTrevor.toMarkdown(content, this.type), this.type); }, onContentPasted: function(event, target){ target.html(this.pastedMarkdownToHTML(target[0].innerHTML)); this.getTextBlock().caretToEnd(); }, beforeLoadingData: function() { this.loading(); if(this.droppable || this.uploadable || this.pastable) { this.$editor.show(); this.$inputs.hide(); } SirTrevor.SimpleBlock.fn.beforeLoadingData.call(this); this.ready(); }, _handleContentPaste: function(ev) { var target = $(ev.currentTarget); _.delay(_.bind(this.onContentPasted, this, ev, target), 0); }, _getBlockClass: function() { return 'st-block--' + this.className; }, /* * Init functions for adding functionality */ _initUIComponents: function() { var positioner = new SirTrevor.BlockPositioner(this.$el, this.instanceID); this._withUIComponent( positioner, '.st-block-ui-btn--reorder', positioner.toggle ); this._withUIComponent( new SirTrevor.BlockReorder(this.$el) ); this._withUIComponent( new SirTrevor.BlockDeletion(), '.st-block-ui-btn--delete', this.onDeleteClick ); this.onFocus(); this.onBlur(); }, _initFormatting: function() { // Enable formatting keyboard input var formatter; for (var name in SirTrevor.Formatters) { if (SirTrevor.Formatters.hasOwnProperty(name)) { formatter = SirTrevor.Formatters[name]; if (!_.isUndefined(formatter.keyCode)) { formatter._bindToBlock(this.$el); } } } }, _initTextBlocks: function() { this.getTextBlock() .bind('paste', this._handleContentPaste) .bind('keyup', this.getSelectionForFormatter) .bind('mouseup', this.getSelectionForFormatter) .bind('DOMNodeInserted', this.clearInsertedStyles); }, getSelectionForFormatter: function() { _.defer(function(){ var selection = window.getSelection(), selectionStr = selection.toString().trim(); if (selectionStr === '') { SirTrevor.EventBus.trigger('formatter:hide'); } else { SirTrevor.EventBus.trigger('formatter:position'); } }); }, clearInsertedStyles: function(e) { var target = e.target; target.removeAttribute('style'); // Hacky fix for Chrome. }, hasTextBlock: function() { return this.getTextBlock().length > 0; }, getTextBlock: function() { if (_.isUndefined(this.text_block)) { this.text_block = this.$('.st-text-block'); } return this.text_block; }, isEmpty: function() { return _.isEmpty(this.saveAndGetData()); } }); Block.extend = extend; // Allow our Block to be extended. return Block; })(); SirTrevor.Formatter = (function(){ var Format = function(options){ this.formatId = _.uniqueId('format-'); this._configure(options || {}); this.initialize.apply(this, arguments); }; var formatOptions = ["title", "className", "cmd", "keyCode", "param", "onClick", "toMarkdown", "toHTML"]; _.extend(Format.prototype, { title: '', className: '', cmd: null, keyCode: null, param: null, toMarkdown: function(markdown){ return markdown; }, toHTML: function(html){ return html; }, initialize: function(){}, _configure: function(options) { if (this.options) options = _.extend({}, this.options, options); for (var i = 0, l = formatOptions.length; i < l; i++) { var attr = formatOptions[i]; if (options[attr]) this[attr] = options[attr]; } this.options = options; }, isActive: function() { return document.queryCommandState(this.cmd); }, _bindToBlock: function(block) { var formatter = this, ctrlDown = false; block .on('keyup','.st-text-block', function(ev) { if(ev.which == 17 || ev.which == 224 || ev.which == 91) { ctrlDown = false; } }) .on('keydown','.st-text-block', { formatter: formatter }, function(ev) { if(ev.which == 17 || ev.which == 224 || ev.which == 91) { ctrlDown = true; } if(ev.which == ev.data.formatter.keyCode && ctrlDown === true) { document.execCommand(ev.data.formatter.cmd, false, true); ev.preventDefault(); ctrlDown = false; } }); } }); Format.extend = extend; // Allow our Formatters to be extended. return Format; })(); /* Default Blocks */ /* Block Quote */ SirTrevor.Blocks.Quote = (function(){ var template = _.template([ '
', '', '"', ' class="st-input-string st-required js-cite-input" type="text" />' ].join("\n")); return SirTrevor.Block.extend({ type: "quote", title: function(){ return i18n.t('blocks:quote:title'); }, icon_name: 'quote', editorHTML: function() { return template(this); }, loadData: function(data){ this.getTextBlock().html(SirTrevor.toHTML(data.text, this.type)); this.$('.js-cite-input').val(data.cite); }, toMarkdown: function(markdown) { return markdown.replace(/^(.+)$/mg,"> $1"); } }); })(); SirTrevor.Blocks.Columns = (function() { return SirTrevor.Block.extend({ type: "Columns", title: function() { return i18n.t('blocks:columns:title'); }, icon_name: 'columns', columns_presets: { 'columns-6-6': [6,6], 'columns-3-9': [3,9], 'columns-9-3': [9,3], 'columns-4-8': [4,8], 'columns-8-4': [8,4], 'columns-4-4-4': [4,4,4], 'columns-3-6-3': [3,6,3], 'columns-3-3-3-3': [3,3,3,3] }, controllable: true, constructor: function(data, instance_id, sirTrevor) { SirTrevor.Block.apply(this, arguments); }, beforeBlockRender: function() { this.controls = { 'twocolumns': this.changeColumnsHandler('columns-6-6'), 'threecolumns': this.changeColumnsHandler('columns-4-4-4'), 'onetwocolumns': this.changeColumnsHandler('columns-4-8'), 'twoonecolumns': this.changeColumnsHandler('columns-8-4'), 'fourcolumns': this.changeColumnsHandler('columns-3-3-3-3'), 'onethreecolumns': this.changeColumnsHandler('columns-3-9'), 'threeonecolumns': this.changeColumnsHandler('columns-9-3'), 'onetwoonecolumns': this.changeColumnsHandler('columns-3-6-3') }; }, changeColumnsHandler: function(preset) { var self = this; return function() { self.changeColumns(preset, false) }; }, changeColumns: function(preset) { if (this.columns_preset != preset) { this.applyColumns(preset); } }, editorHTML: function() { return _.template( '
' , {blockID: this.blockID}) }, _setBlockInner: function() { SirTrevor.Block.prototype._setBlockInner.apply(this, arguments); this.applyColumns('columns-6-6', true); /* default */ }, applyColumns: function(preset, initial) { var self = this; var columns_config = this.columns_presets[preset]; var $to_delete = this.getColumns(':gt('+(columns_config.length-1)+')'); // if there are unneeded columns if ($to_delete.length > 0) { // ask confirmation only if there are nested blocks if ($to_delete.children('.st-block').length > 0) { var txt = $to_delete.length == 1 ? 'column' : ($to_delete.length + ' columns'); if (!confirm('This action will delete last ' + txt + '. Do you really want to proceed?')) { return; // interrupt if "Cancel" is pressed } } $to_delete.each(function() { var $this = $(this); // destroy nested blocks properly $this.children('.st-block').each(function() { self.sirTrevor.removeBlock(this.getAttribute('id')); }); // destroy column itself $this.trigger('destroy').remove(); }); } // apply new configuration var total_width = _.reduce(columns_config, function(total, width){ return total+width; }, 0); var $row = this.$('.columns-row'); _.each(columns_config, function(ratio, i) { var width = Math.round(ratio*1000*100/total_width)/1000; var $column = self.getColumn(i); if ($column.length == 0) { $column = $('
'); var plus = new SirTrevor.FloatingBlockControls($column, self.instanceID); self.listenTo(plus, 'showBlockControls', self.sirTrevor.showBlockControls); $column.prepend(plus.render().$el); $row.append($column); } $column.css('width', width+'%'); }); this.$('.st-block-control-ui-btn').removeClass('active') .filter('[data-icon='+preset+']').addClass('active'); this.columns_preset = preset; if (!initial) this.trigger('block:columns:change'); }, onBlockRender: function() { this.$('.st-block-control-ui-btn').filter('[data-icon='+this.columns_preset+']').addClass('active'); }, getRow: function() { if (this.$row) return this.$row; return this.$row = this.$('#'+this.blockID+'-columns-row'); }, getColumns: function(filter) { return this.getRow().children(filter); }, getColumn: function(index) { return this.getRow().children(':eq('+index+')'); }, toData: function() { var self = this; var column_config = this.columns_presets[this.columns_preset]; var dataObj = { columns: [], preset: this.columns_preset }; this.getColumns().each(function(i) { var blocksData = []; $(this).children('.st-block').each(function(){ var block = self.sirTrevor.findBlockById(this.getAttribute('id')); blocksData.push(block.saveAndReturnData()); }); dataObj.columns.push({ width: column_config[i], blocks: blocksData }); }); this.setData(dataObj); }, loadData: function(data) { if (data.preset) { this.applyColumns(data.preset, true); } var columns_data = (data.columns || []); for (var i=0; i
', icon_name: 'heading', loadData: function(data){ this.getTextBlock().html(SirTrevor.toHTML(data.text, this.type)); this.$('[name=heading]').val(data.heading); } }); /* Simple Image Block */ SirTrevor.Blocks.Image = SirTrevor.Block.extend({ type: "image", title: function() { return i18n.t('blocks:image:title'); }, droppable: true, uploadable: true, icon_name: 'image', loadData: function(data){ // Create our image tag this.$editor.html($('', { src: data.file.url })); }, onBlockRender: function(){ /* Setup the upload button */ this.$inputs.find('button').bind('click', function(ev){ ev.preventDefault(); }); this.$inputs.find('input').on('change', _.bind(function(ev){ this.onDrop(ev.currentTarget); }, this)); }, onDrop: function(transferData){ var file = transferData.files[0], urlAPI = (typeof URL !== "undefined") ? URL : (typeof webkitURL !== "undefined") ? webkitURL : null; // Handle one upload at a time if (/image/.test(file.type)) { this.loading(); // Show this image on here this.$inputs.hide(); this.$editor.html($('', { src: urlAPI.createObjectURL(file) })).show(); this.uploader( file, function(data) { this.setData(data); this.ready(); }, function(error){ this.addMessage(i18n.t('blocks:image:upload_error')); this.ready(); } ); } } }); /* * Extended Image Block * 2014 Ændrew Rininsland * Based on neyre/sir-trevor-wp's ImageCaption.js block. */ SirTrevor.Blocks.ImageExtended = SirTrevor.Blocks.Image.extend({ type: "image_extended", title: function() { return i18n.t('blocks:image:title'); }, droppable: true, uploadable: true, icon_name: 'image', loadData: function(data){ // Create our image tag this.$editor.html($('', { src: data.file.url })).show(); this.$editor.append($('')); this.$editor.append($('', {type: 'text', class: 'st-input-string js-caption-input form-control', name: 'caption', placeholder: '', style: 'width: 100%; margin-top:10px; text-align: center;', value: data.caption})); this.$editor.append($('')); this.$editor.append($('', {type: 'text', class: 'st-input-string js-source-input form-control', name: 'source', placeholder: 'http://', style: 'width: 100%; margin-top:10px; text-align: center;', value: data.source}));}, onBlockRender: function(){ /* Setup the upload button */ this.$inputs.find('button').bind('click', function(ev){ ev.preventDefault(); }); this.$inputs.find('input').on('change', _.bind(function(ev){ this.onDrop(ev.currentTarget); }, this)); }, onDrop: function(transferData){ var file = transferData.files[0], urlAPI = (typeof URL !== "undefined") ? URL : (typeof webkitURL !== "undefined") ? webkitURL : null; // Handle one upload at a time if (/image/.test(file.type)) { this.loading(); // Show this image on here this.$inputs.hide(); this.loadData({file: {url: urlAPI.createObjectURL(file)}}); this.uploader( file, function(data) { this.setData(data); this.ready(); }, function(error){ this.addMessage(i18n.t('blocks:image:upload_error')); this.ready(); } ); } } }); SirTrevor.Blocks.Iframe = (function() { return SirTrevor.Block.extend({ regex_src : /(?:)/i, regex_width : /(?:)/i, regex_height : /(?:)/i, type : 'iframe', icon_name : 'iframe', title : function() { return "Iframe"; }, toolbarEnabled : true, droppable : false, pastable : true, paste_options : { html : '' }, onContentPasted : function(event) { this.loading(); obj = {}; val = $(event.target).val(); match_src = this.regex_src.exec(val); if (match_src !== null && !_.isUndefined(match_src[1])) { obj.src = match_src[1]; match_width = val.match(this.regex_width); if (match_width !== null && !_.isUndefined(match_width[1])) { obj.width = match_width[1]; } match_height = val.match(this.regex_height); if (match_height !== null && !_.isUndefined(match_height[1])) { obj.height = match_height[1]; } this.setAndLoadData(obj); } }, uploadable : false, formattable : false, loadData : function(data) { data.width = (typeof data.width == undefined || !data.width) ? '100%' : data.width; data.height = (typeof data.height == undefined || !data.height) ? '100%' : data.height; this.$inner.prepend( $('" }, youtube: { regex: /(?:http[s]?:\/\/)?(?:www.)?(?:(?:youtube.com\/watch\?(?:.*)(?:v=))|(?:youtu.be\/))([^&].+)/, html: "" }, vine: { regex: /(?:http[s]?:\/\/)?(?:www.)?vine.co\/v\/([^\W]*)/, html: "", square: true }, dailymotion: { regex: /(?:http[s]?:\/\/)?(?:www.)?dai(?:.ly|lymotion.com\/video)\/([^\W_]*)/, html: "" } }, type: 'video', title: function() { return i18n.t('blocks:video:title'); }, droppable: true, pastable: true, icon_name: 'video', loadData: function(data){ if (!this.providers.hasOwnProperty(data.source)) { return; } if (this.providers[data.source].square) { this.$editor.addClass('st-block__editor--with-square-media'); } else { this.$editor.addClass('st-block__editor--with-sixteen-by-nine-media'); } var embed_string = this.providers[data.source].html .replace('{{protocol}}', window.location.protocol) .replace('{{remote_id}}', data.remote_id) .replace('{{width}}', this.$editor.width()); // for videos that can't resize automatically like vine this.$editor.html(embed_string); }, onContentPasted: function(event){ this.handleDropPaste($(event.target).val()); }, handleDropPaste: function(url){ if(!_.isURI(url)) { return; } var match, data; _.each(this.providers, function(provider, index) { match = provider.regex.exec(url); if(match !== null && !_.isUndefined(match[1])) { data = { source: index, remote_id: match[1] }; this.setAndLoadData(data); } }, this); }, onDrop: function(transferData){ var url = transferData.getData('text/plain'); this.handleDropPaste(url); } }); })(); /* Default Formatters */ /* Our base formatters */ (function(){ var Bold = SirTrevor.Formatter.extend({ title: "bold", cmd: "bold", keyCode: 66, text : "B" }); var Italic = SirTrevor.Formatter.extend({ title: "italic", cmd: "italic", keyCode: 73, text : "i" }); var Link = SirTrevor.Formatter.extend({ title: "link", iconName: "link", cmd: "CreateLink", text : "link", onClick: function() { var link = prompt(i18n.t("general:link")), link_regex = /((ftp|http|https):\/\/.)|mailto(?=\:[-\.\w]+@)/; if(link && link.length > 0) { if (!link_regex.test(link)) { link = "http://" + link; } document.execCommand(this.cmd, false, link); } }, isActive: function() { var selection = window.getSelection(), node; if (selection.rangeCount > 0) { node = selection.getRangeAt(0) .startContainer .parentNode; } return (node && node.nodeName == "A"); } }); var UnLink = SirTrevor.Formatter.extend({ title: "unlink", iconName: "link", cmd: "unlink", text : "link" }); /* Create our formatters and add a static reference to them */ SirTrevor.Formatters.Bold = new Bold(); SirTrevor.Formatters.Italic = new Italic(); SirTrevor.Formatters.Link = new Link(); SirTrevor.Formatters.Unlink = new UnLink(); })(); /* Marker */ SirTrevor.BlockControl = (function(){ var BlockControl = function(type, instance_scope) { this.type = type; this.instance_scope = instance_scope; this.block_type = SirTrevor.Blocks[this.type].prototype; this.can_be_rendered = this.block_type.toolbarEnabled; this._ensureElement(); }; _.extend(BlockControl.prototype, FunctionBind, Renderable, SirTrevor.Events, { tagName: 'a', className: "st-block-control", attributes: function() { return { 'data-type': this.block_type.type }; }, render: function() { this.$el.html(''+ _.result(this.block_type, 'icon_name') +'' + _.result(this.block_type, 'title')); return this; } }); return BlockControl; })(); /* SirTrevor Block Controls -- Gives an interface for adding new Sir Trevor blocks. */ SirTrevor.BlockControls = (function(){ var BlockControls = function(available_types, instance_scope) { this.instance_scope = instance_scope; this.available_types = available_types || []; this._ensureElement(); this._bindFunctions(); this.initialize(); }; _.extend(BlockControls.prototype, FunctionBind, Renderable, SirTrevor.Events, { bound: ['handleControlButtonClick'], block_controls: null, className: "st-block-controls", html: "" + i18n.t("general:close") + "", initialize: function() { for(var block_type in this.available_types) { if (SirTrevor.Blocks.hasOwnProperty(block_type)) { var block_control = new SirTrevor.BlockControl(block_type, this.instance_scope); if (block_control.can_be_rendered) { this.$el.append(block_control.render().$el); } } } this.$el.delegate('.st-block-control', 'click', this.handleControlButtonClick); }, show: function() { this.$el.addClass('st-block-controls--active'); SirTrevor.EventBus.trigger('block:controls:shown'); }, hide: function() { this.$el.removeClass('st-block-controls--active'); SirTrevor.EventBus.trigger('block:controls:hidden'); }, handleControlButtonClick: function(e) { e.stopPropagation(); this.trigger('createBlock', $(e.currentTarget).attr('data-type'), null, this.current_container); } }); return BlockControls; })(); /* SirTrevor Floating Block Controls -- Draws the 'plus' between blocks */ SirTrevor.FloatingBlockControls = (function(){ var FloatingBlockControls = function(wrapper, instance_id) { this.$wrapper = wrapper; this.instance_id = instance_id; this._ensureElement(); this._bindFunctions(); this.initialize(); }; _.extend(FloatingBlockControls.prototype, FunctionBind, Renderable, SirTrevor.Events, { className: "st-block-controls__top", attributes: function() { return { 'data-icon': 'add' }; }, bound: ['handleBlockMouseOut', 'handleBlockMouseOver', 'handleBlockClick', 'onDrop'], initialize: function() { this.$el.on('click', this.handleBlockClick) .dropArea() .bind('drop', this.onDrop); this.$wrapper.on('mouseover', '.st-block', this.handleBlockMouseOver) .on('mouseout', '.st-block', this.handleBlockMouseOut) .on('click', '.st-block--with-plus', this.handleBlockClick); }, onDrop: function(ev) { ev.preventDefault(); ev.stopPropagation(); // to prevent event handling on outer blocks var dropped_on = this.$el, item_id = ev.originalEvent.dataTransfer.getData("text/plain"), block = $('#' + item_id); if (!_.isUndefined(item_id) && !_.isEmpty(block) && dropped_on.attr('id') != item_id && this.instance_id == block.attr('data-instance') ) { dropped_on.after(block); } SirTrevor.EventBus.trigger("block:reorder:dropped", item_id); }, handleBlockMouseOver: function(e) { var block = $(e.currentTarget); if (!block.hasClass('st-block--with-plus')) { block.addClass('st-block--with-plus'); } }, handleBlockMouseOut: function(e) { var block = $(e.currentTarget); if (block.hasClass('st-block--with-plus')) { block.removeClass('st-block--with-plus'); } }, handleBlockClick: function(e) { e.stopPropagation(); var block = $(e.currentTarget); this.trigger('showBlockControls', block); } }); return FloatingBlockControls; })(); /* FormatBar */ /* Format Bar -- Displayed on focus on a text area. Renders with all available options for the editor instance */ SirTrevor.FormatBar = (function(){ var FormatBar = function(options) { this.options = _.extend({}, SirTrevor.DEFAULTS.formatBar, options || {}); this._ensureElement(); this._bindFunctions(); this.initialize.apply(this, arguments); }; _.extend(FormatBar.prototype, FunctionBind, SirTrevor.Events, Renderable, { className: 'st-format-bar', bound: ["onFormatButtonClick", "renderBySelection", "hide"], initialize: function() { var formatName, format, btn; this.$btns = []; for (formatName in SirTrevor.Formatters) { if (SirTrevor.Formatters.hasOwnProperty(formatName)) { format = SirTrevor.Formatters[formatName]; btn = $("