3488 lines
107 KiB
JavaScript
Executable File
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, " ")
|
|
.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(/ /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(/</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 = $('<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>",
|
|
"— <%= 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, _));
|