Files
cm-web-legacy/public/assets/adm/js/trevor/sir-trevor.js
2020-06-10 06:19:03 +09:00

3488 lines
107 KiB
JavaScript
Executable File

/*!
* 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<c;b++)a.appendChild(arguments[b]);return a}function j(a,b,c,d){var g=["opacity",b,~~(a*100),c,d].join("-"),h=.01+c/d*100,j=Math.max(1-(1-a)/b*(100-h),a),k=f.substring(0,f.indexOf("Animation")).toLowerCase(),l=k&&"-"+k+"-"||"";return e[g]||(i.insertRule("@"+l+"keyframes "+g+"{"+"0%{opacity:"+j+"}"+h+"%{opacity:"+a+"}"+(h+.01)+"%{opacity:1}"+(h+b)%100+"%{opacity:"+a+"}"+"100%{opacity:"+j+"}"+"}",0),e[g]=1),g}function k(a,b){var e=a.style,f,g;if(e[b]!==c)return b;b=b.charAt(0).toUpperCase()+b.slice(1);for(g=0;g<d.length;g++){f=d[g]+b;if(e[f]!==c)return f}}function l(a,b){for(var c in b)a.style[k(a,c)||c]=b[c];return a}function m(a){for(var b=1;b<arguments.length;b++){var d=arguments[b];for(var e in d)a[e]===c&&(a[e]=d[e])}return a}function n(a){var b={x:a.offsetLeft,y:a.offsetTop};while(a=a.offsetParent)b.x+=a.offsetLeft,b.y+=a.offsetTop;return b}var d=["webkit","Moz","ms","O"],e={},f,i=function(){var a=g("style");return h(b.getElementsByTagName("head")[0],a),a.sheet||a.styleSheet}(),o={lines:12,length:7,width:5,radius:10,rotate:0,color:"#000",speed:1,trail:100,opacity:.25,fps:20,zIndex:2e9,className:"spinner",top:"auto",left:"auto"},p=function q(a){if(!this.spin)return new q(a);this.opts=m(a||{},q.defaults,o)};p.defaults={},m(p.prototype,{spin:function(a){this.stop();var b=this,c=b.opts,d=b.el=l(g(0,{className:c.className}),{position:"relative",zIndex:c.zIndex}),e=c.radius+c.length+c.width,h,i;a&&(a.insertBefore(d,a.firstChild||null),i=n(a),h=n(d),l(d,{left:(c.left=="auto"?i.x-h.x+(a.offsetWidth>>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.lines;c++)d=l(g(),{position:"absolute",top:1+~(b.width/2)+"px",transform:b.hwaccel?"translate3d(0,0,0)":"",opacity:b.opacity,animation:f&&j(b.opacity,b.trail,c,b.lines)+" "+1/b.speed+"s linear infinite"}),b.shadow&&h(d,l(e("#000","0 0 4px #000"),{top:"2px"})),h(a,h(d,e(b.color,"0 0 1px rgba(0,0,0,.1)")));return a},opacity:function(a,b,c){b<a.childNodes.length&&(a.childNodes[b].style.opacity=c)}}),!function(){function a(a,b){return g("<"+a+' xmlns="urn:schemas-microsoft.com:vml" class="spin-vml">',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<e.childNodes.length&&(e=e.childNodes[b+d],e=e&&e.firstChild,e=e&&e.firstChild,e&&(e.opacity=c))}):f=k(b,"animation")}(),a.Spinner=p})(window,document);
/*
* Sir Trevor Editor Store
* By default we store the complete data on the instances $el
* We can easily extend this and store it on some server or something
*/
SirTrevor.editorStore = function(editor, method, options) {
var resp;
options = options || {};
switch(method) {
case "create":
// Grab our JSON from the textarea and clean any whitespace incase there is a line wrap between the opening and closing textarea tags
var content = _.trim(editor.$el.val());
editor.dataStore = { data: [] };
if (content.length > 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 = "<div>" + html;
}
html = html.replace(/\[([^\]]+)\]\(([^\)]+)\)/gm,function(match, p1, p2){
return "<a href='"+p2+"'>"+p1.replace(/\r?\n/g, '')+"</a>";
});
// 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, "</div><div><br></div><div>");
html = html.replace(/\r?\n/gm, "</div><div>");
}
html = html.replace(/\t/g, "&nbsp;&nbsp;&nbsp;&nbsp;")
.replace(/\r?\n/g, "<br>")
.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 += "</div>";
}
return html;
};
SirTrevor.toMarkdown = function(content, type) {
type = _.classify(type);
var markdown = content;
//Normalise whitespace
markdown = markdown.replace(/&nbsp;/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]+'><br></'+inlineTags[i]+'>', 'gi');
markdown = markdown.replace(tagStripper, '<br>');
}
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.*?href=[""'](.*?)[""'].*?>(.*?)<\/a>/gim, function(match, p1, p2){
return "[" + p2.trim().replace(/<(.)?br(.)?>/g, '') + "]("+ p1 +")";
}) // Hyperlinks
.replace(/<strong>(?:\s*)(.*?)(\s)*?<\/strong>/gim, replaceBolds)
.replace(/<b>(?:\s*)(.*?)(\s*)?<\/b>/gim, replaceBolds)
.replace(/<em>(?:\s*)(.*?)(\s*)?<\/em>/gim, replaceItalics)
.replace(/<i>(?:\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(/([^<>]+)(<div>)/g,"$1\n$2") // Divitis style line breaks (handle the first line)
.replace(/<div><div>/g,'\n<div>') // ^ (double opening divs with one close from Chrome)
.replace(/(?:<div>)([^<>]+)(?:<div>)/g,"$1\n") // ^ (handle nested divs that start with content)
.replace(/(?:<div>)(?:<br>)?([^<>]+)(?:<br>)?(?:<\/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(/&lt;/g,"<").replace(/&gt;/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 = $('<div>', {'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 $("<a>",
{ '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 = [
"<div class='st-block-positioner__inner'>",
"<span class='st-block-positioner__selected-value'></span>",
"<select class='st-block-positioner__select'></select>",
"</div>"
].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 = "<option value='0'>" + i18n.t("general:position") + "</option>";
for(var i = 1; i <= this.total_blocks; i++) {
inner += "<option value="+i+">"+i+"</option>";
}
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(
"<div class='st-block__inner'><%= editor_html %></div>"
),
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 = $("<div>", { 'class': 'st-block__ui' });
this.$inner.append(ui_element);
this.$ui = ui_element;
this._initUIComponents();
},
_initMessages: function() {
var msgs_element = $("<div>", { 'class': 'st-block__messages' });
this.$inner.prepend(msgs_element);
this.$messages = msgs_element;
},
addMessage: function(msg, additionalClass) {
var $msg = $("<span>", { 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 = [
"<div class='st-block__ui-delete-controls'>",
"<label class='st-block__delete-label'>",
"<%= i18n.t('general:delete') %>",
"</label>",
"<a class='st-block-ui-btn st-block-ui-btn--confirm-delete st-icon' data-icon='tick'></a>",
"<a class='st-block-ui-btn st-block-ui-btn--deny-delete st-icon' data-icon='close'></a>",
"</div>"
].join("\n");
var drop_options = {
html: ['<div class="st-block__dropzone">',
'<span class="st-icon"><%= _.result(block, "icon_name") %></span>',
'<p><%= i18n.t("general:drop", { block: "<span>" + _.result(block, "title") + "</span>" }) %>',
'</p></div>'].join('\n'),
re_render_on_reorder: false
};
var paste_options = {
html: ['<input type="text" placeholder="<%= i18n.t("general:paste") %>"',
' class="st-block__paste-input st-paste-block">'].join('')
};
var upload_options = {
html: [
'<div class="st-block__upload-container">',
'<input type="file" type="st-file-upload">',
'<button class="st-upload-btn"><%= i18n.t("general:upload") %></button>',
'</div>'
].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: '<div class="st-block__editor"></div>',
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 = $("<div>", { '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([
'<blockquote class="st-required st-text-block" contenteditable="true"></blockquote>',
'<label class="st-input-label"> <%= i18n.t("blocks:quote:credit_field") %></label>',
'<input maxlength="140" name="cite" placeholder="<%= i18n.t("blocks:quote:credit_field") %>"',
' 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(
'<div class="columns-row" id="<%= blockID %>-columns-row" style="overflow: auto"/>'
, {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 = $('<div class="column" style="float: left; "></div>');
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<columns_data.length; i++)
{
var $block = null;
var $column = this.getColumn(i);
for (var j=0; j<columns_data[i].blocks.length; j++) {
var block = columns_data[i].blocks[j];
$block = this.sirTrevor.createBlock(block.type, block.data, $block ? $block.$el : $column.children('.st-block-controls__top'));
}
}
},
_initUIComponents: function() {
SirTrevor.Block.prototype._initUIComponents.apply(this, arguments);
},
performValidations: function() {
// nothing
}
});
})();
/*
Heading Block
*/
SirTrevor.Blocks.Heading = SirTrevor.Block.extend({
type: 'Heading',
title: function(){ return i18n.t('blocks:heading:title'); },
editorHTML: '<div class="heading-block"><select name="heading" class="form-control"><option value="h1">H1</option><option value="h2">H2</option><option value="h3">H3</option><option value="h4">H4</option></select></div><div class="st-required st-text-block st-text-block--heading" contenteditable="true"></div>',
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($('<img>', { 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($('<img>', { 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 <aendrew.rininsland@the-times.co.uk>
* 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($('<img>', { src: data.file.url })).show();
this.$editor.append($('<label class="editlabel" for="alt">Image Alt Text:</label>'));
this.$editor.append($('<input>', {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($('<label class="editlabel" for="link">Link to page or file? include "http://" (leave empty for none):</label>'));
this.$editor.append($('<input>', {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 : /(?:<iframe)(?:.+)(?:src="){1}([^"].+?)(?:")(?:.+)(?:<\/iframe>)/i,
regex_width : /(?:<iframe)(?:.+)(?:width="){1}([^"].+?)(?:")(?:.+)(?:<\/iframe>)/i,
regex_height : /(?:<iframe)(?:.+)(?:height="){1}([^"].+?)(?:")(?:.+)(?:<\/iframe>)/i,
type : 'iframe',
icon_name : 'iframe',
title : function() {
return "Iframe";
},
toolbarEnabled : true,
droppable : false,
pastable : true,
paste_options : {
html : '<input type="text" placeholder="<iframe>" class="st-block__paste-input st-paste-block">'
},
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(
$('<iframe>')
.attr('src', data.src)
.attr('class', 'st-block-embed')
.attr('width', data.width)
.attr('height', data.height)
);
this.ready();
},
});
})();
/*
Text Block
*/
SirTrevor.Blocks.Text = SirTrevor.Block.extend({
type: "text",
title: function() { return i18n.t('blocks:text:title'); },
editorHTML: '<div class="st-required st-text-block" contenteditable="true"></div>',
icon_name: 'text',
loadData: function(data){
this.getTextBlock().html(SirTrevor.toHTML(data.text, this.type));
}
});
SirTrevor.Blocks.Tweet = (function(){
var tweet_template = _.template([
"<blockquote class='twitter-tweet' align='center'>",
"<p><%= text %></p>",
"&mdash; <%= user.name %> (@<%= user.screen_name %>)",
"<a href='<%= status_url %>' data-datetime='<%= created_at %>'><%= created_at %></a>",
"</blockquote>",
'<script src="//platform.twitter.com/widgets.js" charset="utf-8"></script>'
].join("\n"));
return SirTrevor.Block.extend({
type: "tweet",
droppable: true,
pastable: true,
fetchable: true,
drop_options: {
re_render_on_reorder: true
},
title: function(){ return i18n.t('blocks:tweet:title'); },
fetchUrl: function(tweetID) {
return "/tweets/?tweet_id=" + tweetID;
},
icon_name: 'twitter',
loadData: function(data) {
if (_.isUndefined(data.status_url)) { data.status_url = ''; }
this.$inner.find('iframe').remove();
this.$inner.prepend(tweet_template(data));
},
onContentPasted: function(event){
// Content pasted. Delegate to the drop parse method
var input = $(event.target),
val = input.val();
// Pass this to the same handler as onDrop
this.handleTwitterDropPaste(val);
},
handleTwitterDropPaste: function(url){
if (!this.validTweetUrl(url)) {
SirTrevor.log("Invalid Tweet URL");
return;
}
// Twitter status
var tweetID = url.match(/[^\/]+$/);
if (!_.isEmpty(tweetID)) {
this.loading();
tweetID = tweetID[0];
var ajaxOptions = {
url: this.fetchUrl(tweetID),
dataType: "json"
};
this.fetch(ajaxOptions, this.onTweetSuccess, this.onTweetFail);
}
},
validTweetUrl: function(url) {
return (_.isURI(url) &&
url.indexOf("twitter") !== -1 &&
url.indexOf("status") !== -1);
},
onTweetSuccess: function(data) {
// Parse the twitter object into something a bit slimmer..
var obj = {
user: {
profile_image_url: data.user.profile_image_url,
profile_image_url_https: data.user.profile_image_url_https,
screen_name: data.user.screen_name,
name: data.user.name
},
id: data.id_str,
text: data.text,
created_at: data.created_at,
entities: data.entities,
status_url: "https://twitter.com/" + data.user.screen_name + "/status/" + data.id_str
};
this.setAndLoadData(obj);
this.ready();
},
onTweetFail: function() {
this.addMessage(i18n.t("blocks:tweet:fetch_error"));
this.ready();
},
onDrop: function(transferData){
var url = transferData.getData('text/plain');
this.handleTwitterDropPaste(url);
}
});
})();
/*
Unordered List
*/
SirTrevor.Blocks.List = (function() {
var template = '<div class="st-text-block st-required" contenteditable="true"><ul><li></li></ul></div>';
return SirTrevor.Block.extend({
type: 'list',
title: function() { return i18n.t('blocks:list:title'); },
icon_name: 'list',
editorHTML: function() {
return _.template(template, this);
},
loadData: function(data){
this.getTextBlock().html("<ul>" + SirTrevor.toHTML(data.text, this.type) + "</ul>");
},
onBlockRender: function() {
this.checkForList = _.bind(this.checkForList, this);
this.getTextBlock().on('click keyup', this.checkForList);
},
checkForList: function() {
if (this.$('ul').length === 0) {
document.execCommand("insertUnorderedList", false, false);
}
},
toMarkdown: function(markdown) {
return markdown.replace(/<\/li>/mg,"\n")
.replace(/<\/?[^>]+(>|$)/g, "")
.replace(/^(.+)$/mg," - $1");
},
toHTML: function(html) {
html = html.replace(/^ - (.+)$/mg,"<li>$1</li>")
.replace(/\n/mg, "");
return html;
},
onContentPasted: function(event, target) {
var replace = this.pastedMarkdownToHTML(target[0].innerHTML),
list = this.$('ul').html(replace);
this.getTextBlock().caretToEnd();
},
isEmpty: function() {
return _.isEmpty(this.saveAndGetData().text);
}
});
})();
SirTrevor.Blocks.Video = (function(){
return SirTrevor.Block.extend({
// more providers at https://gist.github.com/jeffling/a9629ae28e076785a14f
providers: {
vimeo: {
regex: /(?:http[s]?:\/\/)?(?:www.)?vimeo.com\/(.+)/,
html: "<iframe src=\"{{protocol}}//player.vimeo.com/video/{{remote_id}}?title=0&byline=0\" width=\"580\" height=\"320\" frameborder=\"0\"></iframe>"
},
youtube: {
regex: /(?:http[s]?:\/\/)?(?:www.)?(?:(?:youtube.com\/watch\?(?:.*)(?:v=))|(?:youtu.be\/))([^&].+)/,
html: "<iframe src=\"{{protocol}}//www.youtube.com/embed/{{remote_id}}\" width=\"580\" height=\"320\" frameborder=\"0\" allowfullscreen></iframe>"
},
vine: {
regex: /(?:http[s]?:\/\/)?(?:www.)?vine.co\/v\/([^\W]*)/,
html: "<iframe class=\"vine-embed\" src=\"{{protocol}}//vine.co/v/{{remote_id}}/embed/simple\" width=\"{{width}}\" height=\"{{width}}\" frameborder=\"0\"></iframe><script async src=\"http://platform.vine.co/static/scripts/embed.js\" charset=\"utf-8\"></script>",
square: true
},
dailymotion: {
regex: /(?:http[s]?:\/\/)?(?:www.)?dai(?:.ly|lymotion.com\/video)\/([^\W_]*)/,
html: "<iframe src=\"{{protocol}}//www.dailymotion.com/embed/video/{{remote_id}}\" width=\"580\" height=\"320\" frameborder=\"0\"></iframe>"
}
},
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('<span class="st-icon">'+ _.result(this.block_type, 'icon_name') +'</span>' + _.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: "<a class='st-icon st-icon--close'>" + i18n.t("general:close") + "</a>",
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 = $("<button>", {
'class': 'st-format-btn st-format-btn--' + formatName + ' ' + (format.iconName ? 'st-icon' : ''),
'text': format.text,
'data-type': formatName,
'data-cmd': format.cmd
});
this.$btns.push(btn);
btn.appendTo(this.$el);
}
}
this.$b = $(document);
this.$el.bind('click', '.st-format-btn', this.onFormatButtonClick);
},
hide: function() {
this.$el.removeClass('st-format-bar--is-ready');
},
show: function() {
this.$el.addClass('st-format-bar--is-ready');
},
remove: function(){ this.$el.remove(); },
renderBySelection: function(rectangles) {
var selection = window.getSelection(),
range = selection.getRangeAt(0),
boundary = range.getBoundingClientRect(),
coords = {};
coords.top = boundary.top + 20 + window.pageYOffset - this.$el.height() + 'px';
coords.left = ((boundary.left + boundary.right) / 2) - (this.$el.width() / 2) + 'px';
this.highlightSelectedButtons();
this.show();
this.$el.css(coords);
},
highlightSelectedButtons: function() {
var formatter;
_.each(this.$btns, function($btn) {
formatter = SirTrevor.Formatters[$btn.attr('data-type')];
$btn.toggleClass("st-format-btn--is-active",
formatter.isActive());
}, this);
},
onFormatButtonClick: function(ev){
ev.stopPropagation();
var btn = $(ev.target),
format = SirTrevor.Formatters[btn.attr('data-type')];
if (_.isUndefined(format)) return false;
// Do we have a click function defined on this formatter?
if(!_.isUndefined(format.onClick) && _.isFunction(format.onClick)) {
format.onClick(); // Delegate
} else {
// Call default
document.execCommand(btn.attr('data-cmd'), false, format.param);
}
this.highlightSelectedButtons();
return false;
}
});
return FormatBar;
})();
/*
Sir Trevor Editor
--
Represents one Sir Trevor editor instance (with multiple blocks)
Each block references this instance.
BlockTypes are global however.
*/
SirTrevor.Editor = (function(){
var SirTrevorEditor = function(options) {
this.initialize(options);
};
_.extend(SirTrevorEditor.prototype, FunctionBind, SirTrevor.Events, {
bound: ['onFormSubmit', 'showBlockControls', 'hideAllTheThings', 'hideBlockControls',
'onNewBlockCreated', 'changeBlockPosition', 'onBlockDragStart', 'onBlockDragEnd',
'removeBlockDragOver', 'onBlockDropped', 'createBlock'],
events: {
'block:reorder:down': 'hideBlockControls',
'block:reorder:dragstart': 'onBlockDragStart',
'block:reorder:dragend': 'onBlockDragEnd',
'block:content:dropped': 'removeBlockDragOver',
'block:reorder:dropped': 'onBlockDropped',
'block:create:new': 'onNewBlockCreated'
},
initialize: function(options) {
SirTrevor.log("Init SirTrevor.Editor");
this.blockTypes = {};
this.blockCounts = {}; // Cached block type counts
this.blocks = []; // Block references
this.errors = [];
this.options = _.extend({}, SirTrevor.DEFAULTS, options || {});
this.ID = _.uniqueId('st-editor-');
if (!this._ensureAndSetElements()) { return false; }
if(!_.isUndefined(this.options.onEditorRender) && _.isFunction(this.options.onEditorRender)) {
this.onEditorRender = this.options.onEditorRender;
}
this._setRequired();
this._setBlocksTypes();
this._bindFunctions();
this.store("create");
SirTrevor.instances.push(this);
this.build();
SirTrevor.bindFormSubmit(this.$form);
},
/*
Build the Editor instance.
Check to see if we've been passed JSON already, and if not try and create a default block.
If we have JSON then we need to build all of our blocks from this.
*/
build: function() {
this.$el.hide();
this.block_controls = new SirTrevor.BlockControls(this.blockTypes, this.ID);
this.fl_block_controls = new SirTrevor.FloatingBlockControls(this.$wrapper, this.ID, this);
this.formatBar = new SirTrevor.FormatBar(this.options.formatBar);
this.listenTo(this.block_controls, 'createBlock', this.createBlock);
this.listenTo(this.fl_block_controls, 'showBlockControls', this.showBlockControls);
this._setEvents();
SirTrevor.EventBus.on(this.ID + ":blocks:change_position", this.changeBlockPosition);
SirTrevor.EventBus.on("formatter:position", this.formatBar.renderBySelection);
SirTrevor.EventBus.on("formatter:hide", this.formatBar.hide);
this.$wrapper.prepend(this.fl_block_controls.render().$el);
$(document.body).append(this.formatBar.render().$el);
this.$outer.append(this.block_controls.render().$el);
$(window).bind('click', this.hideAllTheThings);
var store = this.store("read");
if (store.data.length > 0) {
_.each(store.data, function(block){
SirTrevor.log('Creating: ' + block.type);
this.createBlock(block.type, block.data);
}, this);
} else if (this.options.defaultType !== false) {
this.createBlock(this.options.defaultType, {});
}
this.$wrapper.addClass('st-ready');
if(!_.isUndefined(this.onEditorRender)) {
this.onEditorRender();
}
},
destroy: function() {
// Destroy the rendered sub views
this.formatBar.destroy();
this.fl_block_controls.destroy();
this.block_controls.destroy();
// Destroy all blocks
_.each(this.$wrapper.children('.st-block'), function(block) {
this.removeBlock(block.id);
}, this);
// Stop listening to events
this.stopListening();
SirTrevor.unbindFormSubmit(this.$form);
// Cleanup element
var el = this.$el.detach();
// Remove instance
SirTrevor.instances = _.reject(SirTrevor.instances, _.bind(function(instance) {
return instance.ID == this.ID;
}, this));
// Clear the store
this.store("reset");
this.$outer.replaceWith(el);
},
reinitialize: function(options) {
this.destroy();
this.initialize(options || this.options);
},
_setEvents: function() {
_.each(this.events, function(callback, type) {
SirTrevor.EventBus.on(type, this[callback], this);
}, this);
},
hideAllTheThings: function(e) {
this.block_controls.hide();
this.formatBar.hide();
if (!_.isUndefined(this.block_controls.current_container)) {
this.block_controls.current_container.removeClass("with-st-controls");
}
},
showBlockControls: function(container) {
if (!_.isUndefined(this.block_controls.current_container)) {
this.block_controls.current_container.removeClass("with-st-controls");
}
this.block_controls.show();
container.append(this.block_controls.$el.detach());
container.addClass('with-st-controls');
this.block_controls.current_container = container;
},
store: function(method, options){
return SirTrevor.editorStore(this, method, options || {});
},
/*
Create an instance of a block from an available type.
We have to check the number of blocks we're allowed to create before adding one and handle fails accordingly.
A block will have a reference to an Editor instance & the parent BlockType.
We also have to remember to store static counts for how many blocks we have, and keep a nice array of all the blocks available.
*/
createBlock: function(type, data, $container) {
type = _.classify(type);
if(this._blockLimitReached()) {
SirTrevor.log("Cannot add any more blocks. Limit reached.");
return false;
}
if (!this._isBlockTypeAvailable(type)) {
SirTrevor.log("Block type not available " + type);
return false;
}
// Can we have another one of these blocks?
if (!this._canAddBlockType(type)) {
SirTrevor.log("Block Limit reached for type " + type);
return false;
}
// additionally pass master editor object to block (needed for nested blocks)
var block = new SirTrevor.Blocks[type](data, this.ID, this);
// optional `$container` argument can define the DOM element to adopt new block
// this is used for initial blocks structure building to support nested blocks
this._renderInPosition(block.render().$el, $container);
this.listenTo(block, 'removeBlock', this.removeBlock);
this.blocks.push(block);
this._incrementBlockTypeCount(type);
block.focus();
SirTrevor.EventBus.trigger(data ? "block:create:existing" : "block:create:new", block);
SirTrevor.log("Block created of type " + type);
block.trigger("onRender");
this.$wrapper.toggleClass('st--block-limit-reached', this._blockLimitReached());
this.triggerBlockCountUpdate();
return block;
},
onNewBlockCreated: function(block) {
this.hideBlockControls();
this.scrollTo(block.$el);
},
scrollTo: function(element) {
$('html, body').animate({ scrollTop: element.offset().top }, 300, "linear");
},
blockFocus: function(block) {
this.block_controls.current_container = null;
},
hideBlockControls: function() {
if (!_.isUndefined(this.block_controls.current_container)) {
this.block_controls.current_container.removeClass("with-st-controls");
}
this.block_controls.hide();
},
removeBlockDragOver: function() {
this.$outer.find('.st-drag-over').removeClass('st-drag-over');
},
triggerBlockCountUpdate: function() {
SirTrevor.EventBus.trigger(this.ID + ":blocks:count_update", this.blocks.length);
},
changeBlockPosition: function($block, selectedPosition) {
selectedPosition = selectedPosition - 1;
var blockPosition = this.getBlockPosition($block);
var $blockBy = $block.parent().children('.st-block').eq(selectedPosition);
var where = (blockPosition > selectedPosition) ? "Before" : "After";
if($blockBy && $blockBy.attr('id') !== $block.attr('id')) {
this.hideAllTheThings();
$block["insert" + where]($blockBy);
this.scrollTo($block);
}
},
onBlockDropped: function(block_id) {
this.hideAllTheThings();
var block = this.findBlockById(block_id);
if (!_.isUndefined(block) &&
!_.isEmpty(block.getData()) &&
block.drop_options.re_render_on_reorder) {
block.beforeLoadingData();
}
},
onBlockDragStart: function() {
this.hideBlockControls();
this.$wrapper.addClass("st-outer--is-reordering");
},
onBlockDragEnd: function() {
this.removeBlockDragOver();
this.$wrapper.removeClass("st-outer--is-reordering");
},
_renderInPosition: function(block, $container) {
if (!_.isUndefined($container)) {
$container.after(block);
} else {
this.$wrapper.append(block);
}
},
_incrementBlockTypeCount: function(type) {
this.blockCounts[type] = (_.isUndefined(this.blockCounts[type])) ? 1: this.blockCounts[type] + 1;
},
_getBlockTypeCount: function(type) {
return (_.isUndefined(this.blockCounts[type])) ? 0 : this.blockCounts[type];
},
_canAddBlockType: function(type) {
var block_type_limit = this._getBlockTypeLimit(type);
return !(block_type_limit !== 0 && this._getBlockTypeCount(type) >= block_type_limit);
},
_blockLimitReached: function() {
return (this.options.blockLimit !== 0 && this.blocks.length >= this.options.blockLimit);
},
removeBlock: function(block_id) {
var block = this.findBlockById(block_id),
type = _.classify(block.type),
controls = block.$el.find('.st-block-controls');
var nested_blocks = block.$el.find('.st-block');
if (nested_blocks.length > 0)
{
var list = nested_blocks.map(function() {
return { depth: $(this).parents().length, id: this.getAttribute('id') };
}).get();
list.sort(function(a, b) { return a.depth - b.depth; });
// remove nested blocks in depth first order
for (var i=0; i<list.length; i++) this.removeBlock(list[i].id);
}
if (controls.length) {
this.block_controls.hide();
this.$wrapper.prepend(controls);
}
this.blockCounts[type] = this.blockCounts[type] - 1;
this.blocks = _.reject(this.blocks, function(item){ return (item.blockID == block.blockID); });
this.stopListening(block);
SirTrevor.EventBus.trigger("block:remove:pre", block);
block.remove();
SirTrevor.EventBus.trigger("block:remove", block);
this.triggerBlockCountUpdate();
this.$wrapper.toggleClass('st--block-limit-reached', this._blockLimitReached());
},
performValidations : function(block, should_validate) {
var errors = 0;
if (!SirTrevor.SKIP_VALIDATION && should_validate) {
if(!block.valid()){
this.errors.push({ text: _.result(block, 'validationFailMsg') });
SirTrevor.log("Block " + block.blockID + " failed validation");
++errors;
}
}
return errors;
},
saveBlockStateToStore: function(block) {
var store = block.saveAndReturnData();
if(store && !_.isEmpty(store.data)) {
SirTrevor.log("Adding data for block " + block.blockID + " to block store");
this.store("add", { data: store });
}
},
/*
Handle a form submission of this Editor instance.
Validate all of our blocks, and serialise all data onto the JSON objects
*/
onFormSubmit: function(should_validate) {
// if undefined or null or anything other than false - treat as true
should_validate = (should_validate === false) ? false : true;
SirTrevor.log("Handling form submission for Editor " + this.ID);
this.removeErrors();
this.store("reset");
this.validateBlocks(should_validate);
this.validateBlockTypesExist(should_validate);
this.renderErrors();
this.store("save");
return this.errors.length;
},
validateBlocks: function(should_validate) {
var blockIterator = function(block,index) {
var _block = _.find(this.blocks, function(b) {
return (b.blockID == $(block).attr('id')); });
if (_.isUndefined(_block)) { return false; }
// Find our block
this.performValidations(_block, should_validate);
// save only top-level blocks
if (this.$wrapper.children().filter(block).length > 0)
{
this.saveBlockStateToStore(_block);
}
};
_.each(this.$wrapper.find('.st-block'), blockIterator, this);
},
validateBlockTypesExist: function(should_validate) {
if (!this.required && (SirTrevor.SKIP_VALIDATION && !should_validate)) {
return false;
}
var blockTypeIterator = function(type, index) {
if (!this._isBlockTypeAvailable(type)) { return; }
if (this._getBlockTypeCount(type) === 0) {
SirTrevor.log("Failed validation on required block type " + type);
this.errors.push({ text: i18n.t("errors:type_missing", { type: type }) });
} else {
var blocks = _.filter(this.getBlocksByType(type), function(b) {
return !b.isEmpty();
});
if (blocks.length > 0) { return false; }
this.errors.push({ text: i18n.t("errors:required_type_empty", { type: type }) });
SirTrevor.log("A required block type " + type + " is empty");
}
};
if (_.isArray(this.required)) {
_.each(this.required, blockTypeIterator, this);
}
},
renderErrors: function() {
if (this.errors.length === 0) { return false; }
if (_.isUndefined(this.$errors)) {
this.$errors = this._errorsContainer();
}
var str = "<ul>";
_.each(this.errors, function(error) {
str += '<li class="st-errors__msg">'+ error.text +'</li>';
});
str += "</ul>";
this.$errors.append(str);
this.$errors.show();
},
_errorsContainer: function() {
if (_.isUndefined(this.options.errorsContainer)) {
var $container = $("<div>", {
'class': 'st-errors',
html: "<p>" + i18n.t("errors:title") + " </p>"
});
this.$outer.prepend($container);
return $container;
}
return $element(this.options.errorsContainer);
},
removeErrors: function() {
if (this.errors.length === 0) { return false; }
this.$errors.hide().find('ul').html('');
this.errors = [];
},
findBlockById: function(block_id) {
return _.find(this.blocks, function(b){ return b.blockID == block_id; });
},
getBlocksByType: function(block_type) {
return _.filter(this.blocks, function(b){ return _.classify(b.type) == block_type; });
},
getBlocksByIDs: function(block_ids) {
return _.filter(this.blocks, function(b){ return _.contains(block_ids, b.blockID); });
},
getBlockPosition: function($block) {
return $block.parent().children('.st-block').index($block);
},
/*
Get Block Type Limit
--
returns the limit for this block, which can be set on a per Editor instance, or on a global blockType scope.
*/
_getBlockTypeLimit: function(t) {
if (!this._isBlockTypeAvailable(t)) { return 0; }
return parseInt((_.isUndefined(this.options.blockTypeLimits[t])) ? 0 : this.options.blockTypeLimits[t], 10);
},
/*
Availability helper methods
--
Checks if the object exists within the instance of the Editor.
*/
_isBlockTypeAvailable: function(t) {
return !_.isUndefined(this.blockTypes[t]);
},
_ensureAndSetElements: function() {
if(_.isUndefined(this.options.el) || _.isEmpty(this.options.el)) {
SirTrevor.log("You must provide an el");
return false;
}
this.$el = this.options.el;
this.el = this.options.el[0];
this.$form = this.$el.parents('form');
var $outer = $("<div>").attr({ 'id': this.ID, 'class': 'st-outer', 'dropzone': 'copy link move' });
var $wrapper = $("<div>").attr({ 'class': 'st-blocks' });
// Wrap our element in lots of containers *eww*
this.$el.wrap($outer).wrap($wrapper);
this.$outer = this.$form.find('#' + this.ID);
this.$wrapper = this.$outer.find('.st-blocks');
return true;
},
/*
Set our blockTypes
These will either be set on a per Editor instance, or set on a global scope.
*/
_setBlocksTypes: function() {
this.blockTypes = _.flattern((_.isUndefined(this.options.blockTypes)) ? SirTrevor.Blocks : this.options.blockTypes);
},
/* Get our required blocks (if any) */
_setRequired: function() {
if (_.isArray(this.options.required) && !_.isEmpty(this.options.required)) {
this.required = this.options.required;
} else {
this.required = false;
}
}
});
return SirTrevorEditor;
})();
/* We need a form handler here to handle all the form submits */
SirTrevor.setDefaults = function(options) {
SirTrevor.DEFAULTS = _.extend(SirTrevor.DEFAULTS, options || {});
};
SirTrevor.bindFormSubmit = function(form) {
if (!formBound) {
new SirTrevor.Submittable(form);
form.bind('submit', this.onFormSubmit);
formBound = true;
}
};
SirTrevor.unbindFormSubmit = function(form) {
if (formBound) {
form.unbind('submit', this.onFormSubmit);
formBound = false;
}
};
SirTrevor.onBeforeSubmit = function(should_validate) {
// Loop through all of our instances and do our form submits on them
var errors = 0;
_.each(SirTrevor.instances, function(inst, i) {
errors += inst.onFormSubmit(should_validate);
});
SirTrevor.log("Total errors: " + errors);
return errors;
};
SirTrevor.onFormSubmit = function(ev) {
var errors = SirTrevor.onBeforeSubmit();
if(errors > 0) {
SirTrevor.EventBus.trigger("onError");
ev.preventDefault();
}
};
SirTrevor.getInstance = function(identifier) {
if (_.isUndefined(identifier)) {
return this.instances[0];
}
if (_.isString(identifier)) {
return _.find(this.instances,
function(editor){ return editor.ID === identifier; });
}
return this.instances[identifier];
};
SirTrevor.setBlockOptions = function(type, options) {
var block = SirTrevor.Blocks[type];
if (_.isUndefined(block)) {
return;
}
_.extend(block.prototype, options || {});
};
SirTrevor.runOnAllInstances = function(method) {
if (_.has(SirTrevor.Editor.prototype, method)) {
// augment the arguments pseudo array and pass on to invoke()
// this allows us to pass arguments on to the target methods
[].unshift.call(arguments, SirTrevor.instances);
_.invoke.apply(_, arguments);
} else {
SirTrevor.log("method doesn't exist");
}
};
}(jQuery, _));