sir-trevor-rails 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. data/.gitignore +1 -0
  2. data/Gemfile +2 -0
  3. data/Gemfile.lock +101 -0
  4. data/MIT-LICENCE +20 -0
  5. data/README.md +120 -0
  6. data/Rakefile +10 -0
  7. data/app/assets/images/sir-trevor/icons/block_editor_blockquote.png +0 -0
  8. data/app/assets/images/sir-trevor/icons/block_editor_embed.png +0 -0
  9. data/app/assets/images/sir-trevor/icons/block_editor_image.png +0 -0
  10. data/app/assets/images/sir-trevor/icons/block_editor_list.png +0 -0
  11. data/app/assets/images/sir-trevor/icons/block_editor_text.png +0 -0
  12. data/app/assets/images/sir-trevor/icons/block_editor_tweet.png +0 -0
  13. data/app/assets/images/sir-trevor/icons/block_editor_video.png +0 -0
  14. data/app/assets/images/sir-trevor/icons/close.gif +0 -0
  15. data/app/assets/images/sir-trevor/icons/handle.gif +0 -0
  16. data/app/assets/images/sir-trevor/icons/new_image.png +0 -0
  17. data/app/assets/images/sir-trevor/icons/new_tweet.png +0 -0
  18. data/app/assets/images/sir-trevor/icons/new_video.png +0 -0
  19. data/app/assets/images/sir-trevor/icons/quote.png +0 -0
  20. data/app/assets/images/sir-trevor/placeholders/placeholder.jpg +0 -0
  21. data/app/assets/images/sir-trevor/placeholders/post_placeholder.jpg +0 -0
  22. data/app/assets/images/sir-trevor/placeholders/thumbnail_placeholder.jpg +0 -0
  23. data/app/assets/javascript/sir-trevor.js +2 -0
  24. data/app/assets/javascript/sir-trevor/libs/underscore.js +32 -0
  25. data/app/assets/javascript/sir-trevor/sir-trevor.js +2363 -0
  26. data/app/assets/stylesheets/sir-trevor.css +4 -0
  27. data/app/assets/stylesheets/sir-trevor/icons.css.erb +47 -0
  28. data/app/assets/stylesheets/sir-trevor/sir-trevor.css +551 -0
  29. data/app/views/sir-trevor/blocks/_gallery_block.html.erb +6 -0
  30. data/app/views/sir-trevor/blocks/_image_block.html.erb +3 -0
  31. data/app/views/sir-trevor/blocks/_quote_block.html.erb +4 -0
  32. data/app/views/sir-trevor/blocks/_text_block.html.erb +3 -0
  33. data/app/views/sir-trevor/blocks/_tweet_block.html.erb +6 -0
  34. data/app/views/sir-trevor/blocks/_ul_block.html.erb +3 -0
  35. data/app/views/sir-trevor/blocks/_video_block.html.erb +7 -0
  36. data/config/initializers/validators.rb +10 -0
  37. data/lib/generators/sir_trevor/block/block_generator.rb +37 -0
  38. data/lib/generators/sir_trevor/block/templates/_block.css.erb +7 -0
  39. data/lib/generators/sir_trevor/block/templates/_block.html.erb +3 -0
  40. data/lib/generators/sir_trevor/block/templates/_block.js +122 -0
  41. data/lib/generators/sir_trevor/block/templates/_block.png +0 -0
  42. data/lib/generators/sir_trevor/views/views_generator.rb +17 -0
  43. data/lib/sir-trevor-rails.rb +19 -0
  44. data/lib/sir-trevor/engine.rb +29 -0
  45. data/lib/sir-trevor/helpers/form_builder.rb +11 -0
  46. data/lib/sir-trevor/helpers/form_helper.rb +29 -0
  47. data/lib/sir-trevor/helpers/view_helper.rb +78 -0
  48. data/lib/sir-trevor/version.rb +3 -0
  49. data/sir-trevor-rails.gemspec +29 -0
  50. metadata +174 -0
data/.gitignore ADDED
@@ -0,0 +1 @@
1
+ .DS_Store
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source "http://rubygems.org"
2
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,101 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ sir-trevor-rails (0.0.1)
5
+ activesupport (>= 3.0.7)
6
+ jquery-rails
7
+ rails (>= 3.0.7)
8
+ redcarpet (~> 2.0.1)
9
+ twitter-text (~> 1.4)
10
+
11
+ GEM
12
+ remote: http://rubygems.org/
13
+ specs:
14
+ actionmailer (3.2.8)
15
+ actionpack (= 3.2.8)
16
+ mail (~> 2.4.4)
17
+ actionpack (3.2.8)
18
+ activemodel (= 3.2.8)
19
+ activesupport (= 3.2.8)
20
+ builder (~> 3.0.0)
21
+ erubis (~> 2.7.0)
22
+ journey (~> 1.0.4)
23
+ rack (~> 1.4.0)
24
+ rack-cache (~> 1.2)
25
+ rack-test (~> 0.6.1)
26
+ sprockets (~> 2.1.3)
27
+ activemodel (3.2.8)
28
+ activesupport (= 3.2.8)
29
+ builder (~> 3.0.0)
30
+ activerecord (3.2.8)
31
+ activemodel (= 3.2.8)
32
+ activesupport (= 3.2.8)
33
+ arel (~> 3.0.2)
34
+ tzinfo (~> 0.3.29)
35
+ activeresource (3.2.8)
36
+ activemodel (= 3.2.8)
37
+ activesupport (= 3.2.8)
38
+ activesupport (3.2.8)
39
+ i18n (~> 0.6)
40
+ multi_json (~> 1.0)
41
+ arel (3.0.2)
42
+ builder (3.0.0)
43
+ erubis (2.7.0)
44
+ hike (1.2.1)
45
+ i18n (0.6.0)
46
+ journey (1.0.4)
47
+ jquery-rails (2.1.0)
48
+ railties (>= 3.1.0, < 5.0)
49
+ thor (~> 0.14)
50
+ json (1.7.4)
51
+ mail (2.4.4)
52
+ i18n (>= 0.4.0)
53
+ mime-types (~> 1.16)
54
+ treetop (~> 1.4.8)
55
+ mime-types (1.19)
56
+ multi_json (1.3.6)
57
+ polyglot (0.3.3)
58
+ rack (1.4.1)
59
+ rack-cache (1.2)
60
+ rack (>= 0.4)
61
+ rack-ssl (1.3.2)
62
+ rack
63
+ rack-test (0.6.1)
64
+ rack (>= 1.0)
65
+ rails (3.2.8)
66
+ actionmailer (= 3.2.8)
67
+ actionpack (= 3.2.8)
68
+ activerecord (= 3.2.8)
69
+ activeresource (= 3.2.8)
70
+ activesupport (= 3.2.8)
71
+ bundler (~> 1.0)
72
+ railties (= 3.2.8)
73
+ railties (3.2.8)
74
+ actionpack (= 3.2.8)
75
+ activesupport (= 3.2.8)
76
+ rack-ssl (~> 1.3.2)
77
+ rake (>= 0.8.7)
78
+ rdoc (~> 3.4)
79
+ thor (>= 0.14.6, < 2.0)
80
+ rake (0.9.2.2)
81
+ rdoc (3.12)
82
+ json (~> 1.4)
83
+ redcarpet (2.0.1)
84
+ sprockets (2.1.3)
85
+ hike (~> 1.2)
86
+ rack (~> 1.0)
87
+ tilt (~> 1.1, != 1.3.0)
88
+ thor (0.16.0)
89
+ tilt (1.3.3)
90
+ treetop (1.4.10)
91
+ polyglot
92
+ polyglot (>= 0.3.1)
93
+ twitter-text (1.5.0)
94
+ activesupport
95
+ tzinfo (0.3.33)
96
+
97
+ PLATFORMS
98
+ ruby
99
+
100
+ DEPENDENCIES
101
+ sir-trevor-rails!
data/MIT-LICENCE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2011-2012 by ITV plc - http://www.itv.com
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,120 @@
1
+ # Sir Trevor Rails
2
+
3
+ A Rails gem for integrating the Sir Trevor JS into your Rails 3.x application.
4
+
5
+ # Usage
6
+
7
+ Add Sir Trevor to your Gemfile
8
+
9
+ ```ruby
10
+ gem 'sir-trevor-rails'
11
+ ```
12
+
13
+ ```bash
14
+ bundle install
15
+ ```
16
+
17
+ Require SirTrevor in your `application_controller.rb`
18
+
19
+ ```ruby
20
+ require 'sir-trevor-rails'
21
+ ```
22
+
23
+ Include Sir Trevor in your `application.css` file
24
+
25
+ ```css
26
+ *= require sir-trevor
27
+ ```
28
+
29
+ Include Sir Trevor in your `application.js` file
30
+
31
+ ```js
32
+ //= require sir-trevor
33
+ ```
34
+
35
+ In your view file for your editable content (must be a 'text' field as we store the JSON here) here we have a field called 'content'
36
+
37
+ ```ruby
38
+ f.sir_trevor_text_area :content
39
+ ```
40
+
41
+ And instantiate a new `SirTrevor.Editor` instance in your Javascript.
42
+
43
+ ```javascript
44
+ $(function(){
45
+ var editor = new SirTrevor.Editor({ el: $('.sir-trevor-area') });
46
+ });
47
+ ```
48
+
49
+ Or for multiple instances:
50
+
51
+ ```javascript
52
+ $(function(){
53
+ var instances = $('.sir-trevor-area'),
54
+ l = instances.length, instance;
55
+
56
+ while (l--) {
57
+ instance = $(instances[l]);
58
+ new SirTrevor.Editor({ el: instance });
59
+ }
60
+
61
+ });
62
+ ```
63
+
64
+ To render your content (in your view file)
65
+
66
+ ```ruby
67
+ <%= render_sir_trevor(post.content) %>
68
+ ```
69
+
70
+ There's an example Rails 3.2.7 project with all of this already done in the [Sir Trevor JS repository](https://github.com/madebymany/sir-trevor-js/tree/master/examples/rails/sir-trevor-example).
71
+
72
+ # Generators
73
+
74
+ ## Views
75
+
76
+ To grab all of the default block type partials into your application run the following generator command:
77
+
78
+ ```bash
79
+ rails g sir_trevor:views
80
+ ```
81
+
82
+ This will copy all of the SirTrevor block partials into `app/views/sir-trevor/blocks/`
83
+
84
+ # Handling image uploads
85
+
86
+ We don't provide a default image uploader out of the box, because everyone will have different requirements. To see an example of an image uploader, please refer to our Rails examples in the [Sir Trevor JS repository](https://github.com/madebymany/sir-trevor-js/tree/master/examples/rails/image-uploader).
87
+
88
+ # Helper methods
89
+
90
+ **`render_sir_trevor`**
91
+
92
+ Parses the blocks JSON content, loops through each piece of block content and render the appropriate partial for the block.
93
+
94
+ **`render_sir_trevor_image`**
95
+
96
+ Returns the first available SirTrevor image from the supplied JSON.
97
+
98
+ **`sir_trevor_image_tag`**
99
+
100
+ Returns an image tag from a SirTrevor Image block
101
+
102
+ **`pluck_sir_trevor_type` (Private)**
103
+
104
+ Get the first instance of a specified SirTrevor block type from the supplied JSON
105
+
106
+ # Requirements
107
+
108
+ - Rails 3.x
109
+ - jQuery
110
+ - Underscore.js (bundled)
111
+
112
+ # To do
113
+
114
+ - Add tests
115
+
116
+ # Licence
117
+
118
+ Sir Trevor Rails is released under the MIT Licence
119
+
120
+ http://www.opensource.org/licenses/MIT
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env rake
2
+ # encoding: UTF-8
3
+
4
+ require 'net/http'
5
+
6
+ task :default => [:"get-latest-files"]
7
+
8
+ desc "Updates the version of sir-trevor to the latest version in the Github repository"
9
+ task :"get-latest-files" do
10
+ end
@@ -0,0 +1,2 @@
1
+ //= require_directory ./sir-trevor/libs
2
+ //= require ./sir-trevor/sir-trevor
@@ -0,0 +1,32 @@
1
+ // Underscore.js 1.3.3
2
+ // (c) 2009-2012 Jeremy Ashkenas, DocumentCloud Inc.
3
+ // Underscore is freely distributable under the MIT license.
4
+ // Portions of Underscore are inspired or borrowed from Prototype,
5
+ // Oliver Steele's Functional, and John Resig's Micro-Templating.
6
+ // For all details and documentation:
7
+ // http://documentcloud.github.com/underscore
8
+ (function(){function r(a,c,d){if(a===c)return 0!==a||1/a==1/c;if(null==a||null==c)return a===c;a._chain&&(a=a._wrapped);c._chain&&(c=c._wrapped);if(a.isEqual&&b.isFunction(a.isEqual))return a.isEqual(c);if(c.isEqual&&b.isFunction(c.isEqual))return c.isEqual(a);var e=l.call(a);if(e!=l.call(c))return!1;switch(e){case "[object String]":return a==""+c;case "[object Number]":return a!=+a?c!=+c:0==a?1/a==1/c:a==+c;case "[object Date]":case "[object Boolean]":return+a==+c;case "[object RegExp]":return a.source==
9
+ c.source&&a.global==c.global&&a.multiline==c.multiline&&a.ignoreCase==c.ignoreCase}if("object"!=typeof a||"object"!=typeof c)return!1;for(var f=d.length;f--;)if(d[f]==a)return!0;d.push(a);var f=0,g=!0;if("[object Array]"==e){if(f=a.length,g=f==c.length)for(;f--&&(g=f in a==f in c&&r(a[f],c[f],d)););}else{if("constructor"in a!="constructor"in c||a.constructor!=c.constructor)return!1;for(var h in a)if(b.has(a,h)&&(f++,!(g=b.has(c,h)&&r(a[h],c[h],d))))break;if(g){for(h in c)if(b.has(c,h)&&!f--)break;
10
+ g=!f}}d.pop();return g}var s=this,I=s._,o={},k=Array.prototype,p=Object.prototype,i=k.slice,J=k.unshift,l=p.toString,K=p.hasOwnProperty,y=k.forEach,z=k.map,A=k.reduce,B=k.reduceRight,C=k.filter,D=k.every,E=k.some,q=k.indexOf,F=k.lastIndexOf,p=Array.isArray,L=Object.keys,t=Function.prototype.bind,b=function(a){return new m(a)};"undefined"!==typeof exports?("undefined"!==typeof module&&module.exports&&(exports=module.exports=b),exports._=b):s._=b;b.VERSION="1.3.3";var j=b.each=b.forEach=function(a,
11
+ c,d){if(a!=null)if(y&&a.forEach===y)a.forEach(c,d);else if(a.length===+a.length)for(var e=0,f=a.length;e<f;e++){if(e in a&&c.call(d,a[e],e,a)===o)break}else for(e in a)if(b.has(a,e)&&c.call(d,a[e],e,a)===o)break};b.map=b.collect=function(a,c,b){var e=[];if(a==null)return e;if(z&&a.map===z)return a.map(c,b);j(a,function(a,g,h){e[e.length]=c.call(b,a,g,h)});if(a.length===+a.length)e.length=a.length;return e};b.reduce=b.foldl=b.inject=function(a,c,d,e){var f=arguments.length>2;a==null&&(a=[]);if(A&&
12
+ a.reduce===A){e&&(c=b.bind(c,e));return f?a.reduce(c,d):a.reduce(c)}j(a,function(a,b,i){if(f)d=c.call(e,d,a,b,i);else{d=a;f=true}});if(!f)throw new TypeError("Reduce of empty array with no initial value");return d};b.reduceRight=b.foldr=function(a,c,d,e){var f=arguments.length>2;a==null&&(a=[]);if(B&&a.reduceRight===B){e&&(c=b.bind(c,e));return f?a.reduceRight(c,d):a.reduceRight(c)}var g=b.toArray(a).reverse();e&&!f&&(c=b.bind(c,e));return f?b.reduce(g,c,d,e):b.reduce(g,c)};b.find=b.detect=function(a,
13
+ c,b){var e;G(a,function(a,g,h){if(c.call(b,a,g,h)){e=a;return true}});return e};b.filter=b.select=function(a,c,b){var e=[];if(a==null)return e;if(C&&a.filter===C)return a.filter(c,b);j(a,function(a,g,h){c.call(b,a,g,h)&&(e[e.length]=a)});return e};b.reject=function(a,c,b){var e=[];if(a==null)return e;j(a,function(a,g,h){c.call(b,a,g,h)||(e[e.length]=a)});return e};b.every=b.all=function(a,c,b){var e=true;if(a==null)return e;if(D&&a.every===D)return a.every(c,b);j(a,function(a,g,h){if(!(e=e&&c.call(b,
14
+ a,g,h)))return o});return!!e};var G=b.some=b.any=function(a,c,d){c||(c=b.identity);var e=false;if(a==null)return e;if(E&&a.some===E)return a.some(c,d);j(a,function(a,b,h){if(e||(e=c.call(d,a,b,h)))return o});return!!e};b.include=b.contains=function(a,c){var b=false;if(a==null)return b;if(q&&a.indexOf===q)return a.indexOf(c)!=-1;return b=G(a,function(a){return a===c})};b.invoke=function(a,c){var d=i.call(arguments,2);return b.map(a,function(a){return(b.isFunction(c)?c||a:a[c]).apply(a,d)})};b.pluck=
15
+ function(a,c){return b.map(a,function(a){return a[c]})};b.max=function(a,c,d){if(!c&&b.isArray(a)&&a[0]===+a[0])return Math.max.apply(Math,a);if(!c&&b.isEmpty(a))return-Infinity;var e={computed:-Infinity};j(a,function(a,b,h){b=c?c.call(d,a,b,h):a;b>=e.computed&&(e={value:a,computed:b})});return e.value};b.min=function(a,c,d){if(!c&&b.isArray(a)&&a[0]===+a[0])return Math.min.apply(Math,a);if(!c&&b.isEmpty(a))return Infinity;var e={computed:Infinity};j(a,function(a,b,h){b=c?c.call(d,a,b,h):a;b<e.computed&&
16
+ (e={value:a,computed:b})});return e.value};b.shuffle=function(a){var b=[],d;j(a,function(a,f){d=Math.floor(Math.random()*(f+1));b[f]=b[d];b[d]=a});return b};b.sortBy=function(a,c,d){var e=b.isFunction(c)?c:function(a){return a[c]};return b.pluck(b.map(a,function(a,b,c){return{value:a,criteria:e.call(d,a,b,c)}}).sort(function(a,b){var c=a.criteria,d=b.criteria;return c===void 0?1:d===void 0?-1:c<d?-1:c>d?1:0}),"value")};b.groupBy=function(a,c){var d={},e=b.isFunction(c)?c:function(a){return a[c]};
17
+ j(a,function(a,b){var c=e(a,b);(d[c]||(d[c]=[])).push(a)});return d};b.sortedIndex=function(a,c,d){d||(d=b.identity);for(var e=0,f=a.length;e<f;){var g=e+f>>1;d(a[g])<d(c)?e=g+1:f=g}return e};b.toArray=function(a){return!a?[]:b.isArray(a)||b.isArguments(a)?i.call(a):a.toArray&&b.isFunction(a.toArray)?a.toArray():b.values(a)};b.size=function(a){return b.isArray(a)?a.length:b.keys(a).length};b.first=b.head=b.take=function(a,b,d){return b!=null&&!d?i.call(a,0,b):a[0]};b.initial=function(a,b,d){return i.call(a,
18
+ 0,a.length-(b==null||d?1:b))};b.last=function(a,b,d){return b!=null&&!d?i.call(a,Math.max(a.length-b,0)):a[a.length-1]};b.rest=b.tail=function(a,b,d){return i.call(a,b==null||d?1:b)};b.compact=function(a){return b.filter(a,function(a){return!!a})};b.flatten=function(a,c){return b.reduce(a,function(a,e){if(b.isArray(e))return a.concat(c?e:b.flatten(e));a[a.length]=e;return a},[])};b.without=function(a){return b.difference(a,i.call(arguments,1))};b.uniq=b.unique=function(a,c,d){var d=d?b.map(a,d):a,
19
+ e=[];a.length<3&&(c=true);b.reduce(d,function(d,g,h){if(c?b.last(d)!==g||!d.length:!b.include(d,g)){d.push(g);e.push(a[h])}return d},[]);return e};b.union=function(){return b.uniq(b.flatten(arguments,true))};b.intersection=b.intersect=function(a){var c=i.call(arguments,1);return b.filter(b.uniq(a),function(a){return b.every(c,function(c){return b.indexOf(c,a)>=0})})};b.difference=function(a){var c=b.flatten(i.call(arguments,1),true);return b.filter(a,function(a){return!b.include(c,a)})};b.zip=function(){for(var a=
20
+ i.call(arguments),c=b.max(b.pluck(a,"length")),d=Array(c),e=0;e<c;e++)d[e]=b.pluck(a,""+e);return d};b.indexOf=function(a,c,d){if(a==null)return-1;var e;if(d){d=b.sortedIndex(a,c);return a[d]===c?d:-1}if(q&&a.indexOf===q)return a.indexOf(c);d=0;for(e=a.length;d<e;d++)if(d in a&&a[d]===c)return d;return-1};b.lastIndexOf=function(a,b){if(a==null)return-1;if(F&&a.lastIndexOf===F)return a.lastIndexOf(b);for(var d=a.length;d--;)if(d in a&&a[d]===b)return d;return-1};b.range=function(a,b,d){if(arguments.length<=
21
+ 1){b=a||0;a=0}for(var d=arguments[2]||1,e=Math.max(Math.ceil((b-a)/d),0),f=0,g=Array(e);f<e;){g[f++]=a;a=a+d}return g};var H=function(){};b.bind=function(a,c){var d,e;if(a.bind===t&&t)return t.apply(a,i.call(arguments,1));if(!b.isFunction(a))throw new TypeError;e=i.call(arguments,2);return d=function(){if(!(this instanceof d))return a.apply(c,e.concat(i.call(arguments)));H.prototype=a.prototype;var b=new H,g=a.apply(b,e.concat(i.call(arguments)));return Object(g)===g?g:b}};b.bindAll=function(a){var c=
22
+ i.call(arguments,1);c.length==0&&(c=b.functions(a));j(c,function(c){a[c]=b.bind(a[c],a)});return a};b.memoize=function(a,c){var d={};c||(c=b.identity);return function(){var e=c.apply(this,arguments);return b.has(d,e)?d[e]:d[e]=a.apply(this,arguments)}};b.delay=function(a,b){var d=i.call(arguments,2);return setTimeout(function(){return a.apply(null,d)},b)};b.defer=function(a){return b.delay.apply(b,[a,1].concat(i.call(arguments,1)))};b.throttle=function(a,c){var d,e,f,g,h,i,j=b.debounce(function(){h=
23
+ g=false},c);return function(){d=this;e=arguments;f||(f=setTimeout(function(){f=null;h&&a.apply(d,e);j()},c));g?h=true:i=a.apply(d,e);j();g=true;return i}};b.debounce=function(a,b,d){var e;return function(){var f=this,g=arguments;d&&!e&&a.apply(f,g);clearTimeout(e);e=setTimeout(function(){e=null;d||a.apply(f,g)},b)}};b.once=function(a){var b=false,d;return function(){if(b)return d;b=true;return d=a.apply(this,arguments)}};b.wrap=function(a,b){return function(){var d=[a].concat(i.call(arguments,0));
24
+ return b.apply(this,d)}};b.compose=function(){var a=arguments;return function(){for(var b=arguments,d=a.length-1;d>=0;d--)b=[a[d].apply(this,b)];return b[0]}};b.after=function(a,b){return a<=0?b():function(){if(--a<1)return b.apply(this,arguments)}};b.keys=L||function(a){if(a!==Object(a))throw new TypeError("Invalid object");var c=[],d;for(d in a)b.has(a,d)&&(c[c.length]=d);return c};b.values=function(a){return b.map(a,b.identity)};b.functions=b.methods=function(a){var c=[],d;for(d in a)b.isFunction(a[d])&&
25
+ c.push(d);return c.sort()};b.extend=function(a){j(i.call(arguments,1),function(b){for(var d in b)a[d]=b[d]});return a};b.pick=function(a){var c={};j(b.flatten(i.call(arguments,1)),function(b){b in a&&(c[b]=a[b])});return c};b.defaults=function(a){j(i.call(arguments,1),function(b){for(var d in b)a[d]==null&&(a[d]=b[d])});return a};b.clone=function(a){return!b.isObject(a)?a:b.isArray(a)?a.slice():b.extend({},a)};b.tap=function(a,b){b(a);return a};b.isEqual=function(a,b){return r(a,b,[])};b.isEmpty=
26
+ function(a){if(a==null)return true;if(b.isArray(a)||b.isString(a))return a.length===0;for(var c in a)if(b.has(a,c))return false;return true};b.isElement=function(a){return!!(a&&a.nodeType==1)};b.isArray=p||function(a){return l.call(a)=="[object Array]"};b.isObject=function(a){return a===Object(a)};b.isArguments=function(a){return l.call(a)=="[object Arguments]"};b.isArguments(arguments)||(b.isArguments=function(a){return!(!a||!b.has(a,"callee"))});b.isFunction=function(a){return l.call(a)=="[object Function]"};
27
+ b.isString=function(a){return l.call(a)=="[object String]"};b.isNumber=function(a){return l.call(a)=="[object Number]"};b.isFinite=function(a){return b.isNumber(a)&&isFinite(a)};b.isNaN=function(a){return a!==a};b.isBoolean=function(a){return a===true||a===false||l.call(a)=="[object Boolean]"};b.isDate=function(a){return l.call(a)=="[object Date]"};b.isRegExp=function(a){return l.call(a)=="[object RegExp]"};b.isNull=function(a){return a===null};b.isUndefined=function(a){return a===void 0};b.has=function(a,
28
+ b){return K.call(a,b)};b.noConflict=function(){s._=I;return this};b.identity=function(a){return a};b.times=function(a,b,d){for(var e=0;e<a;e++)b.call(d,e)};b.escape=function(a){return(""+a).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&#x27;").replace(/\//g,"&#x2F;")};b.result=function(a,c){if(a==null)return null;var d=a[c];return b.isFunction(d)?d.call(a):d};b.mixin=function(a){j(b.functions(a),function(c){M(c,b[c]=a[c])})};var N=0;b.uniqueId=
29
+ function(a){var b=N++;return a?a+b:b};b.templateSettings={evaluate:/<%([\s\S]+?)%>/g,interpolate:/<%=([\s\S]+?)%>/g,escape:/<%-([\s\S]+?)%>/g};var u=/.^/,n={"\\":"\\","'":"'",r:"\r",n:"\n",t:"\t",u2028:"\u2028",u2029:"\u2029"},v;for(v in n)n[n[v]]=v;var O=/\\|'|\r|\n|\t|\u2028|\u2029/g,P=/\\(\\|'|r|n|t|u2028|u2029)/g,w=function(a){return a.replace(P,function(a,b){return n[b]})};b.template=function(a,c,d){d=b.defaults(d||{},b.templateSettings);a="__p+='"+a.replace(O,function(a){return"\\"+n[a]}).replace(d.escape||
30
+ u,function(a,b){return"'+\n_.escape("+w(b)+")+\n'"}).replace(d.interpolate||u,function(a,b){return"'+\n("+w(b)+")+\n'"}).replace(d.evaluate||u,function(a,b){return"';\n"+w(b)+"\n;__p+='"})+"';\n";d.variable||(a="with(obj||{}){\n"+a+"}\n");var a="var __p='';var print=function(){__p+=Array.prototype.join.call(arguments, '')};\n"+a+"return __p;\n",e=new Function(d.variable||"obj","_",a);if(c)return e(c,b);c=function(a){return e.call(this,a,b)};c.source="function("+(d.variable||"obj")+"){\n"+a+"}";return c};
31
+ b.chain=function(a){return b(a).chain()};var m=function(a){this._wrapped=a};b.prototype=m.prototype;var x=function(a,c){return c?b(a).chain():a},M=function(a,c){m.prototype[a]=function(){var a=i.call(arguments);J.call(a,this._wrapped);return x(c.apply(b,a),this._chain)}};b.mixin(b);j("pop,push,reverse,shift,sort,splice,unshift".split(","),function(a){var b=k[a];m.prototype[a]=function(){var d=this._wrapped;b.apply(d,arguments);var e=d.length;(a=="shift"||a=="splice")&&e===0&&delete d[0];return x(d,
32
+ this._chain)}});j(["concat","join","slice"],function(a){var b=k[a];m.prototype[a]=function(){return x(b.apply(this._wrapped,arguments),this._chain)}});m.prototype.chain=function(){this._chain=true;return this};m.prototype.value=function(){return this._wrapped}}).call(this);
@@ -0,0 +1,2363 @@
1
+ // Sir Trevor, v0.1.3
2
+
3
+ (function ($, _){
4
+
5
+ var root = this,
6
+ SirTrevor;
7
+
8
+ SirTrevor = root.SirTrevor = {};
9
+ SirTrevor.DEBUG = true;
10
+ SirTrevor.SKIP_VALIDATION = false;
11
+
12
+ /*
13
+ Define default attributes that can be extended through an object passed to the
14
+ initialize function of SirTrevor
15
+ */
16
+
17
+ SirTrevor.DEFAULTS = {
18
+ baseCSSClass: "sir-trevor",
19
+ errorClass: "sir-trevor-error",
20
+ defaultType: "Text",
21
+ spinner: {
22
+ className: 'spinner',
23
+ lines: 9,
24
+ length: 8,
25
+ width: 3,
26
+ radius: 6,
27
+ color: '#000',
28
+ speed: 1.4,
29
+ trail: 57,
30
+ shadow: false,
31
+ left: '50%',
32
+ top: '50%'
33
+ },
34
+ marker: {
35
+ baseCSSClass: "marker",
36
+ buttonClass: "button",
37
+ addText: "Click to add:",
38
+ dropText: "Drop to place content"
39
+ },
40
+ formatBar: {
41
+ baseCSSClass: "formatting-control"
42
+ },
43
+ blockLimit: 0,
44
+ blockTypeLimits: {},
45
+ required: [],
46
+ uploadUrl: '/attachments',
47
+ baseImageUrl: '/sir-trevor-uploads/'
48
+ };
49
+
50
+ SirTrevor.Blocks = {};
51
+ SirTrevor.Formatters = {};
52
+ SirTrevor.instances = [];
53
+
54
+ var formBound = false; // Flag to tell us once we've bound our submit event
55
+
56
+ /* Generic function binding utility, used by lots of our classes */
57
+ var FunctionBind = {
58
+ bound: [],
59
+ _bindFunctions: function(){
60
+ var args = [];
61
+ args.push(this);
62
+ args.join(this.bound);
63
+ _.bindAll.apply(this, args);
64
+ }
65
+ };
66
+
67
+ /*
68
+ Given an array or object, flatten it and return only the key => true
69
+ */
70
+
71
+ function flattern(obj){
72
+ var x = {};
73
+ _.each(obj, function(a,b) {
74
+ x[(_.isArray(obj)) ? a : b] = true;
75
+ });
76
+ return x;
77
+ }
78
+ /* Halt event execution */
79
+ function halt(ev){
80
+ ev.preventDefault();
81
+ ev.stopPropagation();
82
+ }
83
+
84
+ function controlKeyDown(ev){
85
+ return (ev.which == 17 || ev.which == 224);
86
+ }
87
+
88
+ function isElementNear($element, distance, event) {
89
+ var left = $element.offset().left - distance,
90
+ top = $element.offset().top - distance,
91
+ right = left + $element.width() + ( 2 * distance ),
92
+ bottom = top + $element.height() + ( 2 * distance ),
93
+ x = event.pageX,
94
+ y = event.pageY;
95
+
96
+ return ( x > left && x < right && y > top && y < bottom );
97
+ }
98
+
99
+ /*
100
+ Drop Area Plugin from @maccman
101
+ http://blog.alexmaccaw.com/svbtle-image-uploading
102
+ --
103
+ Tweaked so we use the parent class of dropzone
104
+ */
105
+
106
+ (function($){
107
+ function dragEnter(e) {
108
+ halt(e);
109
+ }
110
+
111
+ function dragOver(e) {
112
+ e.originalEvent.dataTransfer.dropEffect = "copy";
113
+ halt(e);
114
+ }
115
+
116
+ function dragLeave(e) {
117
+ halt(e);
118
+ }
119
+
120
+ $.fn.dropArea = function(){
121
+ this.bind("dragenter", dragEnter).
122
+ bind("dragover", dragOver).
123
+ bind("dragleave", dragLeave);
124
+ return this;
125
+ };
126
+
127
+ $.fn.noDropArea = function(){
128
+ this.unbind("dragenter").
129
+ unbind("dragover").
130
+ unbind("dragleave");
131
+ return this;
132
+ };
133
+
134
+ })(jQuery);
135
+ /*
136
+ Backbone Inheritence
137
+ --
138
+ From: https://github.com/documentcloud/backbone/blob/master/backbone.js
139
+ Backbone.js 0.9.2
140
+ (c) 2010-2012 Jeremy Ashkenas, DocumentCloud Inc.
141
+ */
142
+
143
+ // The self-propagating extend function that Backbone classes use.
144
+ var extend = function(protoProps, classProps) {
145
+ return inherits(this, protoProps, classProps);
146
+ };
147
+
148
+ // Shared empty constructor function to aid in prototype-chain creation.
149
+ var ctor = function(){};
150
+
151
+ // Helper function to correctly set up the prototype chain, for subclasses.
152
+ // Similar to `goog.inherits`, but uses a hash of prototype properties and
153
+ // class properties to be extended.
154
+ var inherits = function(parent, protoProps, staticProps) {
155
+ var child;
156
+
157
+ // The constructor function for the new subclass is either defined by you
158
+ // (the "constructor" property in your `extend` definition), or defaulted
159
+ // by us to simply call the parent's constructor.
160
+ if (protoProps && protoProps.hasOwnProperty('constructor')) {
161
+ child = protoProps.constructor;
162
+ } else {
163
+ child = function(){ parent.apply(this, arguments); };
164
+ }
165
+
166
+ // Inherit class (static) properties from parent.
167
+ _.extend(child, parent);
168
+
169
+ // Set the prototype chain to inherit from `parent`, without calling
170
+ // `parent`'s constructor function.
171
+ ctor.prototype = parent.prototype;
172
+ child.prototype = new ctor();
173
+
174
+ // Add prototype properties (instance properties) to the subclass,
175
+ // if supplied.
176
+ if (protoProps) _.extend(child.prototype, protoProps);
177
+
178
+ // Add static properties to the constructor function, if supplied.
179
+ if (staticProps) _.extend(child, staticProps);
180
+
181
+ // Correctly set child's `prototype.constructor`.
182
+ child.prototype.constructor = child;
183
+
184
+ // Set a convenience property in case the parent's prototype is needed later.
185
+ child.__super__ = parent.prototype;
186
+
187
+ return child;
188
+ };
189
+ /*
190
+ * Ultra simple logging
191
+ */
192
+
193
+ SirTrevor.log = function(message) {
194
+ if (!_.isUndefined(console) && SirTrevor.DEBUG) {
195
+ console.log(message);
196
+ }
197
+ };
198
+ /* String to slug */
199
+
200
+ function toSlug(string)
201
+ {
202
+ return string
203
+ .toLowerCase()
204
+ .replace(/[^\w ]+/g,'')
205
+ .replace(/ +/g,'-');
206
+ }
207
+ /* jQuery Tiny Pub/Sub - v0.7 - 10/27/2011
208
+ * http://benalman.com/
209
+ * Copyright (c) 2011 "Cowboy" Ben Alman; Licensed MIT, GPL */
210
+ var o = $(SirTrevor);
211
+ SirTrevor.subscribe = SirTrevor.on = function() {
212
+ o.on.apply(o, arguments);
213
+ };
214
+ SirTrevor.unsubscribe = SirTrevor.off = function() {
215
+ o.off.apply(o, arguments);
216
+ };
217
+ SirTrevor.publish = SirTrevor.trigger = function() {
218
+ o.trigger.apply(o, arguments);
219
+ };
220
+ SirTrevor.subscribeAll = function(subscriptions) {
221
+ _.each(subscriptions, function(subscription) {
222
+ o.on.apply(o, arguments);
223
+ });
224
+ };
225
+ //fgnass.github.com/spin.js#v1.2.5
226
+ (function(a,b,c){function g(a,c){var d=b.createElement(a||"div"),e;for(e in c)d[e]=c[e];return d}function h(a){for(var b=1,c=arguments.length;b<c;b++)a.appendChild(arguments[b]);return a}function j(a,b,c,d){var g=["opacity",b,~~(a*100),c,d].join("-"),h=.01+c/d*100,j=Math.max(1-(1-a)/b*(100-h),a),k=f.substring(0,f.indexOf("Animation")).toLowerCase(),l=k&&"-"+k+"-"||"";return e[g]||(i.insertRule("@"+l+"keyframes "+g+"{"+"0%{opacity:"+j+"}"+h+"%{opacity:"+a+"}"+(h+.01)+"%{opacity:1}"+(h+b)%100+"%{opacity:"+a+"}"+"100%{opacity:"+j+"}"+"}",0),e[g]=1),g}function k(a,b){var e=a.style,f,g;if(e[b]!==c)return b;b=b.charAt(0).toUpperCase()+b.slice(1);for(g=0;g<d.length;g++){f=d[g]+b;if(e[f]!==c)return f}}function l(a,b){for(var c in b)a.style[k(a,c)||c]=b[c];return a}function m(a){for(var b=1;b<arguments.length;b++){var d=arguments[b];for(var e in d)a[e]===c&&(a[e]=d[e])}return a}function n(a){var b={x:a.offsetLeft,y:a.offsetTop};while(a=a.offsetParent)b.x+=a.offsetLeft,b.y+=a.offsetTop;return b}var d=["webkit","Moz","ms","O"],e={},f,i=function(){var a=g("style");return h(b.getElementsByTagName("head")[0],a),a.sheet||a.styleSheet}(),o={lines:12,length:7,width:5,radius:10,rotate:0,color:"#000",speed:1,trail:100,opacity:.25,fps:20,zIndex:2e9,className:"spinner",top:"auto",left:"auto"},p=function q(a){if(!this.spin)return new q(a);this.opts=m(a||{},q.defaults,o)};p.defaults={},m(p.prototype,{spin:function(a){this.stop();var b=this,c=b.opts,d=b.el=l(g(0,{className:c.className}),{position:"relative",zIndex:c.zIndex}),e=c.radius+c.length+c.width,h,i;a&&(a.insertBefore(d,a.firstChild||null),i=n(a),h=n(d),l(d,{left:(c.left=="auto"?i.x-h.x+(a.offsetWidth>>1):c.left+e)+"px",top:(c.top=="auto"?i.y-h.y+(a.offsetHeight>>1):c.top+e)+"px"})),d.setAttribute("aria-role","progressbar"),b.lines(d,b.opts);if(!f){var j=0,k=c.fps,m=k/c.speed,o=(1-c.opacity)/(m*c.trail/100),p=m/c.lines;!function q(){j++;for(var a=c.lines;a;a--){var e=Math.max(1-(j+a*p)%m*o,c.opacity);b.opacity(d,c.lines-a,e,c)}b.timeout=b.el&&setTimeout(q,~~(1e3/k))}()}return b},stop:function(){var a=this.el;return a&&(clearTimeout(this.timeout),a.parentNode&&a.parentNode.removeChild(a),this.el=c),this},lines:function(a,b){function e(a,d){return l(g(),{position:"absolute",width:b.length+b.width+"px",height:b.width+"px",background:a,boxShadow:d,transformOrigin:"left",transform:"rotate("+~~(360/b.lines*c+b.rotate)+"deg) translate("+b.radius+"px"+",0)",borderRadius:(b.width>>1)+"px"})}var c=0,d;for(;c<b.lines;c++)d=l(g(),{position:"absolute",top:1+~(b.width/2)+"px",transform:b.hwaccel?"translate3d(0,0,0)":"",opacity:b.opacity,animation:f&&j(b.opacity,b.trail,c,b.lines)+" "+1/b.speed+"s linear infinite"}),b.shadow&&h(d,l(e("#000","0 0 4px #000"),{top:"2px"})),h(a,h(d,e(b.color,"0 0 1px rgba(0,0,0,.1)")));return a},opacity:function(a,b,c){b<a.childNodes.length&&(a.childNodes[b].style.opacity=c)}}),!function(){function a(a,b){return g("<"+a+' xmlns="urn:schemas-microsoft.com:vml" class="spin-vml">',b)}var b=l(g("group"),{behavior:"url(#default#VML)"});!k(b,"transform")&&b.adj?(i.addRule(".spin-vml","behavior:url(#default#VML)"),p.prototype.lines=function(b,c){function f(){return l(a("group",{coordsize:e+" "+e,coordorigin:-d+" "+ -d}),{width:e,height:e})}function k(b,e,g){h(i,h(l(f(),{rotation:360/c.lines*b+"deg",left:~~e}),h(l(a("roundrect",{arcsize:1}),{width:d,height:c.width,left:c.radius,top:-c.width>>1,filter:g}),a("fill",{color:c.color,opacity:c.opacity}),a("stroke",{opacity:0}))))}var d=c.length+c.width,e=2*d,g=-(c.width+c.length)*2+"px",i=l(f(),{position:"absolute",top:g,left:g}),j;if(c.shadow)for(j=1;j<=c.lines;j++)k(j,-2,"progid:DXImageTransform.Microsoft.Blur(pixelradius=2,makeshadow=1,shadowopacity=.3)");for(j=1;j<=c.lines;j++)k(j);return h(b,i)},p.prototype.opacity=function(a,b,c,d){var e=a.firstChild;d=d.shadow&&d.lines||0,e&&b+d<e.childNodes.length&&(e=e.childNodes[b+d],e=e&&e.firstChild,e=e&&e.firstChild,e&&(e.opacity=c))}):f=k(b,"animation")}(),a.Spinner=p})(window,document);
227
+ /* Soft character limits on inputs and textareas */
228
+
229
+ (function($){
230
+
231
+ $.fn.limit_chars = function() {
232
+
233
+ if (this.length===0) return;
234
+
235
+ // Remove browser maxlength, add soft limit
236
+ if(this.attr('maxlength')) {
237
+ this.attr('data-maxlength',this.attr('maxlength'));
238
+ this.removeAttr('maxlength');
239
+ }
240
+
241
+ if(this.parents('.extended_input').length === 0) {
242
+
243
+ count = (this.chars()<this.attr('data-maxlength')) ? this.chars() : '<em>'+this.chars()+'</em>';
244
+
245
+ // Build UI
246
+ this.wrap($('<div>',{
247
+ "class": "extended_input"
248
+ })).after($('<span>', {
249
+ "class": "count",
250
+ html: count+' of '+this.attr('data-maxlength')
251
+ }));
252
+
253
+ // Attach event
254
+ this.bind('keydown keyup paste',function(ev){
255
+ count = ($(this).chars()<$(this).attr('data-maxlength')) ? $(this).chars() : '<em>'+$(this).chars()+'</em>';
256
+ $(this).parent().find('.count').html(count+' of '+$(this).attr('data-maxlength'));
257
+ });
258
+
259
+ }
260
+
261
+ };
262
+
263
+ $.fn.chars = function() {
264
+ count = (this.attr('contenteditable')!==undefined) ? this.text().length : count = this.val().length;
265
+ return count;
266
+ };
267
+
268
+ $.fn.too_long = function() {
269
+ return this.chars() > this.attr('data-maxlength');
270
+ };
271
+
272
+ })(jQuery);
273
+ /*
274
+ * Sir Trevor Block Store
275
+ * By default we store the data on the instance
276
+ * We can easily extend this and store it on some server or something
277
+ */
278
+
279
+ SirTrevor.blockStore = function(method, block, options) {
280
+
281
+ var resp;
282
+
283
+ options = options || {};
284
+
285
+ switch(method) {
286
+
287
+ case "create":
288
+ var data = options.data || {};
289
+ block.dataStore = { type: block.type.toLowerCase(), data: data };
290
+ break;
291
+
292
+ case "save":
293
+ if (options.data) {
294
+ block.dataStore.data = options.data;
295
+ resp = block.dataStore;
296
+ }
297
+ break;
298
+
299
+ case "read":
300
+ resp = block.dataStore;
301
+ break;
302
+
303
+ }
304
+
305
+ if(resp) {
306
+ return resp;
307
+ }
308
+
309
+ };
310
+ /*
311
+ * Sir Trevor Editor Store
312
+ * By default we store the complete data on the instances $el
313
+ * We can easily extend this and store it on some server or something
314
+ */
315
+
316
+ SirTrevor.editorStore = function(method, editor, options) {
317
+
318
+ var resp;
319
+
320
+ options = options || {};
321
+
322
+ switch(method) {
323
+
324
+ case "create":
325
+ // Grab our JSON from the textarea and clean any whitespace incase there is a line wrap between the opening and closing textarea tags
326
+ var content = _.trim(editor.$el.val());
327
+ editor.dataStore = { data: [] };
328
+
329
+ if (content.length > 0) {
330
+ try {
331
+ // Ensure the JSON string has a data element that's an array
332
+ var str = JSON.parse(content);
333
+ if (!_.isUndefined(str.data)) {
334
+ // Set it
335
+ editor.dataStore = str;
336
+ }
337
+ } catch(e) {
338
+ console.log('Sorry there has been a problem with parsing the JSON');
339
+ console.log(e);
340
+ }
341
+ }
342
+ break;
343
+
344
+ case "reset":
345
+ editor.dataStore = { data: [] };
346
+ break;
347
+
348
+ case "add":
349
+ if (options.data) {
350
+ editor.dataStore.data.push(options.data);
351
+ resp = editor.dataStore;
352
+ }
353
+ break;
354
+
355
+ case "save":
356
+ // Store to our element
357
+ editor.$el.val((editor.dataStore.data.length > 0) ? JSON.stringify(editor.dataStore) : '');
358
+ break;
359
+
360
+ case "read":
361
+ resp = editor.dataStore;
362
+ break;
363
+
364
+ }
365
+
366
+ if(resp) {
367
+ return resp;
368
+ }
369
+
370
+ };
371
+
372
+ /*
373
+ SirTrevor.Submittable
374
+ --
375
+ We need a global way of setting if the editor can and can't be submitted,
376
+ and a way to disable the submit button and add messages (when appropriate)
377
+ We also need this to be highly extensible so it can be overridden.
378
+ This will be triggered *by anything* so it needs to subscribe to events.
379
+ */
380
+
381
+ var Submittable = function(){
382
+ this.intialize();
383
+ };
384
+
385
+ _.extend(Submittable.prototype, {
386
+
387
+ intialize: function(){
388
+ this.submitBtn = $("input[type='submit']");
389
+
390
+ var btnTitles = [];
391
+
392
+ _.each(this.submitBtn, function(btn){
393
+ btnTitles.push($(btn).attr('value'));
394
+ });
395
+
396
+ this.submitBtnTitles = btnTitles;
397
+ this.canSubmit = true;
398
+ this.globalUploadCount = 0;
399
+ this._bindEvents();
400
+ },
401
+
402
+ setSubmitButton: function(e, message) {
403
+ this.submitBtn.attr('value', message);
404
+ },
405
+
406
+ resetSubmitButton: function(){
407
+ _.each(this.submitBtn, _.bind(function(item, index){
408
+ $(item).attr('value', this.submitBtnTitles[index]);
409
+ }, this));
410
+ },
411
+
412
+ onUploadStart: function(e){
413
+ this.globalUploadCount++;
414
+ SirTrevor.log('onUploadStart called ' + this.globalUploadCount);
415
+
416
+ if(this.globalUploadCount === 1) {
417
+ this._disableSubmitButton();
418
+ }
419
+ },
420
+
421
+ onUploadStop: function(e) {
422
+ this.globalUploadCount = (this.globalUploadCount <= 0) ? 0 : this.globalUploadCount - 1;
423
+
424
+ SirTrevor.log('onUploadStop called ' + this.globalUploadCount);
425
+
426
+ if(this.globalUploadCount === 0) {
427
+ this._enableSubmitButton();
428
+ }
429
+ },
430
+
431
+ onError: function(e){
432
+ SirTrevor.log('onError called');
433
+ this.canSubmit = false;
434
+ },
435
+
436
+ _disableSubmitButton: function(message){
437
+ this.setSubmitButton(null, message || "Please wait...");
438
+ this.submitBtn
439
+ .attr('disabled', 'disabled')
440
+ .addClass('disabled');
441
+ },
442
+
443
+ _enableSubmitButton: function(){
444
+ this.resetSubmitButton();
445
+ this.submitBtn
446
+ .removeAttr('disabled')
447
+ .removeClass('disabled');
448
+ },
449
+
450
+ _bindEvents: function(){
451
+ SirTrevor.subscribe("disableSubmitButton", _.bind(this._disableSubmitButton, this));
452
+ SirTrevor.subscribe("enableSubmitButton", _.bind(this._enableSubmitButton, this));
453
+ SirTrevor.subscribe("setSubmitButton", _.bind(this.setSubmitButton, this));
454
+ SirTrevor.subscribe("resetSubmitButton", _.bind(this.resetSubmitButton, this));
455
+ SirTrevor.subscribe("onError", _.bind(this.onError, this));
456
+ SirTrevor.subscribe("onUploadStart", _.bind(this.onUploadStart, this));
457
+ SirTrevor.subscribe("onUploadStop", _.bind(this.onUploadStop, this));
458
+ }
459
+
460
+ });
461
+
462
+ SirTrevor.submittable = function(){
463
+ new Submittable();
464
+ };
465
+ /*
466
+ * Sir Trevor Uploader
467
+ * Generic Upload implementation that can be extended for blocks
468
+ */
469
+
470
+ SirTrevor.fileUploader = function(block, file, success, error) {
471
+
472
+ SirTrevor.publish("onUploadStart");
473
+
474
+ var uid = [block.instance.ID, (new Date()).getTime(), 'raw'].join('-');
475
+
476
+ var data = new FormData();
477
+
478
+ data.append('attachment[name]', file.name);
479
+ data.append('attachment[file]', file);
480
+ data.append('attachment[uid]', uid);
481
+
482
+ var callbackSuccess = function(data){
483
+ if (!_.isUndefined(success) && _.isFunction(success)) {
484
+ SirTrevor.log('Upload callback called');
485
+ SirTrevor.publish("onUploadStop");
486
+ _.bind(success, block)(data);
487
+ }
488
+ };
489
+
490
+ var callbackError = function(jqXHR, status, errorThrown){
491
+ if (!_.isUndefined(error) && _.isFunction(error)) {
492
+ SirTrevor.log('Upload callback error called');
493
+ SirTrevor.publish("onUploadError");
494
+ _.bind(error, block)(status);
495
+ }
496
+ };
497
+
498
+ $.ajax({
499
+ url: block.instance.options.uploadUrl,
500
+ data: data,
501
+ cache: false,
502
+ contentType: false,
503
+ processData: false,
504
+ type: 'POST',
505
+ success: callbackSuccess,
506
+ error: callbackError
507
+ });
508
+
509
+ };
510
+
511
+ /*
512
+ Underscore helpers
513
+ */
514
+
515
+ var url_regex = /^(?:([A-Za-z]+):)?(\/{0,3})([0-9.\-A-Za-z]+)(?::(\d+))?(?:\/([^?#]*))?(?:\?([^#]*))?(?:#(.*))?$/;
516
+
517
+ _.mixin({
518
+ isURI : function(string) {
519
+ return (url_regex.test(string));
520
+ },
521
+
522
+ capitalize : function(string) {
523
+ return string.charAt(0).toUpperCase() + string.substring(1).toLowerCase();
524
+ },
525
+
526
+ trim : function(string) {
527
+ return string.replace(/^\s\s*/, '').replace(/\s\s*$/, '');
528
+ }
529
+
530
+ });
531
+
532
+
533
+ var Block = SirTrevor.Block = function(instance, data) {
534
+
535
+ this.instance = instance;
536
+ this.type = this._getBlockType();
537
+
538
+ this.store("create", this, { data: data });
539
+
540
+ //this.data = data;
541
+ this.uploadsCount = 0;
542
+ this.blockID = _.uniqueId(this.className + '-');
543
+
544
+ this._setBaseElements();
545
+ this._bindFunctions();
546
+
547
+ this.render();
548
+
549
+ this.initialize.apply(this, arguments);
550
+ };
551
+
552
+ var blockOptions = [
553
+ "className",
554
+ "toolbarEnabled",
555
+ "formattingEnabled",
556
+ "dropEnabled",
557
+ "title",
558
+ "limit",
559
+ "editorHTML",
560
+ "dropzoneHTML",
561
+ "validate",
562
+ "loadData",
563
+ "toData",
564
+ "onDrop",
565
+ "onContentPasted",
566
+ "onBlockRender",
567
+ "beforeBlockRender",
568
+ "toMarkdown",
569
+ "toHTML"
570
+ ];
571
+
572
+ _.extend(Block.prototype, FunctionBind, {
573
+
574
+ bound: ["_handleDrop", "_handleContentPaste", "onBlockFocus", "onBlockBlur", "onDrop", "onDragStart", "onDragEnd"],
575
+
576
+ $: function(selector) {
577
+ return this.$el.find(selector);
578
+ },
579
+
580
+ $$: function(selector) {
581
+ return this.$editor.find(selector);
582
+ },
583
+
584
+ /* Defaults to be overriden if required */
585
+ className: '',
586
+ title: '',
587
+ limit: 0,
588
+ editorHTML: '<div></div>',
589
+ dropzoneHTML: '<div class="dropzone"><p>Drop content here</p></div>',
590
+ toolbarEnabled: true,
591
+ dropEnabled: false,
592
+ formattingEnabled: true,
593
+
594
+ initialize: function() {},
595
+
596
+ loadData: function(data) {},
597
+ onBlockRender: function(){},
598
+ beforeBlockRender: function(){},
599
+ toMarkdown: function(markdown){ return markdown; },
600
+ toHTML: function(html){ return html; },
601
+
602
+ store: function(){
603
+ return SirTrevor.blockStore.apply(this, arguments);
604
+ },
605
+
606
+ render: function() {
607
+
608
+ this.beforeBlockRender();
609
+
610
+ // Insert before the marker
611
+ this.instance.formatBar.hide();
612
+ this.instance.marker.hide();
613
+ this.instance.marker.$el.before(this.$el);
614
+
615
+ // Do we have a dropzone?
616
+ if (this.dropEnabled) {
617
+ this._initDragDrop();
618
+ }
619
+
620
+ // Has data already?
621
+ var currentData = this.getData();
622
+
623
+ if (!_.isUndefined(currentData) && !_.isEmpty(currentData)) {
624
+ this._loadData();
625
+ }
626
+
627
+ // And save the state
628
+ this.save();
629
+
630
+ // Add UI elements
631
+ this.$el.append($('<span>',{ 'class': 'handle', draggable: true }));
632
+ this.$el.append($('<span>',{ 'class': 'delete block-delete' }));
633
+
634
+ // Stop events propagating through to the container
635
+ this.$el
636
+ .bind('drop', halt)
637
+ .bind('mouseover', halt)
638
+ .bind('mouseout', halt)
639
+ .bind('dragleave', halt)
640
+ .bind('mouseover', function(ev){ $(this).siblings().removeClass('active'); $(this).addClass('active'); })
641
+ .bind('mouseout', function(ev){ $(this).removeClass('active'); })
642
+ .bind('dragover', function(ev){ ev.preventDefault(); });
643
+
644
+ // Handle pastes
645
+ this._initPaste();
646
+
647
+ // Delete
648
+ this.$('.delete.block-delete').bind('click', this.onDeleteClick);
649
+
650
+ // Handle text blocks
651
+ if (this.$$('.text-block').length > 0) {
652
+ document.execCommand("styleWithCSS", false, false);
653
+ document.execCommand("insertBrOnReturn", false, true);
654
+
655
+ // Bind our text block to show the format bar
656
+ this.$$('.text-block')
657
+ .focus(this.onBlockFocus)
658
+ .blur(this.onBlockBlur);
659
+
660
+ // Strip out all the HTML on paste
661
+ this.$$('.text-block').bind('paste', this._handleContentPaste);
662
+
663
+ // Formatting
664
+ this._initFormatting();
665
+ }
666
+
667
+ // Focus if we're adding an empty block, but only if not
668
+ // the only block (i.e. page has just loaded a new editor)
669
+ if (_.isEmpty(currentData.data) && this.instance.blocks.length > 0) {
670
+ var inputs = this.$$('[contenteditable="true"], input');
671
+ if (inputs.length > 0 && !this.dropEnabled) {
672
+ inputs[0].focus();
673
+ }
674
+ }
675
+
676
+ // Reorderable
677
+ this._initReordering();
678
+
679
+ this._initTextLimits();
680
+
681
+ // Set ready state
682
+ this.$el.addClass('sir-trevor-item-ready');
683
+
684
+ this.onBlockRender();
685
+ },
686
+
687
+ remove: function() {
688
+ this.$el.remove();
689
+ },
690
+
691
+ /* Save the state of this block onto the blocks data attr */
692
+ save: function() {
693
+ this.toData();
694
+ return this.store("read", this);
695
+ },
696
+
697
+ getData: function() {
698
+ return this.store("read", this).data;
699
+ },
700
+
701
+ setData: function(data) {
702
+ SirTrevor.log("Setting data for block " + this.blockID);
703
+ this.store("save", this, { data: data });
704
+ },
705
+
706
+ loading: function() {
707
+
708
+ if(!_.isUndefined(this.spinner)) {
709
+ this.ready();
710
+ }
711
+
712
+ this.spinner = new Spinner(this.instance.options.spinner);
713
+ this.spinner.spin(this.$el[0]);
714
+
715
+ this.$el.addClass('loading');
716
+ },
717
+
718
+ ready: function() {
719
+ this.$el.removeClass('loading');
720
+ if (!_.isUndefined(this.spinner)) {
721
+ this.spinner.stop();
722
+ delete this.spinner;
723
+ }
724
+ },
725
+
726
+ /* Generic implementations */
727
+
728
+ validate: function() {
729
+
730
+ this._beforeValidate();
731
+
732
+ var fields = this.$$('.required, [data-maxlength]'),
733
+ errors = 0;
734
+
735
+ _.each(fields, _.bind(function(field) {
736
+ field = $(field);
737
+ var content = (field.attr('contenteditable')) ? field.text() : field.val(),
738
+ too_long = (field.attr('data-maxlength') && field.too_long()),
739
+ required = field.hasClass('required');
740
+
741
+ if ((required && content.length === 0) || too_long) {
742
+ // Error!
743
+ field.addClass(this.instance.options.errorClass).before($("<div>", {
744
+ 'class': 'error-marker',
745
+ 'html': '!'
746
+ }));
747
+ errors++;
748
+ }
749
+ }, this));
750
+
751
+ return (errors === 0);
752
+ },
753
+
754
+ /*
755
+ Generic toData implementation.
756
+ Can be overwritten, although hopefully this will cover most situations
757
+ */
758
+ toData: function() {
759
+
760
+ SirTrevor.log("toData for " + this.blockID);
761
+
762
+ var bl = this.$el,
763
+ dataObj = {};
764
+
765
+ /* Simple to start. Add conditions later */
766
+ if (this.$$('.text-block').length > 0) {
767
+ var content = this.$$('.text-block').html();
768
+ if (content.length > 0) {
769
+ dataObj.text = this.instance._toMarkdown(content, this.type);
770
+ }
771
+ }
772
+
773
+ var hasTextAndData = (!_.isUndefined(dataObj.text) || this.$$('.text-block').length === 0);
774
+
775
+ // Add any inputs to the data attr
776
+ if(this.$$('input[type="text"]').not('.paste-block').length > 0) {
777
+ this.$$('input[type="text"]').each(function(index,input){
778
+ input = $(input);
779
+ if (input.val().length > 0 && hasTextAndData) {
780
+ dataObj[input.attr('name')] = input.val();
781
+ }
782
+ });
783
+ }
784
+
785
+ this.$$('select').each(function(index,input){
786
+ input = $(input);
787
+ if(input.val().length > 0 && hasTextAndData) {
788
+ dataObj[input.attr('name')] = input.val();
789
+ }
790
+ });
791
+
792
+ this.$$('input[type="file"]').each(function(index,input) {
793
+ input = $(input);
794
+ dataObj.file = input.data('json');
795
+ });
796
+
797
+ // Set
798
+ if(!_.isEmpty(dataObj)) {
799
+ this.setData(dataObj);
800
+ }
801
+ },
802
+
803
+ /*
804
+ * Event handlers
805
+ */
806
+
807
+ onDrop: function(dataTransferObj) {},
808
+
809
+ onDragStart: function(ev){
810
+ var item = $(ev.target);
811
+ ev.originalEvent.dataTransfer.setDragImage(item.parent()[0], 13, 25);
812
+ ev.originalEvent.dataTransfer.setData('Text', item.parent().attr('id'));
813
+ item.parent().addClass('dragging');
814
+ this.instance.formatBar.hide();
815
+ },
816
+
817
+ onDragEnd: function(ev){
818
+ var item = $(ev.target);
819
+ item.parent().removeClass('dragging');
820
+ this.instance.marker.hide();
821
+ },
822
+
823
+ onBlockFocus: function(ev) {
824
+ _.delay(_.bind(function(){
825
+ this.instance.formatBar.clicked = false;
826
+ if(this.formattingEnabled) {
827
+ this.instance.formatBar.show(this.$el);
828
+ }
829
+ }, this), 250);
830
+ },
831
+
832
+ onBlockBlur: function(ev) {
833
+ _.delay(_.bind(function(){
834
+ if(!this.instance.formatBar.clicked && this.formattingEnabled) {
835
+ this.instance.formatBar.hide();
836
+ }
837
+ }, this), 250);
838
+ },
839
+
840
+ onDeleteClick: function(ev) {
841
+ if (confirm('Are you sure you wish to delete this content?')) {
842
+ this.instance.removeBlock(this);
843
+ halt(ev);
844
+ }
845
+ },
846
+
847
+ onContentPasted: function(ev){
848
+ var textBlock = this.$$('.text-block');
849
+ if (textBlock.length > 0) {
850
+ textBlock.html(this.instance._toHTML(this.instance._toMarkdown(textBlock.html(), this.type),this.type));
851
+ }
852
+ },
853
+
854
+ /*
855
+ Generic Upload Attachment Function
856
+ Designed to handle any attachments
857
+ */
858
+
859
+ uploader: function(file, callback){
860
+ SirTrevor.fileUploader(this, file, callback);
861
+ },
862
+
863
+ /* Private methods */
864
+
865
+ _loadData: function() {
866
+
867
+ SirTrevor.log("loadData for " + this.blockID);
868
+
869
+ this.loading();
870
+
871
+ if(this.dropEnabled) {
872
+ this.$dropzone.hide();
873
+ this.$editor.show();
874
+ }
875
+
876
+ SirTrevor.publish("editor/block/loadData");
877
+
878
+ this.loadData(this.getData());
879
+ this.ready();
880
+ },
881
+
882
+ _beforeValidate: function() {
883
+ this.errors = [];
884
+ this.$('.error').removeClass('error');
885
+ this.$('.error-marker').remove();
886
+ },
887
+
888
+ _handleContentPaste: function(ev) {
889
+ // We need a little timeout here
890
+ var timed = function(ev){
891
+ // Delegate this off to the super method that can be overwritten
892
+ this.onContentPasted(ev);
893
+ };
894
+ _.delay(_.bind(timed, this, ev), 100);
895
+ },
896
+
897
+ _handleDrop: function(e) {
898
+
899
+ e.preventDefault();
900
+ e = e.originalEvent;
901
+
902
+ SirTrevor.publish("editor/block/handleDrop");
903
+
904
+ var el = $(e.target),
905
+ types = e.dataTransfer.types,
906
+ type, data = [];
907
+
908
+ this.instance.formatBar.hide();
909
+ this.instance.marker.hide();
910
+ this.$dropzone.removeClass('dragOver');
911
+
912
+ /*
913
+ Check the type we just received,
914
+ delegate it away to our blockTypes to process
915
+ */
916
+
917
+ if (!_.isUndefined(types))
918
+ {
919
+ if (_.include(types, 'Files') || _.include(types, 'text/plain') || _.include(types, 'text/uri-list'))
920
+ {
921
+ this.onDrop(e.dataTransfer);
922
+ }
923
+ }
924
+ },
925
+
926
+ _setBaseElements: function(){
927
+ var el = (_.isFunction(this.editorHTML)) ? this.editorHTML() : this.editorHTML;
928
+
929
+ // Set
930
+ var editor = $('<div>', {
931
+ 'class': 'block-editor ' + this.className + '-block',
932
+ html: el
933
+ });
934
+
935
+ this.$el = $('<div>', {
936
+ 'class': this.instance.options.baseCSSClass + "-block",
937
+ id: this.blockID,
938
+ "data-type": this.type,
939
+ "data-instance": this.instance.ID,
940
+ html: editor
941
+ });
942
+
943
+ // Set our element references
944
+ this.el = this.$el[0];
945
+ this.$editor = editor;
946
+ },
947
+
948
+ _getBlockType: function() {
949
+ var objName = "";
950
+ for (var block in SirTrevor.Blocks) {
951
+ if (SirTrevor.Blocks[block].prototype == Object.getPrototypeOf(this)) {
952
+ objName = block;
953
+ }
954
+ }
955
+ return objName;
956
+ },
957
+
958
+ /*
959
+ * Init functions for adding functionality
960
+ *
961
+ */
962
+
963
+ _initDragDrop: function() {
964
+ SirTrevor.log("Adding drag and drop capabilities for block " + this.blockID);
965
+
966
+ this.$dropzone = $("<div>", {
967
+ html: this.dropzoneHTML,
968
+ class: "dropzone " + this.className + '-block'
969
+ });
970
+ this.$el.append(this.$dropzone);
971
+ this.$editor.hide();
972
+
973
+ // Bind our drop event
974
+ this.$dropzone.dropArea();
975
+ this.$dropzone.bind('drop', this._handleDrop);
976
+ },
977
+
978
+ _initReordering: function() {
979
+ this.$('.handle')
980
+ .bind('dragstart', this.onDragStart)
981
+ .bind('dragend', this.onDragEnd)
982
+ .bind('drag', this.instance.marker.show);
983
+ },
984
+
985
+ _initFormatting: function() {
986
+ // Enable formatting keyboard input
987
+ var formatter;
988
+ for (var name in this.instance.formatters) {
989
+ if (this.instance.formatters.hasOwnProperty(name)) {
990
+ formatter = SirTrevor.Formatters[name];
991
+ if (!_.isUndefined(formatter.keyCode)) {
992
+ formatter._bindToBlock(this.$editor);
993
+ }
994
+ }
995
+ }
996
+ },
997
+
998
+ _initPaste: function() {
999
+ this.$('.paste-block')
1000
+ .bind('click', function(){ $(this).select(); })
1001
+ .bind('paste', this._handleContentPaste)
1002
+ .bind('submit', this._handleContentPaste);
1003
+ },
1004
+
1005
+ _initTextLimits: function() {
1006
+ this.$$('input[maxlength!=-1][maxlength!=524288][maxlength!=2147483647]').limit_chars();
1007
+ }
1008
+
1009
+ });
1010
+
1011
+ Block.extend = extend; // Allow our Block to be extended.
1012
+
1013
+ var Format = SirTrevor.Formatter = function(options){
1014
+ this.formatId = _.uniqueId('format-');
1015
+ this._configure(options || {});
1016
+ this.className = SirTrevor.DEFAULTS.baseCSSClass + "-format-" + this.options.className;
1017
+ this.initialize.apply(this, arguments);
1018
+ };
1019
+
1020
+ var formatOptions = ["title", "className", "cmd", "keyCode", "param", "onClick", "toMarkdown", "toHTML"];
1021
+
1022
+ _.extend(Format.prototype, {
1023
+
1024
+ title: '',
1025
+ className: '',
1026
+ cmd: null,
1027
+ keyCode: null,
1028
+ param: null,
1029
+ toMarkdown: function(markdown){ return markdown; },
1030
+ toHTML: function(html){ return html; },
1031
+
1032
+ initialize: function(){},
1033
+
1034
+ _configure: function(options) {
1035
+ if (this.options) options = _.extend({}, this.options, options);
1036
+ for (var i = 0, l = formatOptions.length; i < l; i++) {
1037
+ var attr = formatOptions[i];
1038
+ if (options[attr]) this[attr] = options[attr];
1039
+ }
1040
+ this.options = options;
1041
+ },
1042
+
1043
+ _bindToBlock: function(block) {
1044
+
1045
+ var formatter = this,
1046
+ ctrlDown = false;
1047
+
1048
+ block
1049
+ .on('keyup','.text-block', function(ev) {
1050
+ if(ev.which == 17 || ev.which == 224) {
1051
+ ctrlDown = false;
1052
+ }
1053
+ })
1054
+ .on('keydown','.text-block', { formatter: formatter }, function(ev) {
1055
+ if(ev.which == 17 || ev.which == 224) {
1056
+ ctrlDown = true;
1057
+ }
1058
+ if(ev.which == ev.data.formatter.keyCode && ctrlDown === true) {
1059
+ document.execCommand(ev.data.formatter.cmd, false, true);
1060
+ ev.preventDefault();
1061
+ }
1062
+ });
1063
+ }
1064
+ });
1065
+
1066
+ Format.extend = extend; // Allow our Formatters to be extended.
1067
+
1068
+ /* Default Blocks */
1069
+ /*
1070
+ Block Quote
1071
+ */
1072
+
1073
+ SirTrevor.Blocks.Quote = SirTrevor.Block.extend({
1074
+
1075
+ title: "Quote",
1076
+ className: "block-quote",
1077
+ limit: 0,
1078
+
1079
+ editorHTML: function() {
1080
+ return _.template('<blockquote class="required text-block <%= className %>" contenteditable="true"></blockquote><div class="input text"><label>Credit</label><input maxlength="140" name="cite" class="input-string required" type="text" /></div>', this);
1081
+ },
1082
+
1083
+ loadData: function(data){
1084
+ this.$$('.text-block').html(this.instance._toHTML(data.text, this.type));
1085
+ this.$$('input').val(data.cite);
1086
+ },
1087
+
1088
+ toMarkdown: function(markdown) {
1089
+ return markdown.replace(/^(.+)$/mg,"> $1");
1090
+ }
1091
+
1092
+ });
1093
+ /*
1094
+ Gallery
1095
+ */
1096
+
1097
+ var dropzone_templ = "<p>Drop images here</p><div class=\"input submit\"><input type=\"file\" multiple=\"multiple\" /></div><button>...or choose file(s)</button>";
1098
+
1099
+ SirTrevor.Blocks.Gallery = SirTrevor.Block.extend({
1100
+
1101
+ title: "Gallery",
1102
+ className: "gallery",
1103
+ dropEnabled: true,
1104
+ editorHTML: "<div class=\"gallery-items\"><p>Gallery Contents:</p><ul></ul></div>",
1105
+ dropzoneHTML: dropzone_templ,
1106
+
1107
+ loadData: function(data){
1108
+ // Find all our gallery blocks and draw nice list items from it
1109
+ if (_.isArray(data)) {
1110
+ _.each(data, _.bind(function(item){
1111
+ // Create an image block from this
1112
+ this.renderGalleryThumb(item);
1113
+ }, this));
1114
+
1115
+ // Show the dropzone too
1116
+ this.$dropzone.show();
1117
+ }
1118
+ },
1119
+
1120
+ renderGalleryThumb: function(item) {
1121
+
1122
+ if(_.isUndefined(item.data.file)) return false;
1123
+
1124
+ var img = $("<img>", {
1125
+ src: item.data.file.thumb.url
1126
+ });
1127
+
1128
+ var list = $('<li>', {
1129
+ id: _.uniqueId('gallery-item'),
1130
+ class: 'gallery-item',
1131
+ html: img
1132
+ });
1133
+
1134
+ list.append($("<span>", {
1135
+ class: 'delete',
1136
+ click: _.bind(function(e){
1137
+ // Remove this item
1138
+ halt(e);
1139
+
1140
+ if (confirm('Are you sure you wish to delete this image?')) {
1141
+ $(e.target).parent().remove();
1142
+ this.reindexData();
1143
+ }
1144
+ }, this)
1145
+ }));
1146
+
1147
+ list.data('block', item);
1148
+
1149
+ this.$$('ul').append(list);
1150
+
1151
+ // Make it sortable
1152
+ list
1153
+ .dropArea()
1154
+ .bind('dragstart', _.bind(function(ev){
1155
+ var item = $(ev.target);
1156
+ ev.originalEvent.dataTransfer.setData('Text', item.parent().attr('id'));
1157
+ item.parent().addClass('dragging');
1158
+ }, this))
1159
+
1160
+ .bind('drag', _.bind(function(ev){
1161
+
1162
+ }, this))
1163
+
1164
+ .bind('dragend', _.bind(function(ev){
1165
+ var item = $(ev.target);
1166
+ item.parent().removeClass('dragging');
1167
+ }, this))
1168
+
1169
+ .bind('dragover', _.bind(function(ev){
1170
+ var item = $(ev.target);
1171
+ item.parents('li').addClass('dragover');
1172
+ }, this))
1173
+
1174
+ .bind('dragleave', _.bind(function(ev){
1175
+ var item = $(ev.target);
1176
+ item.parents('li').removeClass('dragover');
1177
+ }, this))
1178
+
1179
+ .bind('drop', _.bind(function(ev){
1180
+
1181
+ var item = $(ev.target),
1182
+ parent = item.parent();
1183
+
1184
+ item = (item.hasClass('gallery-item') ? item : parent);
1185
+
1186
+ this.$$('ul li.dragover').removeClass('dragover');
1187
+
1188
+ // Get the item
1189
+ var target = $('#' + ev.originalEvent.dataTransfer.getData("text/plain"));
1190
+
1191
+ if(target.attr('id') === item.attr('id')) return false;
1192
+
1193
+ if (target.length > 0 && target.hasClass('gallery-item')) {
1194
+ item.before(target);
1195
+ }
1196
+
1197
+ // Reindex the data
1198
+ this.reindexData();
1199
+
1200
+ }, this));
1201
+ },
1202
+
1203
+ onBlockRender: function(){
1204
+ // We need to setup this block for reordering
1205
+ /* Setup the upload button */
1206
+ this.$dropzone.find('button').bind('click', halt);
1207
+ this.$dropzone.find('input').on('change', _.bind(function(ev){
1208
+ this.onDrop(ev.currentTarget);
1209
+ }, this));
1210
+ },
1211
+
1212
+ reindexData: function() {
1213
+ var dataStruct = this.getData();
1214
+ dataStruct = [];
1215
+
1216
+ _.each(this.$$('li.gallery-item'), function(li){
1217
+ li = $(li);
1218
+ dataStruct.push(li.data('block'));
1219
+ });
1220
+
1221
+ this.setData(dataStruct);
1222
+ },
1223
+
1224
+ onDrop: function(transferData){
1225
+
1226
+ if (transferData.files.length > 0) {
1227
+ // Multi files 'ere
1228
+ var l = transferData.files.length,
1229
+ file, urlAPI = (typeof URL !== "undefined") ? URL : (typeof webkitURL !== "undefined") ? webkitURL : null;
1230
+
1231
+ this.loading();
1232
+
1233
+ while (l--) {
1234
+ file = transferData.files[l];
1235
+ if (/image/.test(file.type)) {
1236
+ // Inc the upload count
1237
+ this.uploadsCount += 1;
1238
+ this.$editor.show();
1239
+
1240
+ /* Upload */
1241
+ this.uploader(file, function(data){
1242
+
1243
+ this.uploadsCount -= 1;
1244
+ var dataStruct = this.getData();
1245
+ data = { type: "image", data: data };
1246
+
1247
+ // Add to our struct
1248
+ if (!_.isArray(dataStruct)) {
1249
+ dataStruct = [];
1250
+ }
1251
+ dataStruct.push(data);
1252
+ this.setData(dataStruct);
1253
+
1254
+ // Pass this off to our render gallery thumb method
1255
+ this.renderGalleryThumb(data);
1256
+
1257
+ if(this.uploadsCount === 0) {
1258
+ this.ready();
1259
+ }
1260
+ });
1261
+ }
1262
+ }
1263
+ }
1264
+ }
1265
+
1266
+ });
1267
+ /*
1268
+ Simple Image Block
1269
+ */
1270
+
1271
+ var dropzone_templ = "<p>Drop image here</p><div class=\"input submit\"><input type=\"file\" /></div><button>...or choose a file</button>";
1272
+
1273
+
1274
+ SirTrevor.Blocks.Image = SirTrevor.Block.extend({
1275
+
1276
+ title: "Image",
1277
+ className: "image",
1278
+ dropEnabled: true,
1279
+
1280
+ dropzoneHTML: dropzone_templ,
1281
+
1282
+ loadData: function(data){
1283
+ // Create our image tag
1284
+ this.$editor.html($('<img>', {
1285
+ src: data.file.url
1286
+ }));
1287
+ },
1288
+
1289
+ onBlockRender: function(){
1290
+ /* Setup the upload button */
1291
+ this.$dropzone.find('button').bind('click', halt);
1292
+ this.$dropzone.find('input').on('change', _.bind(function(ev){
1293
+ this.onDrop(ev.currentTarget);
1294
+ }, this));
1295
+ },
1296
+
1297
+ onDrop: function(transferData){
1298
+ var file = transferData.files[0],
1299
+ urlAPI = (typeof URL !== "undefined") ? URL : (typeof webkitURL !== "undefined") ? webkitURL : null;
1300
+
1301
+ // Handle one upload at a time
1302
+ if (/image/.test(file.type)) {
1303
+ this.loading();
1304
+ // Show this image on here
1305
+ this.$dropzone.hide();
1306
+ this.$editor.html($('<img>', {
1307
+ src: urlAPI.createObjectURL(file)
1308
+ }));
1309
+ this.$editor.show();
1310
+
1311
+ // Upload!
1312
+ SirTrevor.publish('setSubmitButton', ['Please wait...']);
1313
+ this.uploader(
1314
+ file,
1315
+ function(data){
1316
+ // Store the data on this block
1317
+ this.setData(data);
1318
+ // Done
1319
+ this.ready();
1320
+ },
1321
+ function(error){
1322
+ alert('Error!');
1323
+ }
1324
+ );
1325
+ }
1326
+ }
1327
+ });
1328
+ /*
1329
+ Text Block
1330
+ */
1331
+
1332
+ var tb_template =
1333
+
1334
+ SirTrevor.Blocks.Text = SirTrevor.Block.extend({
1335
+
1336
+ title: "Text",
1337
+ className: "text",
1338
+ limit: 0,
1339
+
1340
+ editorHTML: '<div class="required text-block" contenteditable="true"></div>',
1341
+
1342
+ loadData: function(data){
1343
+ this.$$('.text-block').html(this.instance._toHTML(data.text, this.type));
1344
+ }
1345
+ });
1346
+ var t_template = '<p>Drop tweet link here</p><div class="input text"><label>or paste URL:</label><input type="text" class="paste-block"></div>';
1347
+ var tweet_template = '<div class="tweet media"><div class="img"><img src="<%= user.profile_image_url %>" class="tweet-avatar"></div><div class="bd tweet-body"><p><a href="http://twitter.com/#!/<%= user.screen_name %>">@<%= user.screen_name %></a>: <%= text %></p><time><%= created_at %></time></div></div>';
1348
+
1349
+ SirTrevor.Blocks.Tweet = SirTrevor.Block.extend({
1350
+
1351
+ title: "Tweet",
1352
+ className: "tweet",
1353
+ dropEnabled: true,
1354
+
1355
+ dropzoneHTML: t_template,
1356
+
1357
+ loadData: function(data){
1358
+ this.$editor.html(_.template(tweet_template, data));
1359
+ },
1360
+
1361
+ onContentPasted: function(event){
1362
+ // Content pasted. Delegate to the drop parse method
1363
+ var input = $(event.target),
1364
+ val = input.val();
1365
+
1366
+ // Pass this to the same handler as onDrop
1367
+ this.handleTwitterDropPaste(val);
1368
+ },
1369
+
1370
+ handleTwitterDropPaste: function(url){
1371
+
1372
+ if(_.isURI(url))
1373
+ {
1374
+ if (url.indexOf("twitter") != -1 && url.indexOf("status") != -1) {
1375
+ // Twitter status
1376
+ var tweetID = url.match(/[^\/]+$/);
1377
+ if (!_.isEmpty(tweetID)) {
1378
+
1379
+ this.loading();
1380
+
1381
+ tweetID = tweetID[0];
1382
+
1383
+ var tweetCallbackSuccess = function(data) {
1384
+ // Parse the twitter object into something a bit slimmer..
1385
+ var obj = {
1386
+ user: {
1387
+ profile_image_url: data.user.profile_image_url,
1388
+ profile_image_url_https: data.user.profile_image_url_https,
1389
+ screen_name: data.user.screen_name,
1390
+ name: data.user.name
1391
+ },
1392
+ text: data.text,
1393
+ created_at: data.created_at,
1394
+ status_url: url
1395
+ };
1396
+
1397
+ // Save this data on the block
1398
+ this.setData(obj);
1399
+ this._loadData();
1400
+
1401
+ this.ready();
1402
+ };
1403
+
1404
+ var tweetCallbackFail = function(){
1405
+ this.ready();
1406
+ };
1407
+
1408
+ // Make our AJAX call
1409
+ $.ajax({
1410
+ url: "//api.twitter.com/1/statuses/show/" + tweetID + ".json",
1411
+ dataType: "JSONP",
1412
+ success: _.bind(tweetCallbackSuccess, this),
1413
+ error: _.bind(tweetCallbackFail, this)
1414
+ });
1415
+ }
1416
+ }
1417
+ }
1418
+
1419
+ },
1420
+
1421
+ onDrop: function(transferData){
1422
+ var url = transferData.getData('text/plain');
1423
+ this.handleTwitterDropPaste(url);
1424
+ }
1425
+ });
1426
+
1427
+ /*
1428
+ Unordered List
1429
+ */
1430
+
1431
+ var template = '<div class="text-block <%= className %>" contenteditable="true"></div>';
1432
+
1433
+ SirTrevor.Blocks.Ul = SirTrevor.Block.extend({
1434
+
1435
+ title: "List",
1436
+ className: "list",
1437
+
1438
+ editorHTML: function() {
1439
+ return _.template(template, this);
1440
+ },
1441
+
1442
+ onBlockRender: function() {
1443
+ this.$$('.text-block').bind('click', function(){
1444
+ if($(this).html().length === 0){
1445
+ document.execCommand("insertUnorderedList",false,false);
1446
+ }
1447
+ });
1448
+
1449
+ // Put in a list
1450
+ if (_.isEmpty(this.data)) {
1451
+ this.$$('.text-block').focus().click();
1452
+ }
1453
+
1454
+ },
1455
+
1456
+ loadData: function(data){
1457
+ this.$$('.text-block').html("<ul>" + this.instance._toHTML(data.text, this.type) + "</ul>");
1458
+ },
1459
+
1460
+ toMarkdown: function(markdown) {
1461
+ return markdown.replace(/<\/li>/mg,"\n")
1462
+ .replace(/<\/?[^>]+(>|$)/g, "")
1463
+ .replace(/^(.+)$/mg," - $1");
1464
+ },
1465
+
1466
+ toHTML: function(html) {
1467
+ html = html.replace(/^ - (.+)$/mg,"<li>$1</li>")
1468
+ .replace(/\n/mg,"");
1469
+
1470
+ html = "<ul>" + html + "</ul>"
1471
+
1472
+ return html
1473
+ }
1474
+
1475
+ });
1476
+
1477
+ var video_drop_template = '<p>Drop video link here</p><div class="input text"><label>or paste URL:</label><input type="text" class="paste-block"></div>';
1478
+ var video_regex = /http[s]?:\/\/(?:www.)?(?:(vimeo).com\/(.*))|(?:(youtu(?:be)?).(?:be|com)\/(?:watch\?v=)?([^&]*)(?:&(?:.))?)/;
1479
+
1480
+ SirTrevor.Blocks.Video = SirTrevor.Block.extend({
1481
+
1482
+ title: "Video",
1483
+ className: "video",
1484
+ dropEnabled: true,
1485
+
1486
+ dropzoneHTML: video_drop_template,
1487
+
1488
+ loadData: function(data){
1489
+ if(data.source == "youtube" || data.source == "youtu") {
1490
+ this.$editor.html("<iframe src=\""+window.location.protocol+"//www.youtube.com/embed/" + data.remote_id + "\" width=\"580\" height=\"320\" frameborder=\"0\" allowfullscreen></iframe>");
1491
+ } else if(data.source == "vimeo") {
1492
+ this.$editor.html("<iframe src=\""+window.location.protocol+"//player.vimeo.com/video/" + data.remote_id + "?title=0&byline=0\" width=\"580\" height=\"320\" frameborder=\"0\"></iframe>");
1493
+ }
1494
+ },
1495
+
1496
+ onContentPasted: function(event){
1497
+ // Content pasted. Delegate to the drop parse method
1498
+ var input = $(event.target),
1499
+ val = input.val();
1500
+
1501
+ // Pass this to the same handler as onDrop
1502
+ this.handleDropPaste(val);
1503
+ },
1504
+
1505
+ handleDropPaste: function(url){
1506
+
1507
+ if(_.isURI(url))
1508
+ {
1509
+ if (url.indexOf("youtu") != -1 || url.indexOf("vimeo") != -1) {
1510
+
1511
+ var data = {},
1512
+ videos = url.match(video_regex);
1513
+
1514
+ // Work out the source and extract ID
1515
+ if(videos[3] !== undefined) {
1516
+ data.source = videos[3];
1517
+ data.remote_id = videos[4];
1518
+ } else if (videos[1] !== undefined) {
1519
+ data.source = videos[1];
1520
+ data.remote_id = videos[2];
1521
+ }
1522
+
1523
+ if (data.source == "youtu") {
1524
+ data.source = "youtube";
1525
+ }
1526
+
1527
+ // Save the data
1528
+ this.setData(data);
1529
+
1530
+ // Render
1531
+ this._loadData();
1532
+ }
1533
+ }
1534
+
1535
+ },
1536
+
1537
+ onDrop: function(transferData){
1538
+ var url = transferData.getData('text/plain');
1539
+ this.handleDropPaste(url);
1540
+ }
1541
+ });
1542
+
1543
+ /* Default Formatters */
1544
+ /* Our base formatters */
1545
+
1546
+ var Bold = SirTrevor.Formatter.extend({
1547
+ title: "B",
1548
+ className: "bold",
1549
+ cmd: "bold",
1550
+ keyCode: 66
1551
+ });
1552
+
1553
+ var Italic = SirTrevor.Formatter.extend({
1554
+ title: "I",
1555
+ className: "italic",
1556
+ cmd: "italic",
1557
+ keyCode: 73
1558
+ });
1559
+
1560
+ var Underline = SirTrevor.Formatter.extend({
1561
+ title: "U",
1562
+ className: "underline",
1563
+ cmd: "underline"
1564
+ });
1565
+
1566
+ var Link = SirTrevor.Formatter.extend({
1567
+
1568
+ title: "Link",
1569
+ className: "link",
1570
+ cmd: "CreateLink",
1571
+
1572
+ onClick: function() {
1573
+
1574
+ var link = prompt("Enter a link"),
1575
+ link_regex = /(ftp|http|https):\/\/./;
1576
+
1577
+ if(link && link.length > 0) {
1578
+
1579
+ if (!link_regex.test(link)) {
1580
+ link = "http://" + link;
1581
+ }
1582
+
1583
+ document.execCommand(this.cmd, false, link);
1584
+ }
1585
+ }
1586
+ });
1587
+
1588
+ var UnLink = SirTrevor.Formatter.extend({
1589
+ title: "Unlink",
1590
+ className: "link",
1591
+ cmd: "unlink"
1592
+ });
1593
+
1594
+ var Heading1 = SirTrevor.Formatter.extend({
1595
+
1596
+ title: "H1",
1597
+ className: "heading h1",
1598
+ cmd: "formatBlock",
1599
+ param: "H1",
1600
+
1601
+ toMarkdown: function(markdown) {
1602
+ return markdown.replace(/<h1>([^*|_]+)<\/h1>/mg,"#$1#\n");
1603
+ },
1604
+
1605
+ toHTML: function(html) {
1606
+ return html.replace(/(?:#)([^*|_]+)(?:#)/mg,"<h1>$1</h1>");
1607
+ }
1608
+ });
1609
+
1610
+ var Heading2 = SirTrevor.Formatter.extend({
1611
+ title: "H2",
1612
+ className: "heading h2",
1613
+ cmd: "formatBlock",
1614
+ param: "H2",
1615
+
1616
+ toMarkdown: function(markdown) {
1617
+ return markdown.replace(/<h2>([^*|_]+)<\/h2>/mg,"##$1##\n");
1618
+ },
1619
+
1620
+ toHTML: function(html) {
1621
+ return html.replace(/(?:##)([^*|_]+)(?:##)/mg,"<h2>$1</h2>");
1622
+ }
1623
+ });
1624
+
1625
+ /*
1626
+ Create our formatters and add a static reference to them
1627
+ */
1628
+ SirTrevor.Formatters.Bold = new Bold();
1629
+ SirTrevor.Formatters.Italic = new Italic();
1630
+ SirTrevor.Formatters.Link = new Link();
1631
+ SirTrevor.Formatters.Unlink = new UnLink();
1632
+ //SirTrevor.Formatters.Heading1 = new Heading1();
1633
+ //SirTrevor.Formatters.Heading2 = new Heading2();
1634
+ /* Marker */
1635
+ /*
1636
+ SirTrevor Marker
1637
+ --
1638
+ This is our toolbar. It's attached to a SirTrveor.Editor instance.
1639
+ */
1640
+
1641
+ var Marker = SirTrevor.Marker = function(options, editorInstance){
1642
+ this.instance = editorInstance;
1643
+ this.options = _.extend({}, SirTrevor.DEFAULTS.marker, options || {});
1644
+ this._bindFunctions();
1645
+ };
1646
+
1647
+ _.extend(Marker.prototype, FunctionBind, {
1648
+
1649
+ bound: ["onButtonClick", "show", "hide", "onDrop"],
1650
+
1651
+ render: function() {
1652
+
1653
+ var marker = $('<span>', {
1654
+ 'class': this.instance.options.baseCSSClass + "-" + this.options.baseCSSClass,
1655
+ html: '<p>' + this.options.addText + '</p><div class="buttons"></div>'
1656
+ });
1657
+
1658
+ // Bind to the wrapper
1659
+ this.instance.$wrapper.append(marker);
1660
+
1661
+ // Cache our elements for later use
1662
+ this.$el = marker;
1663
+ this.$btns = this.$el.find('.buttons');
1664
+ this.$p = this.$el.find('p');
1665
+
1666
+ // Add all of our buttons
1667
+ var blockName, block;
1668
+
1669
+ for (blockName in this.instance.blockTypes) {
1670
+ if (SirTrevor.Blocks.hasOwnProperty(blockName)) {
1671
+ block = SirTrevor.Blocks[blockName];
1672
+ if (block.prototype.toolbarEnabled) {
1673
+ this.$btns.append(
1674
+ $("<a>", {
1675
+ "href": "#",
1676
+ "class": this.options.buttonClass + " new-" + block.prototype.className,
1677
+ "data-type": blockName,
1678
+ "text": block.prototype.title,
1679
+ click: this.onButtonClick
1680
+ })
1681
+ );
1682
+ }
1683
+ }
1684
+ }
1685
+
1686
+ // Do we have any buttons?
1687
+ if(this.$btns.children().length === 0) this.$el.addClass('hidden');
1688
+
1689
+ // Bind our marker to the wrapper
1690
+ this.instance.$outer.bind('mouseover', this.show);
1691
+ this.instance.$outer.bind('mouseout', this.hide);
1692
+ this.instance.$outer.bind('dragover', this.show);
1693
+ this.$el.bind('dragover',halt);
1694
+
1695
+ // Bind the drop function onto here
1696
+ this.instance.$outer.dropArea();
1697
+ this.instance.$outer.bind('dragleave', this.hide);
1698
+ this.instance.$outer.bind('drop', this.onDrop);
1699
+
1700
+ this.$el.addClass('sir-trevor-item-ready');
1701
+ },
1702
+
1703
+ show: function(ev){
1704
+
1705
+ if(ev.type == 'drag' || ev.type == 'dragover') {
1706
+ this.$p.text(this.options.dropText);
1707
+ this.$btns.hide();
1708
+ } else {
1709
+ this.$p.text(this.options.addText);
1710
+ this.$btns.show();
1711
+ }
1712
+
1713
+ var mouse_enter = (ev) ? ev.originalEvent.pageY - this.instance.$wrapper.offset().top : 0;
1714
+
1715
+ // Do we have any sedit blocks?
1716
+ if (this.instance.blocks.length > 0) {
1717
+
1718
+ // Find the closest block to this position
1719
+ var closest_block = false,
1720
+ wrapper = this.instance.$wrapper,
1721
+ blockClass = "." + this.instance.options.baseCSSClass + "-block";
1722
+
1723
+ var blockIterator = function(block, index) {
1724
+ block = $(block);
1725
+ var block_top = block.position().top - 40,
1726
+ block_bottom = block.position().top + block.outerHeight(true) - 40;
1727
+
1728
+ if(block_top <= mouse_enter && mouse_enter < block_bottom) {
1729
+ closest_block = block;
1730
+ }
1731
+ };
1732
+ _.each(wrapper.find(blockClass), _.bind(blockIterator, this));
1733
+
1734
+ // Position it
1735
+ if (closest_block) {
1736
+ this.$el.insertBefore(closest_block);
1737
+ } else if(mouse_enter > 0) {
1738
+ this.$el.insertAfter(wrapper.find(blockClass).last());
1739
+ } else {
1740
+ this.$el.insertBefore(wrapper.find(blockClass).first());
1741
+ }
1742
+ }
1743
+ this.$el.addClass('sir-trevor-item-ready');
1744
+ },
1745
+
1746
+ hide: function(ev){
1747
+ this.$el.removeClass('sir-trevor-item-ready');
1748
+ },
1749
+
1750
+ onDrop: function(ev){
1751
+ ev.preventDefault();
1752
+
1753
+ var marker = this.$el,
1754
+ item_id = ev.originalEvent.dataTransfer.getData("text/plain"),
1755
+ block = $('#' + item_id);
1756
+
1757
+ if (!_.isUndefined(item_id) && !_.isEmpty(block) && block.attr('data-instance') == this.instance.ID) {
1758
+ marker.after(block);
1759
+ }
1760
+ },
1761
+
1762
+ remove: function(){ this.$el.remove(); },
1763
+
1764
+ onButtonClick: function(ev){
1765
+ halt(ev);
1766
+ var button = $(ev.target);
1767
+
1768
+ if (button.hasClass('inactive')) {
1769
+ alert('You cannot create any more blocks of this type');
1770
+ return false;
1771
+ }
1772
+
1773
+ this.instance.createBlock(button.attr('data-type'), {});
1774
+ },
1775
+
1776
+ move: function(top) {
1777
+ this.$el.css({
1778
+ top: top
1779
+ });
1780
+ this.$el.show();
1781
+ this.$el.addClass('sir-trevor-item-ready');
1782
+ }
1783
+ });
1784
+
1785
+
1786
+
1787
+
1788
+ /* FormatBar */
1789
+ /*
1790
+ Format Bar
1791
+ --
1792
+ Displayed on focus on a text area.
1793
+ Renders with all available options for the editor instance
1794
+ */
1795
+
1796
+ var FormatBar = SirTrevor.FormatBar = function(options, editorInstance) {
1797
+ this.instance = editorInstance;
1798
+ this.options = _.extend({}, SirTrevor.DEFAULTS.formatBar, options || {});
1799
+ this.className = this.instance.options.baseCSSClass + "-" + this.options.baseCSSClass;
1800
+ this.clicked = false;
1801
+ this._bindFunctions();
1802
+ };
1803
+
1804
+ _.extend(FormatBar.prototype, FunctionBind, {
1805
+
1806
+ bound: ["onFormatButtonClick"],
1807
+
1808
+ render: function(){
1809
+
1810
+ var bar = $("<div>", {
1811
+ "class": this.className
1812
+ });
1813
+
1814
+ this.instance.$wrapper.prepend(bar);
1815
+ this.$el = bar;
1816
+
1817
+ var formats = this.instance.formatters,
1818
+ formatName, format;
1819
+
1820
+ for (formatName in formats) {
1821
+ if (SirTrevor.Formatters.hasOwnProperty(formatName)) {
1822
+ format = SirTrevor.Formatters[formatName];
1823
+ $("<button>", {
1824
+ 'class': 'format-button ' + format.className,
1825
+ 'text': format.title,
1826
+ 'data-type': formatName,
1827
+ 'data-cmd': format.cmd,
1828
+ click: this.onFormatButtonClick
1829
+ }).appendTo(this.$el);
1830
+ }
1831
+ }
1832
+
1833
+ if(this.$el.find('button').length === 0) this.$el.addClass('hidden');
1834
+
1835
+ this.hide();
1836
+ this.$el.bind('mouseout', _.bind(function(ev){ halt(ev); this.clicked = false; }, this));
1837
+ this.$el.bind('mouseover', halt);
1838
+ },
1839
+
1840
+ /* Convienience methods */
1841
+ show: function(relativeEl){
1842
+ this.$el.css({
1843
+ top: relativeEl.position().top
1844
+ });
1845
+ this.$el.addClass('sir-trevor-item-ready');
1846
+ this.$el.show();
1847
+ },
1848
+
1849
+ hide: function(){
1850
+ this.clicked = false;
1851
+ this.$el.removeClass('sir-trevor-item-ready');
1852
+ this.$el.hide();
1853
+ },
1854
+
1855
+ remove: function(){ this.$el.remove(); },
1856
+
1857
+ onFormatButtonClick: function(ev){
1858
+ halt(ev);
1859
+ this.clicked = true;
1860
+ var btn = $(ev.target),
1861
+ format = SirTrevor.Formatters[btn.attr('data-type')];
1862
+
1863
+ // Do we have a click function defined on this formatter?
1864
+ if(!_.isUndefined(format.onClick) && _.isFunction(format.onClick)) {
1865
+ format.onClick(); // Delegate
1866
+ } else {
1867
+ // Call default
1868
+ document.execCommand(btn.attr('data-cmd'), false, format.param);
1869
+ }
1870
+ // Make sure we still show the bar
1871
+ this.$el.addClass('sir-trevor-item-ready');
1872
+ }
1873
+
1874
+ });
1875
+ /*
1876
+ Sir Trevor Editor
1877
+ --
1878
+ Represents one Sir Trevor editor instance (with multiple blocks)
1879
+ Each block references this instance.
1880
+ BlockTypes are global however.
1881
+ */
1882
+
1883
+ var SirTrevorEditor = SirTrevor.Editor = function(options) {
1884
+
1885
+ SirTrevor.log("Init SirTrevor.Editor");
1886
+
1887
+ this.blockTypes = {};
1888
+ this.formatters = {};
1889
+ this.blockCounts = {}; // Cached block type counts
1890
+ this.blocks = []; // Block references
1891
+ this.errors = [];
1892
+ this.options = _.extend({}, SirTrevor.DEFAULTS, options || {});
1893
+ this.ID = _.uniqueId(this.options.baseCSSClass + "-");
1894
+
1895
+ if (this._ensureAndSetElements()) {
1896
+
1897
+ this.marker = new SirTrevor.Marker(this.options.marker, this);
1898
+ this.formatBar = new SirTrevor.FormatBar(this.options.formatBar, this);
1899
+
1900
+ if(!_.isUndefined(this.options.onEditorRender) && _.isFunction(this.options.onEditorRender)) {
1901
+ this.onEditorRender = this.options.onEditorRender;
1902
+ }
1903
+
1904
+ this._setRequired();
1905
+ this._setBlocksAndFormatters();
1906
+ this._bindFunctions();
1907
+
1908
+ this.store("create", this); // Make our storage
1909
+ this.build();
1910
+
1911
+ SirTrevor.instances.push(this); // Store a reference to this instance
1912
+ SirTrevor.bindFormSubmit(this.$form);
1913
+ }
1914
+ };
1915
+
1916
+ _.extend(SirTrevorEditor.prototype, FunctionBind, {
1917
+
1918
+ bound: ['onFormSubmit'],
1919
+
1920
+ initialize: function() {},
1921
+
1922
+ /*
1923
+ Build the Editor instance.
1924
+ Check to see if we've been passed JSON already, and if not try and create a default block.
1925
+ If we have JSON then we need to build all of our blocks from this.
1926
+ */
1927
+ build: function() {
1928
+ this.$el.hide();
1929
+
1930
+ // Render marker & format bar
1931
+ this.marker.render();
1932
+ this.formatBar.render();
1933
+
1934
+ var store = this.store("read", this);
1935
+
1936
+ if (store.data.length === 0) {
1937
+ // Create a default instance
1938
+ this.createBlock(this.options.defaultType);
1939
+ } else {
1940
+ // We have data. Build our blocks from here.
1941
+ _.each(store.data, _.bind(function(block){
1942
+ SirTrevor.log('Creating: ', block);
1943
+ this.createBlock(block.type, block.data);
1944
+ }, this));
1945
+ }
1946
+
1947
+ this.$wrapper.addClass('sir-trevor-ready');
1948
+
1949
+ if(!_.isUndefined(this.onEditorRender)) {
1950
+ this.onEditorRender();
1951
+ }
1952
+ },
1953
+
1954
+ store: function(){
1955
+ return SirTrevor.editorStore.apply(this, arguments);
1956
+ },
1957
+
1958
+ /*
1959
+ Create an instance of a block from an available type.
1960
+ We have to check the number of blocks we're allowed to create before adding one and handle fails accordingly.
1961
+ A block will have a reference to an Editor instance & the parent BlockType.
1962
+ We also have to remember to store static counts for how many blocks we have, and keep a nice array of all the blocks available.
1963
+ */
1964
+ createBlock: function(type, data) {
1965
+
1966
+ type = _.capitalize(type); // Proper case
1967
+
1968
+ if (this._blockTypeAvailable(type)) {
1969
+
1970
+ var blockType = SirTrevor.Blocks[type],
1971
+ currentBlockCount = (_.isUndefined(this.blockCounts[type])) ? 0 : this.blockCounts[type],
1972
+ totalBlockCounts = this.blocks.length,
1973
+ blockTypeLimit = this._getBlockTypeLimit(type);
1974
+
1975
+ // Can we have another one of these blocks?
1976
+ if ((blockTypeLimit !== 0 && currentBlockCount > blockTypeLimit) || this.options.blockLimit !== 0 && totalBlockCounts >= this.options.blockLimit) {
1977
+ SirTrevor.log("Block Limit reached for type " + type);
1978
+ return false;
1979
+ }
1980
+
1981
+ var block = new blockType(this, data || {});
1982
+
1983
+ if (_.isUndefined(this.blockCounts[type])) {
1984
+ this.blockCounts[type] = 0;
1985
+ }
1986
+
1987
+ this.blocks.push(block);
1988
+ currentBlockCount++;
1989
+ this.blockCounts[type] = currentBlockCount;
1990
+
1991
+ // Check to see if we can add any more blocks
1992
+ if (this.options.blockLimit !== 0 && this.blocks.length >= this.options.blockLimit) {
1993
+ this.marker.$el.addClass('hidden');
1994
+ }
1995
+
1996
+ if (blockTypeLimit !== 0 && currentBlockCount >= blockTypeLimit) {
1997
+ SirTrevor.log("Block Limit reached for type " + type + " setting state as inactive");
1998
+ this.marker.$el.find('[data-type="' + type + '"]')
1999
+ .addClass('inactive')
2000
+ .attr('title','You have reached the limit for this type of block');
2001
+ }
2002
+
2003
+ SirTrevor.publish("editor/block/createBlock");
2004
+
2005
+ SirTrevor.log("Block created of type " + type);
2006
+ } else {
2007
+ SirTrevor.log("Block type not available " + type);
2008
+ }
2009
+ },
2010
+
2011
+ removeBlock: function(block) {
2012
+ // Blocks exist purely on the dom.
2013
+ // Remove the block and decrement the blockCount
2014
+ block.remove();
2015
+ this.blockCounts[block.type] = this.blockCounts[block.type] - 1;
2016
+
2017
+ // Remove the block from our store
2018
+ this.blocks = _.reject(this.blocks, function(item){ return (item.blockID == block.blockID); });
2019
+ if(_.isUndefined(this.blocks)) this.blocks = [];
2020
+ this.formatBar.hide();
2021
+
2022
+ SirTrevor.publish("editor/block/removeBlock");
2023
+
2024
+ // Remove our inactive class if it's no longer relevant
2025
+ if(this._getBlockTypeLimit(block.type) > this.blockCounts[block.type]) {
2026
+ SirTrevor.log("Removing block limit for " + block.type);
2027
+ this.marker.$el.find('[data-type="' + block.type + '"]')
2028
+ .removeClass('inactive')
2029
+ .attr('title','Add a ' + block.type + ' block');
2030
+ }
2031
+ },
2032
+
2033
+ performValidations : function(_block, should_validate) {
2034
+
2035
+ var errors = 0;
2036
+
2037
+ if (!SirTrevor.SKIP_VALIDATION && should_validate) {
2038
+ if(!_block.validate()){
2039
+ // fail validations
2040
+ SirTrevor.log("Block " + _block.blockID + " failed validation");
2041
+ ++errors;
2042
+ }
2043
+ } else {
2044
+ // not validating so clear validation warnings
2045
+ _block._beforeValidate();
2046
+ }
2047
+
2048
+ // success
2049
+ var store = _block.save();
2050
+ if(!_.isEmpty(store.data)) {
2051
+ SirTrevor.log("Adding data for block " + _block.blockID + " to block store");
2052
+ this.store("add", this, { data: store });
2053
+ }
2054
+ return errors;
2055
+ },
2056
+
2057
+ /*
2058
+ Handle a form submission of this Editor instance.
2059
+ Validate all of our blocks, and serialise all data onto the JSON objects
2060
+ */
2061
+ onFormSubmit: function(should_validate) {
2062
+
2063
+ // if undefined or null or anything other than false - treat as true
2064
+ should_validate = (should_validate === false) ? false : true;
2065
+
2066
+ SirTrevor.log("Handling form submission for Editor " + this.ID);
2067
+
2068
+ var blockLength, block, result, errors = 0;
2069
+
2070
+ this.formatBar.hide();
2071
+ this.removeErrors();
2072
+ // Reset our store
2073
+ this.store("reset", this);
2074
+
2075
+ // Loop through blocks to validate
2076
+ var blockIterator = function(block,index) {
2077
+ // Find our block
2078
+ block = $(block);
2079
+ var _block = _.find(this.blocks, function(b){ return (b.blockID == block.attr('id')); });
2080
+
2081
+ if (!_.isUndefined(_block) || !_.isEmpty(_block) || typeof _block == SirTrevor.Block) {
2082
+ // Validate our block
2083
+ errors += this.performValidations(_block, should_validate);
2084
+ }
2085
+
2086
+ };
2087
+ _.each(this.$wrapper.find('.' + this.options.baseCSSClass + "-block"), _.bind(blockIterator, this));
2088
+
2089
+ // Validate against our required fields (if there are any)
2090
+ if (this.required && (!SirTrevor.SKIP_VALIDATION && should_validate)) {
2091
+ _.each(this.required, _.bind(function(type) {
2092
+
2093
+ if (this._blockTypeAvailable(type)) {
2094
+ // Valid block type to validate against
2095
+ if (_.isUndefined(this.blockCounts[type]) || this.blockCounts[type] === 0) {
2096
+
2097
+ this.errors.push({ text: "You must have a block of type " + type });
2098
+
2099
+ SirTrevor.log("Failed validation on required block type " + type);
2100
+ errors++;
2101
+
2102
+ } else {
2103
+ // We need to also validate that we have some data of this type too.
2104
+ // This is ugly, but necessary for proper validation on blocks that don't have required fields.
2105
+ var blocks = _.filter(this.blocks, function(b){ return (b.type == type && !_.isEmpty(b.getData())); });
2106
+
2107
+ if (blocks.length === 0) {
2108
+ this.errors.push({ text: "A required block type " + type + " is empty" });
2109
+ errors++;
2110
+ SirTrevor.log("A required block type " + type + " is empty");
2111
+ }
2112
+
2113
+ }
2114
+ }
2115
+ }, this));
2116
+ }
2117
+
2118
+ // Save it
2119
+ this.store("save", this);
2120
+
2121
+ if (errors > 0) this.renderErrors();
2122
+
2123
+ return errors;
2124
+ },
2125
+
2126
+ renderErrors: function() {
2127
+ if (this.errors.length > 0) {
2128
+
2129
+ if (_.isUndefined(this.$errors)) {
2130
+ this.$errors = $("<div>", {
2131
+ class: this.options.baseCSSClass + "-errors",
2132
+ html: "<p>You have the following errors: </p><ul></ul>"
2133
+ });
2134
+ this.$outer.prepend(this.$errors);
2135
+ }
2136
+
2137
+ var list = this.$errors.find('ul');
2138
+
2139
+ _.each(this.errors, _.bind(function(error) {
2140
+ list.append($("<li>", {
2141
+ class: "error-msg",
2142
+ html: error.text
2143
+ }));
2144
+ }, this));
2145
+
2146
+ this.$errors.show();
2147
+ }
2148
+ },
2149
+
2150
+ removeErrors: function() {
2151
+ if (this.errors.length > 0) {
2152
+ // We have old errors to remove
2153
+ this.$errors.find('ul').html('');
2154
+ this.$errors.hide();
2155
+ this.errors = [];
2156
+ }
2157
+ },
2158
+
2159
+ /*
2160
+ Get Block Type Limit
2161
+ --
2162
+ returns the limit for this block, which can be set on a per Editor instance, or on a global blockType scope.
2163
+ */
2164
+ _getBlockTypeLimit: function(t) {
2165
+ if (this._blockTypeAvailable(t)) {
2166
+ return (_.isUndefined(this.options.blockTypeLimits[t])) ? SirTrevor.Blocks[t].prototype.limit : this.options.blockTypeLimits[t];
2167
+ }
2168
+ return 0;
2169
+ },
2170
+
2171
+ /*
2172
+ Availability helper methods
2173
+ --
2174
+ Checks if the object exists within the instance of the Editor.
2175
+ */
2176
+ _blockTypeAvailable: function(t) {
2177
+ return !_.isUndefined(this.blockTypes[t]);
2178
+ },
2179
+
2180
+ _formatterAvailable: function(f) {
2181
+ return !_.isUndefined(this.formatters[f]);
2182
+ },
2183
+
2184
+ _ensureAndSetElements: function() {
2185
+
2186
+ if(_.isUndefined(this.options.el) || _.isEmpty(this.options.el)) {
2187
+ SirTrevor.log("You must provide an el");
2188
+ return false;
2189
+ }
2190
+
2191
+ this.$el = this.options.el;
2192
+ this.el = this.options.el[0];
2193
+ this.$form = this.$el.parents('form');
2194
+
2195
+ // Wrap our element in lots of containers *eww*
2196
+ this.$el.wrap($('<div>', {
2197
+ id: this.ID,
2198
+ 'class': this.options.baseCSSClass,
2199
+ dropzone: 'copy link move'
2200
+ })
2201
+ )
2202
+ .wrap($("<div>", {
2203
+ class: this.options.baseCSSClass + "-blocks"
2204
+ }));
2205
+
2206
+ this.$outer = this.$form.find('#' + this.ID);
2207
+ this.$wrapper = this.$outer.find("." + this.options.baseCSSClass + "-blocks");
2208
+ return true;
2209
+ },
2210
+
2211
+
2212
+ /*
2213
+ Set our blockTypes and formatters.
2214
+ These will either be set on a per Editor instance, or set on a global scope.
2215
+ */
2216
+ _setBlocksAndFormatters: function() {
2217
+ this.blockTypes = flattern((_.isUndefined(this.options.blockTypes)) ? SirTrevor.Blocks : this.options.blockTypes);
2218
+ this.formatters = flattern((_.isUndefined(this.options.formatters)) ? SirTrevor.Formatters : this.options.formatters);
2219
+ },
2220
+
2221
+ /* Get our required blocks (if any) */
2222
+ _setRequired: function() {
2223
+ this.required = (_.isArray(this.options.required) && !_.isEmpty(this.options.required)) ? this.options.required : false;
2224
+ },
2225
+
2226
+ /*
2227
+ A very generic HTML -> Markdown parser
2228
+ Looks for available formatters / blockTypes toMarkdown methods and calls these if they exist.
2229
+ */
2230
+ _toMarkdown: function(content, type) {
2231
+
2232
+ var markdown;
2233
+
2234
+ markdown = content.replace(/\n/mg,"")
2235
+ .replace(/<a.*?href=[""'](.*?)[""'].*?>(.*?)<\/a>/g,"[$2]($1)") // Hyperlinks
2236
+ .replace(/<\/?b>/g,"**")
2237
+ .replace(/<\/?STRONG>/g,"**") // Bold
2238
+ .replace(/<\/?i>/g,"_")
2239
+ .replace(/<\/?EM>/g,"_"); // Italic
2240
+
2241
+ // Use custom formatters toMarkdown functions (if any exist)
2242
+ var formatName, format;
2243
+ for(formatName in this.formatters) {
2244
+ if (SirTrevor.Formatters.hasOwnProperty(formatName)) {
2245
+ format = SirTrevor.Formatters[formatName];
2246
+ // Do we have a toMarkdown function?
2247
+ if (!_.isUndefined(format.toMarkdown) && _.isFunction(format.toMarkdown)) {
2248
+ markdown = format.toMarkdown(markdown);
2249
+ }
2250
+ }
2251
+ }
2252
+
2253
+ // Do our generic stripping out
2254
+ markdown = markdown.replace(/([^<>]+)(<div>)/g,"$1\n\n$2") // Divitis style line breaks (handle the first line)
2255
+ .replace(/(?:<div>)([^<>]+)(?:<div>)/g,"$1\n\n") // ^ (handle nested divs that start with content)
2256
+ .replace(/(?:<div>)(?:<br>)?([^<>]+)(?:<br>)?(?:<\/div>)/g,"$1\n\n") // ^ (handle content inside divs)
2257
+ .replace(/<\/p>/g,"\n\n\n\n") // P tags as line breaks
2258
+ .replace(/<(.)?br(.)?>/g,"\n\n") // Convert normal line breaks
2259
+ .replace(/&nbsp;/g," ") // Strip white-space entities
2260
+ .replace(/&lt;/g,"<").replace(/&gt;/g,">"); // Encoding
2261
+
2262
+
2263
+ // Use custom block toMarkdown functions (if any exist)
2264
+ var block;
2265
+ if (SirTrevor.Blocks.hasOwnProperty(type)) {
2266
+ block = SirTrevor.Blocks[type];
2267
+ // Do we have a toMarkdown function?
2268
+ if (!_.isUndefined(block.prototype.toMarkdown) && _.isFunction(block.prototype.toMarkdown)) {
2269
+ markdown = block.prototype.toMarkdown(markdown);
2270
+ }
2271
+ }
2272
+
2273
+ // Strip remaining HTML
2274
+ markdown = markdown.replace(/<\/?[^>]+(>|$)/g, "");
2275
+
2276
+
2277
+ return markdown;
2278
+ },
2279
+
2280
+ /*
2281
+ A very generic Markdown -> HTML parser
2282
+ Looks for available formatters / blockTypes toMarkdown methods and calls these if they exist.
2283
+ */
2284
+ _toHTML: function(markdown, type) {
2285
+ var html = markdown;
2286
+
2287
+ // Use custom formatters toHTML functions (if any exist)
2288
+ var formatName, format;
2289
+ for(formatName in this.formatters) {
2290
+ if (SirTrevor.Formatters.hasOwnProperty(formatName)) {
2291
+ format = SirTrevor.Formatters[formatName];
2292
+ // Do we have a toHTML function?
2293
+ if (!_.isUndefined(format.toHTML) && _.isFunction(format.toHTML)) {
2294
+ html = format.toHTML(html);
2295
+ }
2296
+ }
2297
+ }
2298
+
2299
+ // Use custom block toHTML functions (if any exist)
2300
+ var block;
2301
+ if (SirTrevor.Blocks.hasOwnProperty(type)) {
2302
+
2303
+ block = SirTrevor.Blocks[type];
2304
+ // Do we have a toHTML function?
2305
+ if (!_.isUndefined(block.prototype.toHTML) && _.isFunction(block.prototype.toHTML)) {
2306
+ html = block.prototype.toHTML(html);
2307
+ }
2308
+ }
2309
+
2310
+ html = html.replace(/^\> (.+)$/mg,"$1") // Blockquotes
2311
+ .replace(/\n\n/g,"<br>") // Give me some <br>s
2312
+ .replace(/\[([^\]]+)\]\(([^\)]+)\)/g,"<a href='$2'>$1</a>") // Links
2313
+ .replace(/(?:_)([^*|_(http)]+)(?:_)/g,"<i>$1</i>") // Italic, avoid italicizing two links with underscores next to each other
2314
+ .replace(/(?:\*\*)([^*|_]+)(?:\*\*)/g,"<b>$1</b>"); // Bold
2315
+
2316
+ return html;
2317
+ }
2318
+ });
2319
+
2320
+
2321
+
2322
+ /* We need a form handler here to handle all the form submits */
2323
+
2324
+ SirTrevor.bindFormSubmit = function(form) {
2325
+ if (!formBound) {
2326
+ SirTrevor.submittable();
2327
+ form.bind('submit', this.onFormSubmit);
2328
+ formBound = true;
2329
+ }
2330
+ };
2331
+
2332
+ SirTrevor.onBeforeSubmit = function(should_validate) {
2333
+ // Loop through all of our instances and do our form submits on them
2334
+ var errors = 0;
2335
+ _.each(SirTrevor.instances, function(inst, i) {
2336
+ errors += inst.onFormSubmit(should_validate);
2337
+ });
2338
+ SirTrevor.log("Total errors: " + errors);
2339
+
2340
+ return errors;
2341
+ };
2342
+
2343
+ SirTrevor.onFormSubmit = function(ev) {
2344
+ var errors = SirTrevor.onBeforeSubmit();
2345
+
2346
+ if(errors > 0) {
2347
+ SirTrevor.publish("onError");
2348
+ ev.preventDefault();
2349
+ }
2350
+ };
2351
+
2352
+ SirTrevor.runOnAllInstances = function(method) {
2353
+ if (_.has(SirTrevor.Editor.prototype, method)) {
2354
+ // augment the arguments pseudo array and pass on to invoke()
2355
+ // this allows us to pass arguments on to the target methods
2356
+ [].unshift.call(arguments, SirTrevor.instances);
2357
+ _.invoke.apply(_, arguments);
2358
+ } else {
2359
+ SirTrevor.log("method doesn't exist");
2360
+ }
2361
+ };
2362
+
2363
+ }(jQuery, _));