{I" class:ETI"BundledAsset;FI"logical_path;TI"sir-trevor.js;FI" pathname;TI"Œ/Volumes/TempStorage/rbenv/versions/2.1.0/lib/ruby/gems/2.1.0/bundler/gems/sir-trevor-rails-33de17d54122/app/assets/javascript/sir-trevor.js;FI"content_type;TI"application/javascript;TI" mtime;Tl+Ê6ýRI" length;Ti‰8I" digest;TI"%97f65d53b5d1e2972e47b28174473538;FI" source;TI"‰8// Underscore.js 1.5.2 // http://underscorejs.org // (c) 2009-2013 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors // Underscore may be freely distributed under the MIT license. (function() { // Baseline setup // -------------- // Establish the root object, `window` in the browser, or `exports` on the server. var root = this; // Save the previous value of the `_` variable. var previousUnderscore = root._; // Establish the object that gets returned to break out of a loop iteration. var breaker = {}; // Save bytes in the minified (but not gzipped) version: var ArrayProto = Array.prototype, ObjProto = Object.prototype, FuncProto = Function.prototype; // Create quick reference variables for speed access to core prototypes. var push = ArrayProto.push, slice = ArrayProto.slice, concat = ArrayProto.concat, toString = ObjProto.toString, hasOwnProperty = ObjProto.hasOwnProperty; // All **ECMAScript 5** native function implementations that we hope to use // are declared here. var nativeForEach = ArrayProto.forEach, nativeMap = ArrayProto.map, nativeReduce = ArrayProto.reduce, nativeReduceRight = ArrayProto.reduceRight, nativeFilter = ArrayProto.filter, nativeEvery = ArrayProto.every, nativeSome = ArrayProto.some, nativeIndexOf = ArrayProto.indexOf, nativeLastIndexOf = ArrayProto.lastIndexOf, nativeIsArray = Array.isArray, nativeKeys = Object.keys, nativeBind = FuncProto.bind; // Create a safe reference to the Underscore object for use below. var _ = function(obj) { if (obj instanceof _) return obj; if (!(this instanceof _)) return new _(obj); this._wrapped = obj; }; // Export the Underscore object for **Node.js**, with // backwards-compatibility for the old `require()` API. If we're in // the browser, add `_` as a global object via a string identifier, // for Closure Compiler "advanced" mode. if (typeof exports !== 'undefined') { if (typeof module !== 'undefined' && module.exports) { exports = module.exports = _; } exports._ = _; } else { root._ = _; } // Current version. _.VERSION = '1.5.2'; // Collection Functions // -------------------- // The cornerstone, an `each` implementation, aka `forEach`. // Handles objects with the built-in `forEach`, arrays, and raw objects. // Delegates to **ECMAScript 5**'s native `forEach` if available. var each = _.each = _.forEach = function(obj, iterator, context) { if (obj == null) return; if (nativeForEach && obj.forEach === nativeForEach) { obj.forEach(iterator, context); } else if (obj.length === +obj.length) { for (var i = 0, length = obj.length; i < length; i++) { if (iterator.call(context, obj[i], i, obj) === breaker) return; } } else { var keys = _.keys(obj); for (var i = 0, length = keys.length; i < length; i++) { if (iterator.call(context, obj[keys[i]], keys[i], obj) === breaker) return; } } }; // Return the results of applying the iterator to each element. // Delegates to **ECMAScript 5**'s native `map` if available. _.map = _.collect = function(obj, iterator, context) { var results = []; if (obj == null) return results; if (nativeMap && obj.map === nativeMap) return obj.map(iterator, context); each(obj, function(value, index, list) { results.push(iterator.call(context, value, index, list)); }); return results; }; var reduceError = 'Reduce of empty array with no initial value'; // **Reduce** builds up a single result from a list of values, aka `inject`, // or `foldl`. Delegates to **ECMAScript 5**'s native `reduce` if available. _.reduce = _.foldl = _.inject = function(obj, iterator, memo, context) { var initial = arguments.length > 2; if (obj == null) obj = []; if (nativeReduce && obj.reduce === nativeReduce) { if (context) iterator = _.bind(iterator, context); return initial ? obj.reduce(iterator, memo) : obj.reduce(iterator); } each(obj, function(value, index, list) { if (!initial) { memo = value; initial = true; } else { memo = iterator.call(context, memo, value, index, list); } }); if (!initial) throw new TypeError(reduceError); return memo; }; // The right-associative version of reduce, also known as `foldr`. // Delegates to **ECMAScript 5**'s native `reduceRight` if available. _.reduceRight = _.foldr = function(obj, iterator, memo, context) { var initial = arguments.length > 2; if (obj == null) obj = []; if (nativeReduceRight && obj.reduceRight === nativeReduceRight) { if (context) iterator = _.bind(iterator, context); return initial ? obj.reduceRight(iterator, memo) : obj.reduceRight(iterator); } var length = obj.length; if (length !== +length) { var keys = _.keys(obj); length = keys.length; } each(obj, function(value, index, list) { index = keys ? keys[--length] : --length; if (!initial) { memo = obj[index]; initial = true; } else { memo = iterator.call(context, memo, obj[index], index, list); } }); if (!initial) throw new TypeError(reduceError); return memo; }; // Return the first value which passes a truth test. Aliased as `detect`. _.find = _.detect = function(obj, iterator, context) { var result; any(obj, function(value, index, list) { if (iterator.call(context, value, index, list)) { result = value; return true; } }); return result; }; // Return all the elements that pass a truth test. // Delegates to **ECMAScript 5**'s native `filter` if available. // Aliased as `select`. _.filter = _.select = function(obj, iterator, context) { var results = []; if (obj == null) return results; if (nativeFilter && obj.filter === nativeFilter) return obj.filter(iterator, context); each(obj, function(value, index, list) { if (iterator.call(context, value, index, list)) results.push(value); }); return results; }; // Return all the elements for which a truth test fails. _.reject = function(obj, iterator, context) { return _.filter(obj, function(value, index, list) { return !iterator.call(context, value, index, list); }, context); }; // Determine whether all of the elements match a truth test. // Delegates to **ECMAScript 5**'s native `every` if available. // Aliased as `all`. _.every = _.all = function(obj, iterator, context) { iterator || (iterator = _.identity); var result = true; if (obj == null) return result; if (nativeEvery && obj.every === nativeEvery) return obj.every(iterator, context); each(obj, function(value, index, list) { if (!(result = result && iterator.call(context, value, index, list))) return breaker; }); return !!result; }; // Determine if at least one element in the object matches a truth test. // Delegates to **ECMAScript 5**'s native `some` if available. // Aliased as `any`. var any = _.some = _.any = function(obj, iterator, context) { iterator || (iterator = _.identity); var result = false; if (obj == null) return result; if (nativeSome && obj.some === nativeSome) return obj.some(iterator, context); each(obj, function(value, index, list) { if (result || (result = iterator.call(context, value, index, list))) return breaker; }); return !!result; }; // Determine if the array or object contains a given value (using `===`). // Aliased as `include`. _.contains = _.include = function(obj, target) { if (obj == null) return false; if (nativeIndexOf && obj.indexOf === nativeIndexOf) return obj.indexOf(target) != -1; return any(obj, function(value) { return value === target; }); }; // Invoke a method (with arguments) on every item in a collection. _.invoke = function(obj, method) { var args = slice.call(arguments, 2); var isFunc = _.isFunction(method); return _.map(obj, function(value) { return (isFunc ? method : value[method]).apply(value, args); }); }; // Convenience version of a common use case of `map`: fetching a property. _.pluck = function(obj, key) { return _.map(obj, function(value){ return value[key]; }); }; // Convenience version of a common use case of `filter`: selecting only objects // containing specific `key:value` pairs. _.where = function(obj, attrs, first) { if (_.isEmpty(attrs)) return first ? void 0 : []; return _[first ? 'find' : 'filter'](obj, function(value) { for (var key in attrs) { if (attrs[key] !== value[key]) return false; } return true; }); }; // Convenience version of a common use case of `find`: getting the first object // containing specific `key:value` pairs. _.findWhere = function(obj, attrs) { return _.where(obj, attrs, true); }; // Return the maximum element or (element-based computation). // Can't optimize arrays of integers longer than 65,535 elements. // See [WebKit Bug 80797](https://bugs.webkit.org/show_bug.cgi?id=80797) _.max = function(obj, iterator, context) { if (!iterator && _.isArray(obj) && obj[0] === +obj[0] && obj.length < 65535) { return Math.max.apply(Math, obj); } if (!iterator && _.isEmpty(obj)) return -Infinity; var result = {computed : -Infinity, value: -Infinity}; each(obj, function(value, index, list) { var computed = iterator ? iterator.call(context, value, index, list) : value; computed > result.computed && (result = {value : value, computed : computed}); }); return result.value; }; // Return the minimum element (or element-based computation). _.min = function(obj, iterator, context) { if (!iterator && _.isArray(obj) && obj[0] === +obj[0] && obj.length < 65535) { return Math.min.apply(Math, obj); } if (!iterator && _.isEmpty(obj)) return Infinity; var result = {computed : Infinity, value: Infinity}; each(obj, function(value, index, list) { var computed = iterator ? iterator.call(context, value, index, list) : value; computed < result.computed && (result = {value : value, computed : computed}); }); return result.value; }; // Shuffle an array, using the modern version of the // [Fisher-Yates shuffle](http://en.wikipedia.org/wiki/Fisher–Yates_shuffle). _.shuffle = function(obj) { var rand; var index = 0; var shuffled = []; each(obj, function(value) { rand = _.random(index++); shuffled[index - 1] = shuffled[rand]; shuffled[rand] = value; }); return shuffled; }; // Sample **n** random values from an array. // If **n** is not specified, returns a single random element from the array. // The internal `guard` argument allows it to work with `map`. _.sample = function(obj, n, guard) { if (arguments.length < 2 || guard) { return obj[_.random(obj.length - 1)]; } return _.shuffle(obj).slice(0, Math.max(0, n)); }; // An internal function to generate lookup iterators. var lookupIterator = function(value) { return _.isFunction(value) ? value : function(obj){ return obj[value]; }; }; // Sort the object's values by a criterion produced by an iterator. _.sortBy = function(obj, value, context) { var iterator = lookupIterator(value); return _.pluck(_.map(obj, function(value, index, list) { return { value: value, index: index, criteria: iterator.call(context, value, index, list) }; }).sort(function(left, right) { var a = left.criteria; var b = right.criteria; if (a !== b) { if (a > b || a === void 0) return 1; if (a < b || b === void 0) return -1; } return left.index - right.index; }), 'value'); }; // An internal function used for aggregate "group by" operations. var group = function(behavior) { return function(obj, value, context) { var result = {}; var iterator = value == null ? _.identity : lookupIterator(value); each(obj, function(value, index) { var key = iterator.call(context, value, index, obj); behavior(result, key, value); }); return result; }; }; // Groups the object's values by a criterion. Pass either a string attribute // to group by, or a function that returns the criterion. _.groupBy = group(function(result, key, value) { (_.has(result, key) ? result[key] : (result[key] = [])).push(value); }); // Indexes the object's values by a criterion, similar to `groupBy`, but for // when you know that your index values will be unique. _.indexBy = group(function(result, key, value) { result[key] = value; }); // Counts instances of an object that group by a certain criterion. Pass // either a string attribute to count by, or a function that returns the // criterion. _.countBy = group(function(result, key) { _.has(result, key) ? result[key]++ : result[key] = 1; }); // Use a comparator function to figure out the smallest index at which // an object should be inserted so as to maintain order. Uses binary search. _.sortedIndex = function(array, obj, iterator, context) { iterator = iterator == null ? _.identity : lookupIterator(iterator); var value = iterator.call(context, obj); var low = 0, high = array.length; while (low < high) { var mid = (low + high) >>> 1; iterator.call(context, array[mid]) < value ? low = mid + 1 : high = mid; } return low; }; // Safely create a real, live array from anything iterable. _.toArray = function(obj) { if (!obj) return []; if (_.isArray(obj)) return slice.call(obj); if (obj.length === +obj.length) return _.map(obj, _.identity); return _.values(obj); }; // Return the number of elements in an object. _.size = function(obj) { if (obj == null) return 0; return (obj.length === +obj.length) ? obj.length : _.keys(obj).length; }; // Array Functions // --------------- // Get the first element of an array. Passing **n** will return the first N // values in the array. Aliased as `head` and `take`. The **guard** check // allows it to work with `_.map`. _.first = _.head = _.take = function(array, n, guard) { if (array == null) return void 0; return (n == null) || guard ? array[0] : slice.call(array, 0, n); }; // Returns everything but the last entry of the array. Especially useful on // the arguments object. Passing **n** will return all the values in // the array, excluding the last N. The **guard** check allows it to work with // `_.map`. _.initial = function(array, n, guard) { return slice.call(array, 0, array.length - ((n == null) || guard ? 1 : n)); }; // Get the last element of an array. Passing **n** will return the last N // values in the array. The **guard** check allows it to work with `_.map`. _.last = function(array, n, guard) { if (array == null) return void 0; if ((n == null) || guard) { return array[array.length - 1]; } else { return slice.call(array, Math.max(array.length - n, 0)); } }; // Returns everything but the first entry of the array. Aliased as `tail` and `drop`. // Especially useful on the arguments object. Passing an **n** will return // the rest N values in the array. The **guard** // check allows it to work with `_.map`. _.rest = _.tail = _.drop = function(array, n, guard) { return slice.call(array, (n == null) || guard ? 1 : n); }; // Trim out all falsy values from an array. _.compact = function(array) { return _.filter(array, _.identity); }; // Internal implementation of a recursive `flatten` function. var flatten = function(input, shallow, output) { if (shallow && _.every(input, _.isArray)) { return concat.apply(output, input); } each(input, function(value) { if (_.isArray(value) || _.isArguments(value)) { shallow ? push.apply(output, value) : flatten(value, shallow, output); } else { output.push(value); } }); return output; }; // Flatten out an array, either recursively (by default), or just one level. _.flatten = function(array, shallow) { return flatten(array, shallow, []); }; // Return a version of the array that does not contain the specified value(s). _.without = function(array) { return _.difference(array, slice.call(arguments, 1)); }; // Produce a duplicate-free version of the array. If the array has already // been sorted, you have the option of using a faster algorithm. // Aliased as `unique`. _.uniq = _.unique = function(array, isSorted, iterator, context) { if (_.isFunction(isSorted)) { context = iterator; iterator = isSorted; isSorted = false; } var initial = iterator ? _.map(array, iterator, context) : array; var results = []; var seen = []; each(initial, function(value, index) { if (isSorted ? (!index || seen[seen.length - 1] !== value) : !_.contains(seen, value)) { seen.push(value); results.push(array[index]); } }); return results; }; // Produce an array that contains the union: each distinct element from all of // the passed-in arrays. _.union = function() { return _.uniq(_.flatten(arguments, true)); }; // Produce an array that contains every item shared between all the // passed-in arrays. _.intersection = function(array) { var rest = slice.call(arguments, 1); return _.filter(_.uniq(array), function(item) { return _.every(rest, function(other) { return _.indexOf(other, item) >= 0; }); }); }; // Take the difference between one array and a number of other arrays. // Only the elements present in just the first array will remain. _.difference = function(array) { var rest = concat.apply(ArrayProto, slice.call(arguments, 1)); return _.filter(array, function(value){ return !_.contains(rest, value); }); }; // Zip together multiple lists into a single array -- elements that share // an index go together. _.zip = function() { var length = _.max(_.pluck(arguments, "length").concat(0)); var results = new Array(length); for (var i = 0; i < length; i++) { results[i] = _.pluck(arguments, '' + i); } return results; }; // Converts lists into objects. Pass either a single array of `[key, value]` // pairs, or two parallel arrays of the same length -- one of keys, and one of // the corresponding values. _.object = function(list, values) { if (list == null) return {}; var result = {}; for (var i = 0, length = list.length; i < length; i++) { if (values) { result[list[i]] = values[i]; } else { result[list[i][0]] = list[i][1]; } } return result; }; // If the browser doesn't supply us with indexOf (I'm looking at you, **MSIE**), // we need this function. Return the position of the first occurrence of an // item in an array, or -1 if the item is not included in the array. // Delegates to **ECMAScript 5**'s native `indexOf` if available. // If the array is large and already in sort order, pass `true` // for **isSorted** to use binary search. _.indexOf = function(array, item, isSorted) { if (array == null) return -1; var i = 0, length = array.length; if (isSorted) { if (typeof isSorted == 'number') { i = (isSorted < 0 ? Math.max(0, length + isSorted) : isSorted); } else { i = _.sortedIndex(array, item); return array[i] === item ? i : -1; } } if (nativeIndexOf && array.indexOf === nativeIndexOf) return array.indexOf(item, isSorted); for (; i < length; i++) if (array[i] === item) return i; return -1; }; // Delegates to **ECMAScript 5**'s native `lastIndexOf` if available. _.lastIndexOf = function(array, item, from) { if (array == null) return -1; var hasIndex = from != null; if (nativeLastIndexOf && array.lastIndexOf === nativeLastIndexOf) { return hasIndex ? array.lastIndexOf(item, from) : array.lastIndexOf(item); } var i = (hasIndex ? from : array.length); while (i--) if (array[i] === item) return i; return -1; }; // Generate an integer Array containing an arithmetic progression. A port of // the native Python `range()` function. See // [the Python documentation](http://docs.python.org/library/functions.html#range). _.range = function(start, stop, step) { if (arguments.length <= 1) { stop = start || 0; start = 0; } step = arguments[2] || 1; var length = Math.max(Math.ceil((stop - start) / step), 0); var idx = 0; var range = new Array(length); while(idx < length) { range[idx++] = start; start += step; } return range; }; // Function (ahem) Functions // ------------------ // Reusable constructor function for prototype setting. var ctor = function(){}; // Create a function bound to a given object (assigning `this`, and arguments, // optionally). Delegates to **ECMAScript 5**'s native `Function.bind` if // available. _.bind = function(func, context) { var args, bound; if (nativeBind && func.bind === nativeBind) return nativeBind.apply(func, slice.call(arguments, 1)); if (!_.isFunction(func)) throw new TypeError; args = slice.call(arguments, 2); return bound = function() { if (!(this instanceof bound)) return func.apply(context, args.concat(slice.call(arguments))); ctor.prototype = func.prototype; var self = new ctor; ctor.prototype = null; var result = func.apply(self, args.concat(slice.call(arguments))); if (Object(result) === result) return result; return self; }; }; // Partially apply a function by creating a version that has had some of its // arguments pre-filled, without changing its dynamic `this` context. _.partial = function(func) { var args = slice.call(arguments, 1); return function() { return func.apply(this, args.concat(slice.call(arguments))); }; }; // Bind all of an object's methods to that object. Useful for ensuring that // all callbacks defined on an object belong to it. _.bindAll = function(obj) { var funcs = slice.call(arguments, 1); if (funcs.length === 0) throw new Error("bindAll must be passed function names"); each(funcs, function(f) { obj[f] = _.bind(obj[f], obj); }); return obj; }; // Memoize an expensive function by storing its results. _.memoize = function(func, hasher) { var memo = {}; hasher || (hasher = _.identity); return function() { var key = hasher.apply(this, arguments); return _.has(memo, key) ? memo[key] : (memo[key] = func.apply(this, arguments)); }; }; // Delays a function for the given number of milliseconds, and then calls // it with the arguments supplied. _.delay = function(func, wait) { var args = slice.call(arguments, 2); return setTimeout(function(){ return func.apply(null, args); }, wait); }; // Defers a function, scheduling it to run after the current call stack has // cleared. _.defer = function(func) { return _.delay.apply(_, [func, 1].concat(slice.call(arguments, 1))); }; // Returns a function, that, when invoked, will only be triggered at most once // during a given window of time. Normally, the throttled function will run // as much as it can, without ever going more than once per `wait` duration; // but if you'd like to disable the execution on the leading edge, pass // `{leading: false}`. To disable execution on the trailing edge, ditto. _.throttle = function(func, wait, options) { var context, args, result; var timeout = null; var previous = 0; options || (options = {}); var later = function() { previous = options.leading === false ? 0 : new Date; timeout = null; result = func.apply(context, args); }; return function() { var now = new Date; if (!previous && options.leading === false) previous = now; var remaining = wait - (now - previous); context = this; args = arguments; if (remaining <= 0) { clearTimeout(timeout); timeout = null; previous = now; result = func.apply(context, args); } else if (!timeout && options.trailing !== false) { timeout = setTimeout(later, remaining); } return result; }; }; // Returns a function, that, as long as it continues to be invoked, will not // be triggered. The function will be called after it stops being called for // N milliseconds. If `immediate` is passed, trigger the function on the // leading edge, instead of the trailing. _.debounce = function(func, wait, immediate) { var timeout, args, context, timestamp, result; return function() { context = this; args = arguments; timestamp = new Date(); var later = function() { var last = (new Date()) - timestamp; if (last < wait) { timeout = setTimeout(later, wait - last); } else { timeout = null; if (!immediate) result = func.apply(context, args); } }; var callNow = immediate && !timeout; if (!timeout) { timeout = setTimeout(later, wait); } if (callNow) result = func.apply(context, args); return result; }; }; // Returns a function that will be executed at most one time, no matter how // often you call it. Useful for lazy initialization. _.once = function(func) { var ran = false, memo; return function() { if (ran) return memo; ran = true; memo = func.apply(this, arguments); func = null; return memo; }; }; // Returns the first function passed as an argument to the second, // allowing you to adjust arguments, run code before and after, and // conditionally execute the original function. _.wrap = function(func, wrapper) { return function() { var args = [func]; push.apply(args, arguments); return wrapper.apply(this, args); }; }; // Returns a function that is the composition of a list of functions, each // consuming the return value of the function that follows. _.compose = function() { var funcs = arguments; return function() { var args = arguments; for (var i = funcs.length - 1; i >= 0; i--) { args = [funcs[i].apply(this, args)]; } return args[0]; }; }; // Returns a function that will only be executed after being called N times. _.after = function(times, func) { return function() { if (--times < 1) { return func.apply(this, arguments); } }; }; // Object Functions // ---------------- // Retrieve the names of an object's properties. // Delegates to **ECMAScript 5**'s native `Object.keys` _.keys = nativeKeys || function(obj) { if (obj !== Object(obj)) throw new TypeError('Invalid object'); var keys = []; for (var key in obj) if (_.has(obj, key)) keys.push(key); return keys; }; // Retrieve the values of an object's properties. _.values = function(obj) { var keys = _.keys(obj); var length = keys.length; var values = new Array(length); for (var i = 0; i < length; i++) { values[i] = obj[keys[i]]; } return values; }; // Convert an object into a list of `[key, value]` pairs. _.pairs = function(obj) { var keys = _.keys(obj); var length = keys.length; var pairs = new Array(length); for (var i = 0; i < length; i++) { pairs[i] = [keys[i], obj[keys[i]]]; } return pairs; }; // Invert the keys and values of an object. The values must be serializable. _.invert = function(obj) { var result = {}; var keys = _.keys(obj); for (var i = 0, length = keys.length; i < length; i++) { result[obj[keys[i]]] = keys[i]; } return result; }; // Return a sorted list of the function names available on the object. // Aliased as `methods` _.functions = _.methods = function(obj) { var names = []; for (var key in obj) { if (_.isFunction(obj[key])) names.push(key); } return names.sort(); }; // Extend a given object with all the properties in passed-in object(s). _.extend = function(obj) { each(slice.call(arguments, 1), function(source) { if (source) { for (var prop in source) { obj[prop] = source[prop]; } } }); return obj; }; // Return a copy of the object only containing the whitelisted properties. _.pick = function(obj) { var copy = {}; var keys = concat.apply(ArrayProto, slice.call(arguments, 1)); each(keys, function(key) { if (key in obj) copy[key] = obj[key]; }); return copy; }; // Return a copy of the object without the blacklisted properties. _.omit = function(obj) { var copy = {}; var keys = concat.apply(ArrayProto, slice.call(arguments, 1)); for (var key in obj) { if (!_.contains(keys, key)) copy[key] = obj[key]; } return copy; }; // Fill in a given object with default properties. _.defaults = function(obj) { each(slice.call(arguments, 1), function(source) { if (source) { for (var prop in source) { if (obj[prop] === void 0) obj[prop] = source[prop]; } } }); return obj; }; // Create a (shallow-cloned) duplicate of an object. _.clone = function(obj) { if (!_.isObject(obj)) return obj; return _.isArray(obj) ? obj.slice() : _.extend({}, obj); }; // Invokes interceptor with the obj, and then returns obj. // The primary purpose of this method is to "tap into" a method chain, in // order to perform operations on intermediate results within the chain. _.tap = function(obj, interceptor) { interceptor(obj); return obj; }; // Internal recursive comparison function for `isEqual`. var eq = function(a, b, aStack, bStack) { // Identical objects are equal. `0 === -0`, but they aren't identical. // See the [Harmony `egal` proposal](http://wiki.ecmascript.org/doku.php?id=harmony:egal). if (a === b) return a !== 0 || 1 / a == 1 / b; // A strict comparison is necessary because `null == undefined`. if (a == null || b == null) return a === b; // Unwrap any wrapped objects. if (a instanceof _) a = a._wrapped; if (b instanceof _) b = b._wrapped; // Compare `[[Class]]` names. var className = toString.call(a); if (className != toString.call(b)) return false; switch (className) { // Strings, numbers, dates, and booleans are compared by value. case '[object String]': // Primitives and their corresponding object wrappers are equivalent; thus, `"5"` is // equivalent to `new String("5")`. return a == String(b); case '[object Number]': // `NaN`s are equivalent, but non-reflexive. An `egal` comparison is performed for // other numeric values. return a != +a ? b != +b : (a == 0 ? 1 / a == 1 / b : a == +b); case '[object Date]': case '[object Boolean]': // Coerce dates and booleans to numeric primitive values. Dates are compared by their // millisecond representations. Note that invalid dates with millisecond representations // of `NaN` are not equivalent. return +a == +b; // RegExps are compared by their source patterns and flags. case '[object RegExp]': return a.source == b.source && a.global == b.global && a.multiline == b.multiline && a.ignoreCase == b.ignoreCase; } if (typeof a != 'object' || typeof b != 'object') return false; // Assume equality for cyclic structures. The algorithm for detecting cyclic // structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`. var length = aStack.length; while (length--) { // Linear search. Performance is inversely proportional to the number of // unique nested structures. if (aStack[length] == a) return bStack[length] == b; } // Objects with different constructors are not equivalent, but `Object`s // from different frames are. var aCtor = a.constructor, bCtor = b.constructor; if (aCtor !== bCtor && !(_.isFunction(aCtor) && (aCtor instanceof aCtor) && _.isFunction(bCtor) && (bCtor instanceof bCtor))) { return false; } // Add the first object to the stack of traversed objects. aStack.push(a); bStack.push(b); var size = 0, result = true; // Recursively compare objects and arrays. if (className == '[object Array]') { // Compare array lengths to determine if a deep comparison is necessary. size = a.length; result = size == b.length; if (result) { // Deep compare the contents, ignoring non-numeric properties. while (size--) { if (!(result = eq(a[size], b[size], aStack, bStack))) break; } } } else { // Deep compare objects. for (var key in a) { if (_.has(a, key)) { // Count the expected number of properties. size++; // Deep compare each member. if (!(result = _.has(b, key) && eq(a[key], b[key], aStack, bStack))) break; } } // Ensure that both objects contain the same number of properties. if (result) { for (key in b) { if (_.has(b, key) && !(size--)) break; } result = !size; } } // Remove the first object from the stack of traversed objects. aStack.pop(); bStack.pop(); return result; }; // Perform a deep comparison to check if two objects are equal. _.isEqual = function(a, b) { return eq(a, b, [], []); }; // Is a given array, string, or object empty? // An "empty" object has no enumerable own-properties. _.isEmpty = function(obj) { if (obj == null) return true; if (_.isArray(obj) || _.isString(obj)) return obj.length === 0; for (var key in obj) if (_.has(obj, key)) return false; return true; }; // Is a given value a DOM element? _.isElement = function(obj) { return !!(obj && obj.nodeType === 1); }; // Is a given value an array? // Delegates to ECMA5's native Array.isArray _.isArray = nativeIsArray || function(obj) { return toString.call(obj) == '[object Array]'; }; // Is a given variable an object? _.isObject = function(obj) { return obj === Object(obj); }; // Add some isType methods: isArguments, isFunction, isString, isNumber, isDate, isRegExp. each(['Arguments', 'Function', 'String', 'Number', 'Date', 'RegExp'], function(name) { _['is' + name] = function(obj) { return toString.call(obj) == '[object ' + name + ']'; }; }); // Define a fallback version of the method in browsers (ahem, IE), where // there isn't any inspectable "Arguments" type. if (!_.isArguments(arguments)) { _.isArguments = function(obj) { return !!(obj && _.has(obj, 'callee')); }; } // Optimize `isFunction` if appropriate. if (typeof (/./) !== 'function') { _.isFunction = function(obj) { return typeof obj === 'function'; }; } // Is a given object a finite number? _.isFinite = function(obj) { return isFinite(obj) && !isNaN(parseFloat(obj)); }; // Is the given value `NaN`? (NaN is the only number which does not equal itself). _.isNaN = function(obj) { return _.isNumber(obj) && obj != +obj; }; // Is a given value a boolean? _.isBoolean = function(obj) { return obj === true || obj === false || toString.call(obj) == '[object Boolean]'; }; // Is a given value equal to null? _.isNull = function(obj) { return obj === null; }; // Is a given variable undefined? _.isUndefined = function(obj) { return obj === void 0; }; // Shortcut function for checking if an object has a given property directly // on itself (in other words, not on a prototype). _.has = function(obj, key) { return hasOwnProperty.call(obj, key); }; // Utility Functions // ----------------- // Run Underscore.js in *noConflict* mode, returning the `_` variable to its // previous owner. Returns a reference to the Underscore object. _.noConflict = function() { root._ = previousUnderscore; return this; }; // Keep the identity function around for default iterators. _.identity = function(value) { return value; }; // Run a function **n** times. _.times = function(n, iterator, context) { var accum = Array(Math.max(0, n)); for (var i = 0; i < n; i++) accum[i] = iterator.call(context, i); return accum; }; // Return a random integer between min and max (inclusive). _.random = function(min, max) { if (max == null) { max = min; min = 0; } return min + Math.floor(Math.random() * (max - min + 1)); }; // List of HTML entities for escaping. var entityMap = { escape: { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' } }; entityMap.unescape = _.invert(entityMap.escape); // Regexes containing the keys and values listed immediately above. var entityRegexes = { escape: new RegExp('[' + _.keys(entityMap.escape).join('') + ']', 'g'), unescape: new RegExp('(' + _.keys(entityMap.unescape).join('|') + ')', 'g') }; // Functions for escaping and unescaping strings to/from HTML interpolation. _.each(['escape', 'unescape'], function(method) { _[method] = function(string) { if (string == null) return ''; return ('' + string).replace(entityRegexes[method], function(match) { return entityMap[method][match]; }); }; }); // If the value of the named `property` is a function then invoke it with the // `object` as context; otherwise, return it. _.result = function(object, property) { if (object == null) return void 0; var value = object[property]; return _.isFunction(value) ? value.call(object) : value; }; // Add your own custom functions to the Underscore object. _.mixin = function(obj) { each(_.functions(obj), function(name) { var func = _[name] = obj[name]; _.prototype[name] = function() { var args = [this._wrapped]; push.apply(args, arguments); return result.call(this, func.apply(_, args)); }; }); }; // Generate a unique integer id (unique within the entire client session). // Useful for temporary DOM ids. var idCounter = 0; _.uniqueId = function(prefix) { var id = ++idCounter + ''; return prefix ? prefix + id : id; }; // By default, Underscore uses ERB-style template delimiters, change the // following template settings to use alternative delimiters. _.templateSettings = { evaluate : /<%([\s\S]+?)%>/g, interpolate : /<%=([\s\S]+?)%>/g, escape : /<%-([\s\S]+?)%>/g }; // When customizing `templateSettings`, if you don't want to define an // interpolation, evaluation or escaping regex, we need one that is // guaranteed not to match. var noMatch = /(.)^/; // Certain characters need to be escaped so that they can be put into a // string literal. var escapes = { "'": "'", '\\': '\\', '\r': 'r', '\n': 'n', '\t': 't', '\u2028': 'u2028', '\u2029': 'u2029' }; var escaper = /\\|'|\r|\n|\t|\u2028|\u2029/g; // JavaScript micro-templating, similar to John Resig's implementation. // Underscore templating handles arbitrary delimiters, preserves whitespace, // and correctly escapes quotes within interpolated code. _.template = function(text, data, settings) { var render; settings = _.defaults({}, settings, _.templateSettings); // Combine delimiters into one regular expression via alternation. var matcher = new RegExp([ (settings.escape || noMatch).source, (settings.interpolate || noMatch).source, (settings.evaluate || noMatch).source ].join('|') + '|$', 'g'); // Compile the template source, escaping string literals appropriately. var index = 0; var source = "__p+='"; text.replace(matcher, function(match, escape, interpolate, evaluate, offset) { source += text.slice(index, offset) .replace(escaper, function(match) { return '\\' + escapes[match]; }); if (escape) { source += "'+\n((__t=(" + escape + "))==null?'':_.escape(__t))+\n'"; } if (interpolate) { source += "'+\n((__t=(" + interpolate + "))==null?'':__t)+\n'"; } if (evaluate) { source += "';\n" + evaluate + "\n__p+='"; } index = offset + match.length; return match; }); source += "';\n"; // If a variable is not specified, place data values in local scope. if (!settings.variable) source = 'with(obj||{}){\n' + source + '}\n'; source = "var __t,__p='',__j=Array.prototype.join," + "print=function(){__p+=__j.call(arguments,'');};\n" + source + "return __p;\n"; try { render = new Function(settings.variable || 'obj', '_', source); } catch (e) { e.source = source; throw e; } if (data) return render(data, _); var template = function(data) { return render.call(this, data, _); }; // Provide the compiled function source as a convenience for precompilation. template.source = 'function(' + (settings.variable || 'obj') + '){\n' + source + '}'; return template; }; // Add a "chain" function, which will delegate to the wrapper. _.chain = function(obj) { return _(obj).chain(); }; // OOP // --------------- // If Underscore is called as a function, it returns a wrapped object that // can be used OO-style. This wrapper holds altered versions of all the // underscore functions. Wrapped objects may be chained. // Helper function to continue chaining intermediate results. var result = function(obj) { return this._chain ? _(obj).chain() : obj; }; // Add all of the Underscore functions to the wrapper object. _.mixin(_); // Add all mutator Array functions to the wrapper. each(['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'], function(name) { var method = ArrayProto[name]; _.prototype[name] = function() { var obj = this._wrapped; method.apply(obj, arguments); if ((name == 'shift' || name == 'splice') && obj.length === 0) delete obj[0]; return result.call(this, obj); }; }); // Add all accessor Array functions to the wrapper. each(['concat', 'join', 'slice'], function(name) { var method = ArrayProto[name]; _.prototype[name] = function() { return result.call(this, method.apply(this._wrapped, arguments)); }; }); _.extend(_.prototype, { // Start chaining a wrapped Underscore object. chain: function() { this._chain = true; return this; }, // Extracts the result from a wrapped and chained object. value: function() { return this._wrapped; } }); }).call(this); (function() { // Copy and pasted straight out of Backbone 1.0.0 // We'll try and keep this updated to the latest var array = []; var slice = array.slice; // Backbone.Events // --------------- // A module that can be mixed in to *any object* in order to provide it with // custom events. You may bind with `on` or remove with `off` callback // functions to an event; `trigger`-ing an event fires all callbacks in // succession. // // var object = {}; // _.extend(object, Backbone.Events); // object.on('expand', function(){ alert('expanded'); }); // object.trigger('expand'); // var Eventable = { // Bind an event to a `callback` function. Passing `"all"` will bind // the callback to all events fired. on: function(name, callback, context) { if (!eventsApi(this, 'on', name, [callback, context]) || !callback) return this; this._events || (this._events = {}); var events = this._events[name] || (this._events[name] = []); events.push({callback: callback, context: context, ctx: context || this}); return this; }, // Bind an event to only be triggered a single time. After the first time // the callback is invoked, it will be removed. once: function(name, callback, context) { if (!eventsApi(this, 'once', name, [callback, context]) || !callback) return this; var self = this; var once = _.once(function() { self.off(name, once); callback.apply(this, arguments); }); once._callback = callback; return this.on(name, once, context); }, // Remove one or many callbacks. If `context` is null, removes all // callbacks with that function. If `callback` is null, removes all // callbacks for the event. If `name` is null, removes all bound // callbacks for all events. off: function(name, callback, context) { var retain, ev, events, names, i, l, j, k; if (!this._events || !eventsApi(this, 'off', name, [callback, context])) return this; if (!name && !callback && !context) { this._events = {}; return this; } names = name ? [name] : _.keys(this._events); for (i = 0, l = names.length; i < l; i++) { name = names[i]; if (events = this._events[name]) { this._events[name] = retain = []; if (callback || context) { for (j = 0, k = events.length; j < k; j++) { ev = events[j]; if ((callback && callback !== ev.callback && callback !== ev.callback._callback) || (context && context !== ev.context)) { retain.push(ev); } } } if (!retain.length) delete this._events[name]; } } return this; }, // Trigger one or many events, firing all bound callbacks. Callbacks are // passed the same arguments as `trigger` is, apart from the event name // (unless you're listening on `"all"`, which will cause your callback to // receive the true name of the event as the first argument). trigger: function(name) { if (!this._events) return this; var args = slice.call(arguments, 1); if (!eventsApi(this, 'trigger', name, args)) return this; var events = this._events[name]; var allEvents = this._events.all; if (events) triggerEvents(events, args); if (allEvents) triggerEvents(allEvents, arguments); return this; }, // Tell this object to stop listening to either specific events ... or // to every object it's currently listening to. stopListening: function(obj, name, callback) { var listeners = this._listeners; if (!listeners) return this; var deleteListener = !name && !callback; if (typeof name === 'object') callback = this; if (obj) (listeners = {})[obj._listenerId] = obj; for (var id in listeners) { listeners[id].off(name, callback, this); if (deleteListener) delete this._listeners[id]; } return this; } }; // Regular expression used to split event strings. var eventSplitter = /\s+/; // Implement fancy features of the Events API such as multiple event // names `"change blur"` and jQuery-style event maps `{change: action}` // in terms of the existing API. var eventsApi = function(obj, action, name, rest) { if (!name) return true; // Handle event maps. if (typeof name === 'object') { for (var key in name) { obj[action].apply(obj, [key, name[key]].concat(rest)); } return false; } // Handle space separated event names. if (eventSplitter.test(name)) { var names = name.split(eventSplitter); for (var i = 0, l = names.length; i < l; i++) { obj[action].apply(obj, [names[i]].concat(rest)); } return false; } return true; }; // A difficult-to-believe, but optimized internal dispatch function for // triggering events. Tries to keep the usual cases speedy (most internal // Backbone events have 3 arguments). var triggerEvents = function(events, args) { var ev, i = -1, l = events.length, a1 = args[0], a2 = args[1], a3 = args[2]; switch (args.length) { case 0: while (++i < l) (ev = events[i]).callback.call(ev.ctx); return; case 1: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1); return; case 2: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2); return; case 3: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2, a3); return; default: while (++i < l) (ev = events[i]).callback.apply(ev.ctx, args); } }; var listenMethods = {listenTo: 'on', listenToOnce: 'once'}; // Inversion-of-control versions of `on` and `once`. Tell *this* object to // listen to an event in another object ... keeping track of what it's // listening to. _.each(listenMethods, function(implementation, method) { Eventable[method] = function(obj, name, callback) { var listeners = this._listeners || (this._listeners = {}); var id = obj._listenerId || (obj._listenerId = _.uniqueId('l')); listeners[id] = obj; if (typeof name === 'object') callback = this; obj[implementation](name, callback, this); return this; }; }); // Aliases for backwards compatibility. Eventable.bind = Eventable.on; Eventable.unbind = Eventable.off; if (typeof define !== "undefined" && typeof define === "function" && define.amd) { define( "eventable", [], function () { return Eventable; } ); } this.Eventable = Eventable; })(); /*! * Sir Trevor JS v0.3.2 * * Released under the MIT license * www.opensource.org/licenses/MIT * * 2013-12-20 */ (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" }, 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) { // Minimal i18n stub that only reads the English strings SirTrevor.log("Using i18n stub"); window.i18n = { t: function(key, options) { var parts = key.split(':'), str, obj, part, i; obj = SirTrevor.Locales[SirTrevor.LANGUAGE]; for(i = 0; i < parts.length; i++) { part = parts[i]; if(!_.isUndefined(obj[part])) { obj = obj[part]; } } str = obj; if (!_.isString(str)) { return ""; } if (str.indexOf('__') >= 0) { _.each(options, function(value, opt) { str = str.replace('__' + opt + '__', value); }); } return str; } }; } else { SirTrevor.log("Using i18next"); // Only use i18next when the library has been loaded by the user, keeps // dependencies slim i18n.init({ resStore: SirTrevor.Locales, fallbackLng: SirTrevor.LANGUAGE, ns: { namespaces: ['general', 'blocks'], defaultNs: 'general' } }); } //fgnass.github.com/spin.js#v1.2.5 (function(a,b,c){function g(a,c){var d=b.createElement(a||"div"),e;for(e in c)d[e]=c[e];return d}function h(a){for(var b=1,c=arguments.length;b>1):c.left+e)+"px",top:(c.top=="auto"?i.y-h.y+(a.offsetHeight>>1):c.top+e)+"px"})),d.setAttribute("aria-role","progressbar"),b.lines(d,b.opts);if(!f){var j=0,k=c.fps,m=k/c.speed,o=(1-c.opacity)/(m*c.trail/100),p=m/c.lines;!function q(){j++;for(var a=c.lines;a;a--){var e=Math.max(1-(j+a*p)%m*o,c.opacity);b.opacity(d,c.lines-a,e,c)}b.timeout=b.el&&setTimeout(q,~~(1e3/k))}()}return b},stop:function(){var a=this.el;return a&&(clearTimeout(this.timeout),a.parentNode&&a.parentNode.removeChild(a),this.el=c),this},lines:function(a,b){function e(a,d){return l(g(),{position:"absolute",width:b.length+b.width+"px",height:b.width+"px",background:a,boxShadow:d,transformOrigin:"left",transform:"rotate("+~~(360/b.lines*c+b.rotate)+"deg) translate("+b.radius+"px"+",0)",borderRadius:(b.width>>1)+"px"})}var c=0,d;for(;c',b)}var b=l(g("group"),{behavior:"url(#default#VML)"});!k(b,"transform")&&b.adj?(i.addRule(".spin-vml","behavior:url(#default#VML)"),p.prototype.lines=function(b,c){function f(){return l(a("group",{coordsize:e+" "+e,coordorigin:-d+" "+ -d}),{width:e,height:e})}function k(b,e,g){h(i,h(l(f(),{rotation:360/c.lines*b+"deg",left:~~e}),h(l(a("roundrect",{arcsize:1}),{width:d,height:c.width,left:c.radius,top:-c.width>>1,filter:g}),a("fill",{color:c.color,opacity:c.opacity}),a("stroke",{opacity:0}))))}var d=c.length+c.width,e=2*d,g=-(c.width+c.length)*2+"px",i=l(f(),{position:"absolute",top:g,left:g}),j;if(c.shadow)for(j=1;j<=c.lines;j++)k(j,-2,"progid:DXImageTransform.Microsoft.Blur(pixelradius=2,makeshadow=1,shadowopacity=.3)");for(j=1;j<=c.lines;j++)k(j);return h(b,i)},p.prototype.opacity=function(a,b,c,d){var e=a.firstChild;d=d.shadow&&d.lines||0,e&&b+d 0) { try { // Ensure the JSON string has a data element that's an array var str = JSON.parse(content); if (!_.isUndefined(str.data)) { // Set it editor.dataStore = str; } } catch(e) { editor.errors.push({ text: i18n.t("errors:load_fail") }); editor.renderErrors(); console.log('Sorry there has been a problem with parsing the JSON'); console.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. */ var Submittable = function(){ this.intialize(); }; _.extend(Submittable.prototype, { intialize: function(){ this.submitBtn = $("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); } }); SirTrevor.submittable = function(){ new Submittable(); }; /* * Sir Trevor Uploader * Generic Upload implementation that can be extended for blocks */ SirTrevor.fileUploader = function(block, file, success, error) { SirTrevor.EventBus.trigger("onUploadStart"); 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'); SirTrevor.EventBus.trigger("onUploadStop"); if (!_.isUndefined(success) && _.isFunction(success)) { _.bind(success, block)(data); } }; var callbackError = function(jqXHR, status, errorThrown){ SirTrevor.log('Upload callback error called'); SirTrevor.EventBus.trigger("onUploadStop"); if (!_.isUndefined(error) && _.isFunction(error)) { _.bind(error, block)(status); } }; var xhr = $.ajax({ url: SirTrevor.DEFAULTS.uploadUrl, data: data, cache: false, contentType: false, processData: false, dataType: 'json', type: 'POST' }); block.addQueuedItem(uid, xhr); xhr.done(callbackSuccess) .fail(callbackError) .always(_.bind(block.removeQueuedItem, block, uid)); return xhr; }; /* Underscore helpers */ var url_regex = /^(?:([A-Za-z]+):)?(\/{0,3})([0-9.\-A-Za-z]+)(?::(\d+))?(?:\/([^?#]*))?(?:\?([^#]*))?(?:#(.*))?$/; _.mixin({ isURI : function(string) { return (url_regex.test(string)); }, titleize: function(str){ if (str === null) return ''; str = String(str).toLowerCase(); return str.replace(/(?:^|\s|-)\S/g, function(c){ return c.toUpperCase(); }); }, classify: function(str){ return _.titleize(String(str).replace(/[\W_]/g, ' ')).replace(/\s/g, ''); }, classifyList: function(a){ return _.map(a, function(i){ return _.classify(i); }); }, capitalize : function(string) { return string.charAt(0).toUpperCase() + string.substring(1).toLowerCase(); }, underscored: function(str){ return _.trim(str).replace(/([a-z\d])([A-Z]+)/g, '$1_$2') .replace(/[-\s]+/g, '_').toLowerCase(); }, trim : function(string) { return string.replace(/^\s\s*/, '').replace(/\s\s*$/, ''); }, reverse: function(str) { return str.split("").reverse().join(""); }, flattern: function(obj) { var x = {}; _.each(obj, function(a,b) { x[(_.isArray(obj)) ? a : b] = true; }); return x; }, to_slug: function(str) { return str .toLowerCase() .replace(/[^\w ]+/g,'') .replace(/ +/g,'-'); } }); SirTrevor.toHTML = function(markdown, type) { // MD -> HTML type = _.classify(type); var html = markdown, shouldWrap = type === "Text"; if(_.isUndefined(shouldWrap)) { shouldWrap = false; } if (shouldWrap) { html = "
" + html; } html = html.replace(/\[([^\]]+)\]\(([^\)]+)\)/gm,function(match, p1, p2){ return ""+p1.replace(/\n/g, '')+""; }); // This may seem crazy, but because JS doesn't have a look behind, // we reverse the string to regex out the italic items (and bold) // and look for something that doesn't start (or end in the reversed strings case) // with a slash. html = _.reverse( _.reverse(html) .replace(/_(?!\\)((_\\|[^_])*)_(?=$|[^\\])/gm, function(match, p1) { return ">i/<"+ p1.replace(/\n/g, '').replace(/[\s]+$/,'') +">i<"; }) .replace(/\*\*(?!\\)((\*\*\\|[^\*\*])*)\*\*(?=$|[^\\])/gm, function(match, p1){ return ">b/<"+ p1.replace(/\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(/\n\n/gm, "

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

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

'].join('\n'), re_render_on_reorder: false }; var paste_options = { html: ['"', ' class="st-block__paste-input st-paste-block">'].join('') }; var upload_options = { html: [ '
', '', '', '
' ].join('\n') }; SirTrevor.DEFAULTS.Block = { drop_options: drop_options, paste_options: paste_options, upload_options: upload_options }; _.extend(Block.prototype, SirTrevor.SimpleBlock.fn, SirTrevor.BlockValidations, { bound: ["_handleContentPaste", "_onFocus", "_onBlur", "onDrop", "onDeleteClick", "clearInsertedStyles", "getSelectionForFormatter", "onBlockRender"], className: 'st-block st-icon--add', attributes: function() { return _.extend(SirTrevor.SimpleBlock.fn.attributes.call(this), { 'data-icon-after' : "add" }); }, icon_name: 'default', validationFailMsg: function() { return i18n.t('errors:validation_fail', { type: this.title() }); }, editorHTML: '
', toolbarEnabled: true, droppable: false, pastable: false, uploadable: false, fetchable: false, ajaxable: false, drop_options: {}, paste_options: {}, upload_options: {}, formattable: true, _previousSelection: '', initialize: function() {}, toMarkdown: function(markdown){ return markdown; }, toHTML: function(html){ return html; }, withMixin: function(mixin) { if (!_.isObject(mixin)) { return; } var initializeMethod = "initialize" + mixin.mixinName; if (_.isUndefined(this[initializeMethod])) { _.extend(this, mixin); this[initializeMethod](); } }, render: function() { this.beforeBlockRender(); this._setBlockInner(); this.$editor = this.$inner.children().first(); if(this.droppable || this.pastable || this.uploadable) { var input_html = $("
", { 'class': 'st-block__inputs' }); this.$inner.append(input_html); this.$inputs = input_html; } if (this.hasTextBlock) { this._initTextBlocks(); } if (this.droppable) { this.withMixin(SirTrevor.BlockMixins.Droppable); } if (this.pastable) { this.withMixin(SirTrevor.BlockMixins.Pastable); } if (this.uploadable) { this.withMixin(SirTrevor.BlockMixins.Uploadable); } if (this.fetchable) { this.withMixin(SirTrevor.BlockMixins.Fetchable); } if (this.controllable) { this.withMixin(SirTrevor.BlockMixins.Controllable); } if (this.formattable) { this._initFormatting(); } this._blockPrepare(); return this; }, remove: function() { if (this.ajaxable) { this.resolveAllInQueue(); } this.$el.remove(); }, loading: function() { if(!_.isUndefined(this.spinner)) { this.ready(); } this.spinner = new Spinner(SirTrevor.DEFAULTS.spinner); this.spinner.spin(this.$el[0]); this.$el.addClass('st--is-loading'); }, ready: function() { this.$el.removeClass('st--is-loading'); if (!_.isUndefined(this.spinner)) { this.spinner.stop(); delete this.spinner; } }, /* Generic toData implementation. Can be overwritten, although hopefully this will cover most situations */ toData: function() { SirTrevor.log("toData for " + this.blockID); var bl = this.$el, dataObj = {}; /* Simple to start. Add conditions later */ if (this.hasTextBlock()) { var content = this.getTextBlock().html(); if (content.length > 0) { dataObj.text = SirTrevor.toMarkdown(content, this.type); } } // Add any inputs to the data attr if(this.$(':input').not('.st-paste-block').length > 0) { this.$(':input').each(function(index,input){ if (input.getAttribute('name')) { dataObj[input.getAttribute('name')] = input.value; } }); } // Set if(!_.isEmpty(dataObj)) { this.setData(dataObj); } }, /* Generic implementation to tell us when the block is active */ focus: function() { this.getTextBlock().focus(); }, blur: function() { this.getTextBlock().blur(); }, onFocus: function() { this.getTextBlock().bind('focus', this._onFocus); }, onBlur: function() { this.getTextBlock().bind('blur', this._onBlur); }, /* * Event handlers */ _onFocus: function() { this.trigger('blockFocus', this.$el); }, _onBlur: function() {}, onDrop: function(dataTransferObj) {}, onDeleteClick: function(ev) { ev.preventDefault(); var onDeleteConfirm = function(e) { e.preventDefault(); this.trigger('removeBlock', this.blockID); }; var onDeleteDeny = function(e) { e.preventDefault(); this.$el.removeClass('st-block--delete-active'); $delete_el.remove(); }; if (this.isEmpty()) { onDeleteConfirm.call(this, new Event('click')); return; } this.$inner.append(_.template(delete_template)); this.$el.addClass('st-block--delete-active'); var $delete_el = this.$inner.find('.st-block__ui-delete-controls'); this.$inner.on('click', '.st-block-ui-btn--confirm-delete', _.bind(onDeleteConfirm, this)) .on('click', '.st-block-ui-btn--deny-delete', _.bind(onDeleteDeny, this)); }, pastedMarkdownToHTML: function(content) { return SirTrevor.toHTML(SirTrevor.toMarkdown(content, this.type), this.type); }, onContentPasted: function(event, target){ target.html(this.pastedMarkdownToHTML(target[0].innerHTML)); this.getTextBlock().caretToEnd(); }, beforeLoadingData: function() { this.loading(); if(this.droppable || this.uploadable || this.pastable) { this.$editor.show(); this.$inputs.hide(); } SirTrevor.SimpleBlock.fn.beforeLoadingData.call(this); this.ready(); }, _handleContentPaste: function(ev) { var target = $(ev.currentTarget); _.delay(_.bind(this.onContentPasted, this, ev, target), 0); }, _getBlockClass: function() { return 'st-block--' + this.className; }, /* * Init functions for adding functionality */ _initUIComponents: function() { var positioner = new SirTrevor.BlockPositioner(this.$el, this.instanceID); this._withUIComponent( positioner, '.st-block-ui-btn--reorder', positioner.toggle ); this._withUIComponent( new SirTrevor.BlockReorder(this.$el) ); this._withUIComponent( new SirTrevor.BlockDeletion(), '.st-block-ui-btn--delete', this.onDeleteClick ); this.onFocus(); this.onBlur(); }, _initFormatting: function() { // Enable formatting keyboard input var formatter; for (var name in SirTrevor.Formatters) { if (SirTrevor.Formatters.hasOwnProperty(name)) { formatter = SirTrevor.Formatters[name]; if (!_.isUndefined(formatter.keyCode)) { formatter._bindToBlock(this.$el); } } } }, _initTextBlocks: function() { this.getTextBlock() .bind('paste', this._handleContentPaste) .bind('keyup', this.getSelectionForFormatter) .bind('mouseup', this.getSelectionForFormatter) .bind('DOMNodeInserted', this.clearInsertedStyles); }, getSelectionForFormatter: function() { _.defer(function(){ var selection = window.getSelection(), selectionStr = selection.toString().trim(); if (selectionStr === '') { SirTrevor.EventBus.trigger('formatter:hide'); } else { SirTrevor.EventBus.trigger('formatter:positon'); } }); }, clearInsertedStyles: function(e) { var target = e.target; target.removeAttribute('style'); // Hacky fix for Chrome. }, hasTextBlock: function() { return this.getTextBlock().length > 0; }, getTextBlock: function() { if (_.isUndefined(this.text_block)) { this.text_block = this.$('.st-text-block'); } return this.text_block; }, isEmpty: function() { return _.isEmpty(this.saveAndGetData()); } }); Block.extend = extend; // Allow our Block to be extended. return Block; })(); SirTrevor.Formatter = (function(){ var Format = function(options){ this.formatId = _.uniqueId('format-'); this._configure(options || {}); this.initialize.apply(this, arguments); }; var formatOptions = ["title", "className", "cmd", "keyCode", "param", "onClick", "toMarkdown", "toHTML"]; _.extend(Format.prototype, { title: '', className: '', cmd: null, keyCode: null, param: null, toMarkdown: function(markdown){ return markdown; }, toHTML: function(html){ return html; }, initialize: function(){}, _configure: function(options) { if (this.options) options = _.extend({}, this.options, options); for (var i = 0, l = formatOptions.length; i < l; i++) { var attr = formatOptions[i]; if (options[attr]) this[attr] = options[attr]; } this.options = options; }, isActive: function() { return document.queryCommandState(this.cmd); }, _bindToBlock: function(block) { var formatter = this, ctrlDown = false; block .on('keyup','.st-text-block', function(ev) { if(ev.which == 17 || ev.which == 224 || ev.which == 91) { ctrlDown = false; } }) .on('keydown','.st-text-block', { formatter: formatter }, function(ev) { if(ev.which == 17 || ev.which == 224 || ev.which == 91) { ctrlDown = true; } if(ev.which == ev.data.formatter.keyCode && ctrlDown === true) { document.execCommand(ev.data.formatter.cmd, false, true); ev.preventDefault(); ctrlDown = false; } }); } }); Format.extend = extend; // Allow our Formatters to be extended. return Format; })(); /* Default Blocks */ /* Block Quote */ SirTrevor.Blocks.Quote = (function(){ var template = _.template([ '
', '', '"', ' class="st-input-string st-required js-cite-input" type="text" />' ].join("\n")); return SirTrevor.Block.extend({ type: "quote", title: function(){ return i18n.t('blocks:quote:title'); }, icon_name: 'quote', editorHTML: function() { return template(this); }, loadData: function(data){ this.getTextBlock().html(SirTrevor.toHTML(data.text, this.type)); this.$('.js-cite-input').val(data.cite); }, toMarkdown: function(markdown) { return markdown.replace(/^(.+)$/mg,"> $1"); } }); })(); /* Heading Block */ SirTrevor.Blocks.Heading = SirTrevor.Block.extend({ type: 'Heading', title: function(){ return i18n.t('blocks:heading:title'); }, editorHTML: '
', icon_name: 'heading', loadData: function(data){ this.getTextBlock().html(SirTrevor.toHTML(data.text, this.type)); } }); /* Simple Image Block */ SirTrevor.Blocks.Image = SirTrevor.Block.extend({ type: "image", title: function() { return i18n.t('blocks:image:title'); }, droppable: true, uploadable: true, icon_name: 'image', loadData: function(data){ // Create our image tag this.$editor.html($('', { src: data.file.url })); }, onBlockRender: function(){ /* Setup the upload button */ this.$inputs.find('button').bind('click', function(ev){ ev.preventDefault(); }); this.$inputs.find('input').on('change', _.bind(function(ev){ this.onDrop(ev.currentTarget); }, this)); }, onDrop: function(transferData){ var file = transferData.files[0], urlAPI = (typeof URL !== "undefined") ? URL : (typeof webkitURL !== "undefined") ? webkitURL : null; // Handle one upload at a time if (/image/.test(file.type)) { this.loading(); // Show this image on here this.$inputs.hide(); this.$editor.html($('', { src: urlAPI.createObjectURL(file) })).show(); // Upload! SirTrevor.EventBus.trigger('setSubmitButton', ['Please wait...']); this.uploader( file, function(data) { this.setData(data); this.ready(); }, function(error){ this.addMessage(i18n.t('blocks:image:upload_error')); this.ready(); } ); } } }); /* Text Block */ SirTrevor.Blocks.Text = SirTrevor.Block.extend({ type: "text", title: function() { return i18n.t('blocks:text:title'); }, editorHTML: '
', icon_name: 'text', loadData: function(data){ this.getTextBlock().html(SirTrevor.toHTML(data.text, this.type)); } }); SirTrevor.Blocks.Tweet = (function(){ var tweet_template = _.template([ "", '' ].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 = '
'; 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("
    " + SirTrevor.toHTML(data.text, this.type) + "
"); }, 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,"
  • $1
  • ") .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: "" }, youtube: { regex: /(?:http[s]?:\/\/)?(?:www.)?(?:(?:youtube.com\/watch\?(?:.*)(?:v=))|(?:youtu.be\/))([^&].+)/, html: "" } }, type: 'video', title: function() { return i18n.t('blocks:video:title'); }, droppable: true, pastable: true, icon_name: 'video', loadData: function(data){ if (!this.providers.hasOwnProperty(data.source)) { return; } if (this.providers[data.source].square) { this.$editor.addClass('st-block__editor--with-square-media'); } else { this.$editor.addClass('st-block__editor--with-sixteen-by-nine-media'); } var embed_string = this.providers[data.source].html .replace('{{protocol}}', window.location.protocol) .replace('{{remote_id}}', data.remote_id) .replace('{{width}}', this.$editor.width()); // for videos that can't resize automatically like vine this.$editor.html(embed_string); }, onContentPasted: function(event){ this.handleDropPaste($(event.target).val()); }, handleDropPaste: function(url){ if(!_.isURI(url)) { return; } var match, data; _.each(this.providers, function(provider, index) { match = provider.regex.exec(url); if(match !== null && !_.isUndefined(match[1])) { data = { source: index, remote_id: match[1] }; this.setAndLoadData(data); } }, this); }, onDrop: function(transferData){ var url = transferData.getData('text/plain'); this.handleDropPaste(url); } }); })(); /* Default Formatters */ /* Our base formatters */ (function(){ var Bold = SirTrevor.Formatter.extend({ title: "bold", cmd: "bold", keyCode: 66, text : "B" }); var Italic = SirTrevor.Formatter.extend({ title: "italic", cmd: "italic", keyCode: 73, text : "i" }); var Link = SirTrevor.Formatter.extend({ title: "link", iconName: "link", cmd: "CreateLink", text : "link", onClick: function() { var link = prompt(i18n.t("general:link")), link_regex = /((ftp|http|https):\/\/.)|mailto(?=\:[-\.\w]+@)/; if(link && link.length > 0) { if (!link_regex.test(link)) { link = "http://" + link; } document.execCommand(this.cmd, false, link); } }, isActive: function() { var selection = window.getSelection(), node; if (selection.rangeCount > 0) { node = selection.getRangeAt(0) .startContainer .parentNode; } return (node && node.nodeName == "A"); } }); var UnLink = SirTrevor.Formatter.extend({ title: "unlink", iconName: "link", cmd: "unlink", text : "link" }); /* Create our formatters and add a static reference to them */ SirTrevor.Formatters.Bold = new Bold(); SirTrevor.Formatters.Italic = new Italic(); SirTrevor.Formatters.Link = new Link(); SirTrevor.Formatters.Unlink = new UnLink(); })(); /* Marker */ SirTrevor.BlockControl = (function(){ var BlockControl = function(type, instance_scope) { this.type = type; this.instance_scope = instance_scope; this.block_type = SirTrevor.Blocks[this.type].prototype; this.can_be_rendered = this.block_type.toolbarEnabled; this._ensureElement(); }; _.extend(BlockControl.prototype, FunctionBind, Renderable, SirTrevor.Events, { tagName: 'a', className: "st-block-control", attributes: function() { return { 'data-type': this.block_type.type }; }, render: function() { this.$el.html(''+ _.result(this.block_type, 'icon_name') +'' + _.result(this.block_type, 'title')); return this; } }); return BlockControl; })(); /* SirTrevor Block Controls -- Gives an interface for adding new Sir Trevor blocks. */ SirTrevor.BlockControls = (function(){ var BlockControls = function(available_types, instance_scope) { this.instance_scope = instance_scope; this.available_types = available_types || []; this._ensureElement(); this._bindFunctions(); this.initialize(); }; _.extend(BlockControls.prototype, FunctionBind, Renderable, SirTrevor.Events, { bound: ['handleControlButtonClick'], block_controls: null, className: "st-block-controls", html: "" + i18n.t("general:close") + "", initialize: function() { for(var block_type in this.available_types) { if (SirTrevor.Blocks.hasOwnProperty(block_type)) { var block_control = new SirTrevor.BlockControl(block_type, this.instance_scope); if (block_control.can_be_rendered) { this.$el.append(block_control.render().$el); } } } this.$el.delegate('.st-block-control', 'click', this.handleControlButtonClick); }, show: function() { this.$el.addClass('st-block-controls--active'); SirTrevor.EventBus.trigger('block:controls:shown'); }, hide: function() { this.$el.removeClass('st-block-controls--active'); SirTrevor.EventBus.trigger('block:controls:hidden'); }, handleControlButtonClick: function(e) { e.stopPropagation(); this.trigger('createBlock', $(e.currentTarget).attr('data-type')); } }); 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(); 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 = $("