sir-trevor-rails 0.1.3

Sign up to get free protection for your applications and to get access to all the features.
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, _));