grandstand 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +2 -0
- data/MIT-LICENSE +20 -0
- data/README +7 -0
- data/Rakefile +44 -0
- data/VERSION +1 -0
- data/app/controllers/admin/galleries_controller.rb +52 -0
- data/app/controllers/admin/images_controller.rb +68 -0
- data/app/controllers/admin/main_controller.rb +36 -0
- data/app/controllers/admin/pages_controller.rb +51 -0
- data/app/controllers/admin/posts_controller.rb +48 -0
- data/app/controllers/admin/sessions_controller.rb +41 -0
- data/app/controllers/admin/templates_controller.rb +6 -0
- data/app/controllers/admin/users_controller.rb +48 -0
- data/app/controllers/galleries_controller.rb +5 -0
- data/app/controllers/pages_controller.rb +5 -0
- data/app/controllers/posts_controller.rb +5 -0
- data/app/helpers/admin/main_helper.rb +31 -0
- data/app/helpers/admin/pages_helper.rb +2 -0
- data/app/helpers/admin/posts_helper.rb +2 -0
- data/app/helpers/admin/sessions_helper.rb +2 -0
- data/app/helpers/admin/templates_helper.rb +2 -0
- data/app/helpers/admin/users_helper.rb +2 -0
- data/app/helpers/pages_helper.rb +2 -0
- data/app/helpers/posts_helper.rb +2 -0
- data/app/helpers/site_helper.rb +2 -0
- data/app/models/gallery.rb +22 -0
- data/app/models/image.rb +61 -0
- data/app/models/page.rb +45 -0
- data/app/models/page_section.rb +6 -0
- data/app/models/post.rb +27 -0
- data/app/models/template.rb +33 -0
- data/app/models/user.rb +68 -0
- data/app/stylesheets/_buttons.less +76 -0
- data/app/stylesheets/_dialogs.less +85 -0
- data/app/stylesheets/application.less +238 -0
- data/app/stylesheets/global.less +435 -0
- data/app/stylesheets/login.less +30 -0
- data/app/stylesheets/wysiwyg.less +96 -0
- data/app/views/admin/galleries/_form.html.erb +11 -0
- data/app/views/admin/galleries/_gallery.html.erb +16 -0
- data/app/views/admin/galleries/_list.html.erb +17 -0
- data/app/views/admin/galleries/delete.html.erb +8 -0
- data/app/views/admin/galleries/edit.html.erb +8 -0
- data/app/views/admin/galleries/editor.html.erb +13 -0
- data/app/views/admin/galleries/editor_with_images.html.erb +19 -0
- data/app/views/admin/galleries/index.html.erb +13 -0
- data/app/views/admin/galleries/new.html.erb +8 -0
- data/app/views/admin/galleries/show.html.erb +15 -0
- data/app/views/admin/images/_form.html.erb +11 -0
- data/app/views/admin/images/delete.html.erb +8 -0
- data/app/views/admin/images/edit.html.erb +8 -0
- data/app/views/admin/images/new.html.erb +8 -0
- data/app/views/admin/images/upload.html.erb +11 -0
- data/app/views/admin/main/index.html.erb +10 -0
- data/app/views/admin/pages/_form.html.erb +33 -0
- data/app/views/admin/pages/_left.html.erb +3 -0
- data/app/views/admin/pages/_row.html.erb +9 -0
- data/app/views/admin/pages/delete.html.erb +8 -0
- data/app/views/admin/pages/edit.html.erb +8 -0
- data/app/views/admin/pages/index.html.erb +20 -0
- data/app/views/admin/pages/new.html.erb +8 -0
- data/app/views/admin/pages/show.html.erb +3 -0
- data/app/views/admin/posts/_form.html.erb +29 -0
- data/app/views/admin/posts/_left.html.erb +3 -0
- data/app/views/admin/posts/_list.html.erb +22 -0
- data/app/views/admin/posts/delete.html.erb +9 -0
- data/app/views/admin/posts/edit.html.erb +10 -0
- data/app/views/admin/posts/index.html.erb +10 -0
- data/app/views/admin/posts/new.html.erb +10 -0
- data/app/views/admin/posts/show.html.erb +4 -0
- data/app/views/admin/sessions/forgot.html.erb +8 -0
- data/app/views/admin/sessions/show.html.erb +12 -0
- data/app/views/admin/shared/_flash.html.erb +3 -0
- data/app/views/admin/users/_form.html.erb +16 -0
- data/app/views/admin/users/_left.html.erb +3 -0
- data/app/views/admin/users/delete.html.erb +10 -0
- data/app/views/admin/users/edit.html.erb +8 -0
- data/app/views/admin/users/index.html.erb +22 -0
- data/app/views/admin/users/new.html.erb +8 -0
- data/app/views/admin/users/show.html.erb +12 -0
- data/app/views/galleries/index.html.erb +0 -0
- data/app/views/galleries/show.html.erb +12 -0
- data/app/views/layouts/admin.html.erb +80 -0
- data/app/views/layouts/admin_login.html.erb +17 -0
- data/app/views/layouts/admin_xhr.html.erb +3 -0
- data/app/views/pages/show.html.erb +8 -0
- data/app/views/posts/show.html.erb +3 -0
- data/app/views/shared/404.html.erb +5 -0
- data/app/views/shared/gallery.html +14 -0
- data/app/views/shared/image.html +1 -0
- data/app/views/shared/page.html +0 -0
- data/app/views/shared/post.html +3 -0
- data/grandstand.gemspec +189 -0
- data/lib/grandstand/application.rb +50 -0
- data/lib/grandstand/controller/development.rb +15 -0
- data/lib/grandstand/controller.rb +104 -0
- data/lib/grandstand/helper.rb +117 -0
- data/lib/grandstand/routes.rb +59 -0
- data/lib/grandstand/session.rb +25 -0
- data/lib/grandstand.rb +27 -0
- data/public/.DS_Store +0 -0
- data/public/admin/.DS_Store +0 -0
- data/public/admin/images/.DS_Store +0 -0
- data/public/admin/images/background-input.gif +0 -0
- data/public/admin/images/background-progress-bar.png +0 -0
- data/public/admin/images/background-progress-complete.gif +0 -0
- data/public/admin/images/background-progress.gif +0 -0
- data/public/admin/images/icons/.DS_Store +0 -0
- data/public/admin/images/icons/add.png +0 -0
- data/public/admin/images/icons/collapse.png +0 -0
- data/public/admin/images/icons/delete.png +0 -0
- data/public/admin/images/icons/edit.png +0 -0
- data/public/admin/images/icons/editor/bold.png +0 -0
- data/public/admin/images/icons/editor/gallery.png +0 -0
- data/public/admin/images/icons/editor/image-center.png +0 -0
- data/public/admin/images/icons/editor/image-left.png +0 -0
- data/public/admin/images/icons/editor/image-right.png +0 -0
- data/public/admin/images/icons/editor/image.png +0 -0
- data/public/admin/images/icons/editor/italic.png +0 -0
- data/public/admin/images/icons/editor/ordered-list.png +0 -0
- data/public/admin/images/icons/editor/quote.png +0 -0
- data/public/admin/images/icons/editor/source.png +0 -0
- data/public/admin/images/icons/editor/strikethrough.png +0 -0
- data/public/admin/images/icons/editor/underline.png +0 -0
- data/public/admin/images/icons/editor/unordered-list.png +0 -0
- data/public/admin/images/icons/error.png +0 -0
- data/public/admin/images/icons/expand.png +0 -0
- data/public/admin/images/icons/galleries.png +0 -0
- data/public/admin/images/icons/gallery.png +0 -0
- data/public/admin/images/icons/image.png +0 -0
- data/public/admin/images/icons/okay.png +0 -0
- data/public/admin/images/icons/pages.png +0 -0
- data/public/admin/images/icons/posts.png +0 -0
- data/public/admin/images/icons/upload.png +0 -0
- data/public/admin/images/icons/users.png +0 -0
- data/public/admin/images/logo.png +0 -0
- data/public/admin/images/spinner-dark.gif +0 -0
- data/public/admin/images/uploader.swf +0 -0
- data/public/admin/javascripts/application.js +231 -0
- data/public/admin/javascripts/jquery.js +404 -0
- data/public/admin/javascripts/mustache.js +324 -0
- data/public/admin/javascripts/selection.js +280 -0
- data/public/admin/javascripts/string.js +264 -0
- data/public/admin/javascripts/wysiwyg.js +335 -0
- data/public/admin/stylesheets/application.css +1 -0
- data/public/admin/stylesheets/global.css +1 -0
- data/public/admin/stylesheets/login.css +1 -0
- data/public/admin/stylesheets/wysiwyg-content.css +20 -0
- data/public/admin/stylesheets/wysiwyg.css +1 -0
- data/vendor/cache/more-0.1.1.gem +0 -0
- metadata +216 -0
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
/*
|
|
2
|
+
mustache.js — Logic-less templates in JavaScript
|
|
3
|
+
|
|
4
|
+
See http://mustache.github.com/ for more info.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
var Mustache = function() {
|
|
8
|
+
var Renderer = function() {};
|
|
9
|
+
|
|
10
|
+
Renderer.prototype = {
|
|
11
|
+
otag: "{{",
|
|
12
|
+
ctag: "}}",
|
|
13
|
+
pragmas: {},
|
|
14
|
+
buffer: [],
|
|
15
|
+
pragmas_implemented: {
|
|
16
|
+
"IMPLICIT-ITERATOR": true
|
|
17
|
+
},
|
|
18
|
+
context: {},
|
|
19
|
+
|
|
20
|
+
render: function(template, context, partials, in_recursion) {
|
|
21
|
+
// reset buffer & set context
|
|
22
|
+
if(!in_recursion) {
|
|
23
|
+
this.context = context;
|
|
24
|
+
this.buffer = []; // TODO: make this non-lazy
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// fail fast
|
|
28
|
+
if(!this.includes("", template)) {
|
|
29
|
+
if(in_recursion) {
|
|
30
|
+
return template;
|
|
31
|
+
} else {
|
|
32
|
+
this.send(template);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
template = this.render_pragmas(template);
|
|
38
|
+
var html = this.render_section(template, context, partials);
|
|
39
|
+
if(in_recursion) {
|
|
40
|
+
return this.render_tags(html, context, partials, in_recursion);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
this.render_tags(html, context, partials, in_recursion);
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
/*
|
|
47
|
+
Sends parsed lines
|
|
48
|
+
*/
|
|
49
|
+
send: function(line) {
|
|
50
|
+
if(line != "") {
|
|
51
|
+
this.buffer.push(line);
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
|
|
55
|
+
/*
|
|
56
|
+
Looks for %PRAGMAS
|
|
57
|
+
*/
|
|
58
|
+
render_pragmas: function(template) {
|
|
59
|
+
// no pragmas
|
|
60
|
+
if(!this.includes("%", template)) {
|
|
61
|
+
return template;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
var that = this;
|
|
65
|
+
var regex = new RegExp(this.otag + "%([\\w-]+) ?([\\w]+=[\\w]+)?" +
|
|
66
|
+
this.ctag);
|
|
67
|
+
return template.replace(regex, function(match, pragma, options) {
|
|
68
|
+
if(!that.pragmas_implemented[pragma]) {
|
|
69
|
+
throw({message:
|
|
70
|
+
"This implementation of mustache doesn't understand the '" +
|
|
71
|
+
pragma + "' pragma"});
|
|
72
|
+
}
|
|
73
|
+
that.pragmas[pragma] = {};
|
|
74
|
+
if(options) {
|
|
75
|
+
var opts = options.split("=");
|
|
76
|
+
that.pragmas[pragma][opts[0]] = opts[1];
|
|
77
|
+
}
|
|
78
|
+
return "";
|
|
79
|
+
// ignore unknown pragmas silently
|
|
80
|
+
});
|
|
81
|
+
},
|
|
82
|
+
|
|
83
|
+
/*
|
|
84
|
+
Tries to find a partial in the curent scope and render it
|
|
85
|
+
*/
|
|
86
|
+
render_partial: function(name, context, partials) {
|
|
87
|
+
name = this.trim(name);
|
|
88
|
+
if(!partials || partials[name] === undefined) {
|
|
89
|
+
throw({message: "unknown_partial '" + name + "'"});
|
|
90
|
+
}
|
|
91
|
+
if(typeof(context[name]) != "object") {
|
|
92
|
+
return this.render(partials[name], context, partials, true);
|
|
93
|
+
}
|
|
94
|
+
return this.render(partials[name], context[name], partials, true);
|
|
95
|
+
},
|
|
96
|
+
|
|
97
|
+
/*
|
|
98
|
+
Renders inverted (^) and normal (#) sections
|
|
99
|
+
*/
|
|
100
|
+
render_section: function(template, context, partials) {
|
|
101
|
+
if(!this.includes("#", template) && !this.includes("^", template)) {
|
|
102
|
+
return template;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
var that = this;
|
|
106
|
+
// CSW - Added "+?" so it finds the tighest bound, not the widest
|
|
107
|
+
var regex = new RegExp(this.otag + "(\\^|\\#)\\s*(.+)\\s*" + this.ctag +
|
|
108
|
+
"\n*([\\s\\S]+?)" + this.otag + "\\/\\s*\\2\\s*" + this.ctag +
|
|
109
|
+
"\\s*", "mg");
|
|
110
|
+
|
|
111
|
+
// for each {{#foo}}{{/foo}} section do...
|
|
112
|
+
return template.replace(regex, function(match, type, name, content) {
|
|
113
|
+
var value = that.find(name, context);
|
|
114
|
+
if(type == "^") { // inverted section
|
|
115
|
+
if(!value || that.is_array(value) && value.length === 0) {
|
|
116
|
+
// false or empty list, render it
|
|
117
|
+
return that.render(content, context, partials, true);
|
|
118
|
+
} else {
|
|
119
|
+
return "";
|
|
120
|
+
}
|
|
121
|
+
} else if(type == "#") { // normal section
|
|
122
|
+
if(that.is_array(value)) { // Enumerable, Let's loop!
|
|
123
|
+
return that.map(value, function(row) {
|
|
124
|
+
return that.render(content, that.create_context(row),
|
|
125
|
+
partials, true);
|
|
126
|
+
}).join("");
|
|
127
|
+
} else if(that.is_object(value)) { // Object, Use it as subcontext!
|
|
128
|
+
return that.render(content, that.create_context(value),
|
|
129
|
+
partials, true);
|
|
130
|
+
} else if(typeof value === "function") {
|
|
131
|
+
// higher order section
|
|
132
|
+
return value.call(context, content, function(text) {
|
|
133
|
+
return that.render(text, context, partials, true);
|
|
134
|
+
});
|
|
135
|
+
} else if(value) { // boolean section
|
|
136
|
+
return that.render(content, context, partials, true);
|
|
137
|
+
} else {
|
|
138
|
+
return "";
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
},
|
|
143
|
+
|
|
144
|
+
/*
|
|
145
|
+
Replace {{foo}} and friends with values from our view
|
|
146
|
+
*/
|
|
147
|
+
render_tags: function(template, context, partials, in_recursion) {
|
|
148
|
+
// tit for tat
|
|
149
|
+
var that = this;
|
|
150
|
+
|
|
151
|
+
var new_regex = function() {
|
|
152
|
+
return new RegExp(that.otag + "(=|!|>|\\{|%)?([^\\/#\\^]+?)\\1?" +
|
|
153
|
+
that.ctag + "+", "g");
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
var regex = new_regex();
|
|
157
|
+
var tag_replace_callback = function(match, operator, name) {
|
|
158
|
+
switch(operator) {
|
|
159
|
+
case "!": // ignore comments
|
|
160
|
+
return "";
|
|
161
|
+
case "=": // set new delimiters, rebuild the replace regexp
|
|
162
|
+
that.set_delimiters(name);
|
|
163
|
+
regex = new_regex();
|
|
164
|
+
return "";
|
|
165
|
+
case ">": // render partial
|
|
166
|
+
return that.render_partial(name, context, partials);
|
|
167
|
+
case "{": // the triple mustache is unescaped
|
|
168
|
+
return that.find(name, context);
|
|
169
|
+
default: // escape the value
|
|
170
|
+
return that.escape(that.find(name, context));
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
var lines = template.split("\n");
|
|
174
|
+
for(var i = 0; i < lines.length; i++) {
|
|
175
|
+
lines[i] = lines[i].replace(regex, tag_replace_callback, this);
|
|
176
|
+
if(!in_recursion) {
|
|
177
|
+
this.send(lines[i]);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if(in_recursion) {
|
|
182
|
+
return lines.join("\n");
|
|
183
|
+
}
|
|
184
|
+
},
|
|
185
|
+
|
|
186
|
+
set_delimiters: function(delimiters) {
|
|
187
|
+
var dels = delimiters.split(" ");
|
|
188
|
+
this.otag = this.escape_regex(dels[0]);
|
|
189
|
+
this.ctag = this.escape_regex(dels[1]);
|
|
190
|
+
},
|
|
191
|
+
|
|
192
|
+
escape_regex: function(text) {
|
|
193
|
+
// thank you Simon Willison
|
|
194
|
+
if(!arguments.callee.sRE) {
|
|
195
|
+
var specials = [
|
|
196
|
+
'/', '.', '*', '+', '?', '|',
|
|
197
|
+
'(', ')', '[', ']', '{', '}', '\\'
|
|
198
|
+
];
|
|
199
|
+
arguments.callee.sRE = new RegExp(
|
|
200
|
+
'(\\' + specials.join('|\\') + ')', 'g'
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
return text.replace(arguments.callee.sRE, '\\$1');
|
|
204
|
+
},
|
|
205
|
+
|
|
206
|
+
/*
|
|
207
|
+
find `name` in current `context`. That is find me a value
|
|
208
|
+
from the view object
|
|
209
|
+
*/
|
|
210
|
+
find: function(name, context) {
|
|
211
|
+
name = this.trim(name);
|
|
212
|
+
|
|
213
|
+
// Checks whether a value is thruthy or false or 0
|
|
214
|
+
function is_kinda_truthy(bool) {
|
|
215
|
+
return bool === false || bool === 0 || bool;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
var value;
|
|
219
|
+
if(is_kinda_truthy(context[name])) {
|
|
220
|
+
value = context[name];
|
|
221
|
+
} else if(is_kinda_truthy(this.context[name])) {
|
|
222
|
+
value = this.context[name];
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if(typeof value === "function") {
|
|
226
|
+
return value.apply(context);
|
|
227
|
+
}
|
|
228
|
+
if(value !== undefined) {
|
|
229
|
+
return value;
|
|
230
|
+
}
|
|
231
|
+
// silently ignore unkown variables
|
|
232
|
+
return "";
|
|
233
|
+
},
|
|
234
|
+
|
|
235
|
+
// Utility methods
|
|
236
|
+
|
|
237
|
+
/* includes tag */
|
|
238
|
+
includes: function(needle, haystack) {
|
|
239
|
+
return haystack.indexOf(this.otag + needle) != -1;
|
|
240
|
+
},
|
|
241
|
+
|
|
242
|
+
/*
|
|
243
|
+
Does away with nasty characters
|
|
244
|
+
*/
|
|
245
|
+
escape: function(s) {
|
|
246
|
+
s = String(s === null ? "" : s);
|
|
247
|
+
return s.replace(/&(?!\w+;)|["<>\\]/g, function(s) {
|
|
248
|
+
switch(s) {
|
|
249
|
+
case "&": return "&";
|
|
250
|
+
case "\\": return "\\\\";
|
|
251
|
+
case '"': return '\"';
|
|
252
|
+
case "<": return "<";
|
|
253
|
+
case ">": return ">";
|
|
254
|
+
default: return s;
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
},
|
|
258
|
+
|
|
259
|
+
// by @langalex, support for arrays of strings
|
|
260
|
+
create_context: function(_context) {
|
|
261
|
+
if(this.is_object(_context)) {
|
|
262
|
+
return _context;
|
|
263
|
+
} else {
|
|
264
|
+
var iterator = ".";
|
|
265
|
+
if(this.pragmas["IMPLICIT-ITERATOR"]) {
|
|
266
|
+
iterator = this.pragmas["IMPLICIT-ITERATOR"].iterator;
|
|
267
|
+
}
|
|
268
|
+
var ctx = {};
|
|
269
|
+
ctx[iterator] = _context;
|
|
270
|
+
return ctx;
|
|
271
|
+
}
|
|
272
|
+
},
|
|
273
|
+
|
|
274
|
+
is_object: function(a) {
|
|
275
|
+
return a && typeof a == "object";
|
|
276
|
+
},
|
|
277
|
+
|
|
278
|
+
is_array: function(a) {
|
|
279
|
+
return Object.prototype.toString.call(a) === '[object Array]';
|
|
280
|
+
},
|
|
281
|
+
|
|
282
|
+
/*
|
|
283
|
+
Gets rid of leading and trailing whitespace
|
|
284
|
+
*/
|
|
285
|
+
trim: function(s) {
|
|
286
|
+
return s.replace(/^\s*|\s*$/g, "");
|
|
287
|
+
},
|
|
288
|
+
|
|
289
|
+
/*
|
|
290
|
+
Why, why, why? Because IE. Cry, cry cry.
|
|
291
|
+
*/
|
|
292
|
+
map: function(array, fn) {
|
|
293
|
+
if (typeof array.map == "function") {
|
|
294
|
+
return array.map(fn);
|
|
295
|
+
} else {
|
|
296
|
+
var r = [];
|
|
297
|
+
var l = array.length;
|
|
298
|
+
for(var i = 0; i < l; i++) {
|
|
299
|
+
r.push(fn(array[i]));
|
|
300
|
+
}
|
|
301
|
+
return r;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
return({
|
|
307
|
+
name: "mustache.js",
|
|
308
|
+
version: "0.3.0-dev",
|
|
309
|
+
|
|
310
|
+
/*
|
|
311
|
+
Turns a template and view into HTML
|
|
312
|
+
*/
|
|
313
|
+
to_html: function(template, view, partials, send_fun) {
|
|
314
|
+
var renderer = new Renderer();
|
|
315
|
+
if(send_fun) {
|
|
316
|
+
renderer.send = send_fun;
|
|
317
|
+
}
|
|
318
|
+
renderer.render(template, view, partials);
|
|
319
|
+
if(!send_fun) {
|
|
320
|
+
return renderer.buffer.join("\n");
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
});
|
|
324
|
+
}();
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Selection.js
|
|
3
|
+
* Copyright (c) 2010 Flip Sasser, All Rights Reserved
|
|
4
|
+
* Use to manipulate a text selection object using jQuery. Fun!
|
|
5
|
+
*
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
require('string');
|
|
9
|
+
|
|
10
|
+
$.extend($.expr[':'], {
|
|
11
|
+
block: function(a) {
|
|
12
|
+
return $(a).css('display') === 'block';
|
|
13
|
+
},
|
|
14
|
+
textnode: function(a) {
|
|
15
|
+
return a.nodeType == 3;
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
jQuery.fn.selection = function(rootSelector) {
|
|
20
|
+
return new Selection(this[0], rootSelector);
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
// The selection class will provide some basic range finding methods, such as:
|
|
24
|
+
// * Find text before and after the selection, inside of current block
|
|
25
|
+
// * Move the caret to a block if it's in a Textnode directly under body
|
|
26
|
+
// The selection class can be used to move after() to a new adjacent
|
|
27
|
+
// block without much overhead or trouble. It can also be used to wrap,
|
|
28
|
+
// unwrap, and detect wrapping tags in, HTML content. Example:
|
|
29
|
+
//
|
|
30
|
+
// var selection = $(window).selection();
|
|
31
|
+
// var before = selection.before();
|
|
32
|
+
// var after = selection.after();
|
|
33
|
+
// // Splits the `before` and `after` into
|
|
34
|
+
// // two new blocks; the default block type is
|
|
35
|
+
// // paragraph but that can be changed:
|
|
36
|
+
// var newParagraph = selection.split('p');
|
|
37
|
+
// selection.wrap('strong');
|
|
38
|
+
// selection.unwrap('strong');
|
|
39
|
+
// selection.wrappedIn('strong'); //=> true if the selection is inside <strong>
|
|
40
|
+
// selection.insert('<img alt="Image!" src="/images/foo.gif" />');
|
|
41
|
+
|
|
42
|
+
var Selection = function(element, rootSelector) {
|
|
43
|
+
this.window = element;
|
|
44
|
+
this.document = element.document;
|
|
45
|
+
this.rootSelector = rootSelector || 'body';
|
|
46
|
+
this.root = $(this.document).find(this.rootSelector + ':first');
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
Selection.prototype = {
|
|
50
|
+
// `after` and `before` return the strings before and after the current selection,
|
|
51
|
+
// inside of the current block (or an automatically-created paragraph block).
|
|
52
|
+
after: function() {
|
|
53
|
+
return this.afterRange().toString();
|
|
54
|
+
},
|
|
55
|
+
afterAll: function() {
|
|
56
|
+
var range = this.range();
|
|
57
|
+
var afterAllRange = this.document.createRange();
|
|
58
|
+
afterAllRange.selectNodeContents(this.root[0]);
|
|
59
|
+
afterAllRange.setStart(range.endContainer, range.endOffset);
|
|
60
|
+
return afterAllRange.cloneContents();
|
|
61
|
+
},
|
|
62
|
+
before: function() {
|
|
63
|
+
return this.beforeRange().toString();
|
|
64
|
+
},
|
|
65
|
+
beforeAll: function() {
|
|
66
|
+
var range = this.range();
|
|
67
|
+
var beforeAllRange = this.document.createRange();
|
|
68
|
+
beforeAllRange.setStart(this.root[0], 0);
|
|
69
|
+
beforeAllRange.setEnd(range.startContainer, range.startOffset);
|
|
70
|
+
return beforeAllRange.cloneContents();
|
|
71
|
+
},
|
|
72
|
+
// Inserts some content into the selection. Really, it just replaces the
|
|
73
|
+
// current selection with the content. Coolness.
|
|
74
|
+
insert: function(content) {
|
|
75
|
+
this.replace(content);
|
|
76
|
+
},
|
|
77
|
+
normalize: function() {
|
|
78
|
+
if (this.startBlock().length > 0) {
|
|
79
|
+
this.startBlock()[0].normalize();
|
|
80
|
+
}
|
|
81
|
+
if (this.endBlock().length > 0) {
|
|
82
|
+
this.endBlock()[0].normalize();
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
// Returns the before or after range of the current selection, scoped to the
|
|
86
|
+
// containing block. If no containing block is found, it automatically
|
|
87
|
+
// creates one and set the range inside of it.
|
|
88
|
+
range: function(name) {
|
|
89
|
+
return this._data('range', function() {
|
|
90
|
+
var range;
|
|
91
|
+
if (this.selection.getRangeAt) {
|
|
92
|
+
range = this.selection.getRangeAt(0);
|
|
93
|
+
} else if (this.document.selection) {
|
|
94
|
+
range = this.document.createRange();
|
|
95
|
+
range.setStart(this.selection.anchorNode, this.selection.anchorOffset);
|
|
96
|
+
range.setEnd(this.selection.focusNode, this.selection.focusOffset);
|
|
97
|
+
}
|
|
98
|
+
return range;
|
|
99
|
+
});
|
|
100
|
+
},
|
|
101
|
+
// Replaces the selection with the passed-in content. Primarily used internally
|
|
102
|
+
// for `insert` and `wrap`.
|
|
103
|
+
replace: function(content) {
|
|
104
|
+
this.range().deleteContents();
|
|
105
|
+
var startNode = this.startNode();
|
|
106
|
+
startNode.append(content);
|
|
107
|
+
this.select(startNode);
|
|
108
|
+
},
|
|
109
|
+
// Sets the caret position inside the first element that matches the selector.
|
|
110
|
+
// You can also pass in an element and an offset if you're feeling extra-aggressive,
|
|
111
|
+
// but be forewarned: the element MUST appear inside the `root` element of the
|
|
112
|
+
// selection, or you can go to hell, sir.
|
|
113
|
+
select: function(selector, offset) {
|
|
114
|
+
this.root.focus();
|
|
115
|
+
var element;
|
|
116
|
+
if (typeof(selector) == 'object') {
|
|
117
|
+
element = $(selector);
|
|
118
|
+
} else {
|
|
119
|
+
element = this.root.find(selector + ':first');
|
|
120
|
+
}
|
|
121
|
+
if (!offset) {
|
|
122
|
+
offset = 0;
|
|
123
|
+
}
|
|
124
|
+
element = element[0];
|
|
125
|
+
if (element) {
|
|
126
|
+
if (!this.selection) {
|
|
127
|
+
this.selection = this.window.getSelection ? this.window.getSelection() : this.document.selection;
|
|
128
|
+
}
|
|
129
|
+
var range = this.document.createRange();
|
|
130
|
+
range.selectNodeContents(element);
|
|
131
|
+
range.collapse(true);
|
|
132
|
+
this.selection.removeAllRanges();
|
|
133
|
+
this.selection.addRange(range);
|
|
134
|
+
} else {
|
|
135
|
+
this.root.focus();
|
|
136
|
+
}
|
|
137
|
+
},
|
|
138
|
+
split: function(tagname) {
|
|
139
|
+
// Default to a new paragraph
|
|
140
|
+
if (!tagname) {
|
|
141
|
+
tagname = 'p';
|
|
142
|
+
}
|
|
143
|
+
tagname = tagname.toLowerCase();
|
|
144
|
+
// Build whatever comes after the current node, cutting the contents out
|
|
145
|
+
var afterNode = $('<' + tagname + '></' + tagname + '>');
|
|
146
|
+
var after = this.afterRange();
|
|
147
|
+
afterNode.html(after.cloneContents());
|
|
148
|
+
afterNode.append('<br />');
|
|
149
|
+
// Add the new node after the current block - and then, and this is important,
|
|
150
|
+
// SELECT the top of that block
|
|
151
|
+
var endBlock = this.endBlock();
|
|
152
|
+
endBlock.after(afterNode);
|
|
153
|
+
var startBlock = this.startBlock();
|
|
154
|
+
if (startBlock == endBlock && startBlock.text() == '') {
|
|
155
|
+
afterNode = $('<br />');
|
|
156
|
+
startBlock.append(afterNode);
|
|
157
|
+
}
|
|
158
|
+
this.select(afterNode);
|
|
159
|
+
this.range().deleteContents();
|
|
160
|
+
after.deleteContents();
|
|
161
|
+
startBlock[0].normalize();
|
|
162
|
+
endBlock[0].normalize();
|
|
163
|
+
},
|
|
164
|
+
afterRange: function() {
|
|
165
|
+
return this._data('afterRange', function() {
|
|
166
|
+
var endNode = this.endNode();
|
|
167
|
+
|
|
168
|
+
var afterRange = this.document.createRange();
|
|
169
|
+
afterRange.setStart(endNode[0], this.data['endOffset']);
|
|
170
|
+
afterRange.setEnd(endNode[0], (endNode[0].wholeText || endNode.html()).length);
|
|
171
|
+
|
|
172
|
+
return afterRange;
|
|
173
|
+
});
|
|
174
|
+
},
|
|
175
|
+
beforeRange: function() {
|
|
176
|
+
return this._data('beforeRange', function() {
|
|
177
|
+
var startNode = this.startNode();
|
|
178
|
+
|
|
179
|
+
var beforeRange = this.document.createRange();
|
|
180
|
+
beforeRange.setStart(startNode[0], 0);
|
|
181
|
+
beforeRange.setEnd(startNode[0], this.data['startOffset']);
|
|
182
|
+
|
|
183
|
+
return beforeRange;
|
|
184
|
+
});
|
|
185
|
+
},
|
|
186
|
+
endNode: function() {
|
|
187
|
+
return this._data('endNode', function() {
|
|
188
|
+
var beforeRange = this.document.createRange();
|
|
189
|
+
beforeRange.setStart(this.selection.anchorNode, this.selection.anchorOffset);
|
|
190
|
+
beforeRange.collapse(true);
|
|
191
|
+
|
|
192
|
+
var afterRange = this.document.createRange();
|
|
193
|
+
afterRange.setStart(this.selection.focusNode, this.selection.focusOffset);
|
|
194
|
+
afterRange.collapse(true);
|
|
195
|
+
|
|
196
|
+
var ltr = beforeRange.compareBoundaryPoints(beforeRange.START_TO_END, afterRange) < 0;
|
|
197
|
+
|
|
198
|
+
var endNode = $(ltr ? this.selection.focusNode : this.selection.anchorNode);
|
|
199
|
+
this.data['startNode'] = $(ltr ? this.selection.anchorNode : this.selection.focusNode);
|
|
200
|
+
this.data['startOffset'] = ltr ? this.selection.anchorOffset : this.selection.focusOffset;
|
|
201
|
+
this.data['endOffset'] = ltr ? this.selection.focusOffset : this.selection.anchorOffset;
|
|
202
|
+
|
|
203
|
+
return endNode;
|
|
204
|
+
|
|
205
|
+
});
|
|
206
|
+
},
|
|
207
|
+
endBlock: function() {
|
|
208
|
+
return this._data('endBlock', function() {
|
|
209
|
+
var endNode = this.endNode();
|
|
210
|
+
var endBlock = endNode.parents(':block:not(body, html, ' + this.rootSelector + '):first') || endNode;
|
|
211
|
+
if (endBlock.length > 0) {
|
|
212
|
+
endBlock[0].normalize();
|
|
213
|
+
}
|
|
214
|
+
return endBlock;
|
|
215
|
+
});
|
|
216
|
+
},
|
|
217
|
+
startBlock: function() {
|
|
218
|
+
return this._data('startBlock', function() {
|
|
219
|
+
var startNode = this.startNode();
|
|
220
|
+
return startNode.parents(':block:not(body, html):first') || startNode;
|
|
221
|
+
});
|
|
222
|
+
},
|
|
223
|
+
startNode: function() {
|
|
224
|
+
return this._data('startNode', function() {
|
|
225
|
+
var beforeRange = this.document.createRange();
|
|
226
|
+
beforeRange.setStart(this.selection.anchorNode, this.selection.anchorOffset);
|
|
227
|
+
beforeRange.collapse(true);
|
|
228
|
+
|
|
229
|
+
var afterRange = this.document.createRange();
|
|
230
|
+
afterRange.setStart(this.selection.focusNode, this.selection.focusOffset);
|
|
231
|
+
afterRange.collapse(true);
|
|
232
|
+
|
|
233
|
+
var ltr = beforeRange.compareBoundaryPoints(beforeRange.START_TO_END, afterRange) < 0;
|
|
234
|
+
var startNode = $(ltr ? this.selection.anchorNode : this.selection.focusNode);
|
|
235
|
+
this.data['startOffset'] = ltr ? this.selection.anchorOffset : this.selection.focusOffset;
|
|
236
|
+
this.data['endNode'] = $(ltr ? this.selection.focusNode : this.selection.anchorNode);
|
|
237
|
+
this.data['endOffset'] = ltr ? this.selection.focusOffset : this.selection.anchorOffset;
|
|
238
|
+
|
|
239
|
+
return startNode;
|
|
240
|
+
});
|
|
241
|
+
},
|
|
242
|
+
unwrap: function(tagname) {
|
|
243
|
+
var tags = this.wrappedIn(tagname);
|
|
244
|
+
if (tags) {
|
|
245
|
+
|
|
246
|
+
}
|
|
247
|
+
},
|
|
248
|
+
wrap: function(tagname) {
|
|
249
|
+
// Nice, clean-looking HTML.
|
|
250
|
+
tagname = tagname.toLowerCase();
|
|
251
|
+
var wrapper = this.document.createElement(tagname);
|
|
252
|
+
this.range().surroundContents(wrapper);
|
|
253
|
+
},
|
|
254
|
+
// Returns a boolean for whether or not the selection (or caret position)
|
|
255
|
+
// is inside of a certain tag. Useful for activating / deactivating button
|
|
256
|
+
// items as the user navigates.
|
|
257
|
+
wrappedIn: function(selector) {
|
|
258
|
+
var startNode = this.startNode();
|
|
259
|
+
if (startNode.is(selector)) {
|
|
260
|
+
return startNode;
|
|
261
|
+
} else {
|
|
262
|
+
var parents = startNode.parents(selector);
|
|
263
|
+
if (parents.length === 0) {
|
|
264
|
+
return false;
|
|
265
|
+
} else {
|
|
266
|
+
return parents;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
},
|
|
270
|
+
_data: function(name, block) {
|
|
271
|
+
if (!this.data) {
|
|
272
|
+
this.data = {};
|
|
273
|
+
}
|
|
274
|
+
if (!this.data[name]) {
|
|
275
|
+
this.selection = this.window.getSelection ? this.window.getSelection() : this.document.selection;
|
|
276
|
+
this.data[name] = block.call(this);
|
|
277
|
+
}
|
|
278
|
+
return this.data[name];
|
|
279
|
+
}
|
|
280
|
+
};
|