jipe 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile ADDED
@@ -0,0 +1,8 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Add dependencies to develop your gem here.
4
+ # Include everything needed to run rake, tests, features, etc.
5
+ group :development do
6
+ gem "bundler", "~> 1.0.0"
7
+ gem "jeweler", "~> 1.5.2"
8
+ end
@@ -0,0 +1,16 @@
1
+ GEM
2
+ remote: http://rubygems.org/
3
+ specs:
4
+ git (1.2.5)
5
+ jeweler (1.5.2)
6
+ bundler (~> 1.0.0)
7
+ git (>= 1.2.5)
8
+ rake
9
+ rake (0.8.7)
10
+
11
+ PLATFORMS
12
+ ruby
13
+
14
+ DEPENDENCIES
15
+ bundler (~> 1.0.0)
16
+ jeweler (~> 1.5.2)
data/README ADDED
@@ -0,0 +1,47 @@
1
+ JIPE: In-Place Editing Components for Jester
2
+ ============================================
3
+
4
+ JIPE stands for Jester In-Place Editing. It started out as a replacement for
5
+ Rails' built-in in_place_editor helper function using Eric Mill's Jester
6
+ library, but is beginning to add additional editing widgets as well.
7
+
8
+ The upshot of this is that if you have RESTful controllers in your Rails app,
9
+ then JIPE can very quickly help you set up in-place editing for your RESTful
10
+ objects.
11
+
12
+ Setup
13
+ =====
14
+
15
+ To begin using JIPE in your project, run:
16
+
17
+ script/generate jipe
18
+
19
+ This will copy the JIPE icons into your public/images directory.
20
+
21
+ Usage
22
+ =====
23
+
24
+ To use JIPE, you'll need to include the Javascripts in your layout:
25
+
26
+ <%= javascript_include_tag 'jipe/jester' %>
27
+ <%= javascript_include_tag 'jipe/jipe' %>
28
+
29
+ This requires Rails 2.3, since it makes use of the Engines-like features. For
30
+ older versions of Rails, you will need to either install the Engines plugin,
31
+ or copy the Javascripts into your public/javascript directory yourself.
32
+
33
+ You'll also need to tell Jester how to access your REST resources, like so:
34
+
35
+ <script type="text/javascript">
36
+ Resource.model("Post");
37
+ </script>
38
+
39
+ Now that you've done that, creating in-place editing fields is easy:
40
+
41
+ Title: <%= jipe_editor @post, "title" %><br/>
42
+ <%= jipe_editor @post, "body", :rows => 4 %>
43
+
44
+ In addition to textbox-style in-place editors, JIPE lets you do clickable image
45
+ toggles for boolean fields. For example:
46
+
47
+ Mood: <%= jipe_image_toggle @post, "is_happy", "smiley.png", "frowny.png" %>
@@ -0,0 +1,47 @@
1
+ require 'rubygems'
2
+ require 'bundler'
3
+ begin
4
+ Bundler.setup(:default, :development)
5
+ rescue Bundler::BundlerError => e
6
+ $stderr.puts e.message
7
+ $stderr.puts "Run `bundle install` to install missing gems"
8
+ exit e.status_code
9
+ end
10
+ require 'rake'
11
+
12
+ require 'jeweler'
13
+ Jeweler::Tasks.new do |gem|
14
+ # gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options
15
+ gem.name = "jipe"
16
+ gem.homepage = "http://github.com/nbudin/jipe"
17
+ gem.license = "MIT"
18
+ gem.summary = %Q{RESTful In-place editors for Rails using Jester}
19
+ gem.email = "natbudin@gmail.com"
20
+ gem.authors = ["Nat Budin"]
21
+ # Include your dependencies below. Runtime dependencies are required when using your gem,
22
+ # and development dependencies are only needed for development (ie running rake tasks, tests, etc)
23
+ # gem.add_runtime_dependency 'jabber4r', '> 0.1'
24
+ # gem.add_development_dependency 'rspec', '> 1.2.3'
25
+ end
26
+ Jeweler::RubygemsDotOrgTasks.new
27
+
28
+ require 'rake/testtask'
29
+ require 'rake/rdoctask'
30
+ desc 'Default: run unit tests.'
31
+ task :default => :test
32
+
33
+ desc 'Test the jipe plugin.'
34
+ Rake::TestTask.new(:test) do |t|
35
+ t.libs << 'lib'
36
+ t.pattern = 'test/**/*_test.rb'
37
+ t.verbose = true
38
+ end
39
+
40
+ desc 'Generate documentation for the jipe plugin.'
41
+ Rake::RDocTask.new(:rdoc) do |rdoc|
42
+ rdoc.rdoc_dir = 'rdoc'
43
+ rdoc.title = 'Jipe'
44
+ rdoc.options << '--line-numbers' << '--inline-source'
45
+ rdoc.rdoc_files.include('README')
46
+ rdoc.rdoc_files.include('lib/**/*.rb')
47
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 1.0.0
@@ -0,0 +1,17 @@
1
+ class JipeController < ApplicationController
2
+ unloadable
3
+ layout nil
4
+ caches_page :jester, :jipe
5
+
6
+ def jester
7
+ respond_to do |format|
8
+ format.js {}
9
+ end
10
+ end
11
+
12
+ def jipe
13
+ respond_to do |format|
14
+ format.js {}
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,860 @@
1
+ // Jester version 1.5
2
+ // Released October 25th, 2007
3
+
4
+ // Compatible, tested with Prototype 1.6.0.2
5
+
6
+ // Copyright 2007, thoughtbot, inc.
7
+ // Released under the MIT License.
8
+
9
+ Jester = {}
10
+ Jester.Resource = function(){};
11
+
12
+ // Doing it this way forces the validation of the syntax but gives flexibility enough to rename the new class.
13
+ Jester.Constructor = function(model){
14
+ return (function CONSTRUCTOR() {
15
+ this.klass = CONSTRUCTOR;
16
+ this.initialize.apply(this, arguments);
17
+ this.after_initialization.apply(this, arguments);
18
+ }).toString().replace(/CONSTRUCTOR/g, model);
19
+ }
20
+
21
+ // universal Jester callback holder for remote JSON loading
22
+ var jesterCallback = null;
23
+
24
+ Object.extend(Jester.Resource, {
25
+ model: function(model, options)
26
+ {
27
+ var new_model = null;
28
+ new_model = eval(model + " = " + Jester.Constructor(model));
29
+ new_model.prototype = new Jester.Resource();
30
+ Object.extend(new_model, Jester.Resource);
31
+
32
+ // We delay instantiating XML.ObjTree() so that it can be listed at the end of this file instead of the beginning
33
+ if (!Jester.Tree) {
34
+ Jester.Tree = new XML.ObjTree();
35
+ Jester.Tree.attr_prefix = "@";
36
+ }
37
+ if (!options) options = {};
38
+
39
+ var default_options = {
40
+ format: "xml",
41
+ singular: model.underscore(),
42
+ name: model
43
+ }
44
+ options = Object.extend(default_options, options);
45
+ options.format = options.format.toLowerCase();
46
+ options.plural = options.singular.pluralize(options.plural);
47
+ options.singular_xml = options.singular.replace(/_/g, "-");
48
+ options.plural_xml = options.plural.replace(/_/g, "-");
49
+ options.remote = false;
50
+
51
+ // Establish prefix
52
+ var default_prefix = window.location.protocol + "//" + window.location.hostname + (window.location.port ? ":" + window.location.port : "");
53
+ if (options.prefix && options.prefix.match(/^https?:/))
54
+ options.remote = true;
55
+
56
+ if (!options.prefix)
57
+ options.prefix = default_prefix;
58
+
59
+ if (!options.prefix.match(/^(https?|file):/))
60
+ options.prefix = default_prefix + (options.prefix.match(/^\//) ? "" : "/") + options.prefix;
61
+
62
+ options.prefix = options.prefix.replace(/\b\/+$/,"");
63
+
64
+ // Establish custom URLs
65
+ options.urls = Object.extend(this._default_urls(options), options.urls);
66
+
67
+ // Assign options to model
68
+ new_model.name = model;
69
+ new_model.options = options;
70
+ for(var opt in options)
71
+ new_model["_" + opt] = options[opt];
72
+
73
+ // Establish custom URL helpers
74
+ for (var url in options.urls)
75
+ eval('new_model._' + url + '_url = function(params) {return this._url_for("' + url + '", params);}');
76
+
77
+ if (options.checkNew)
78
+ this.buildAttributes(new_model, options);
79
+
80
+ if (window)
81
+ window[model] = new_model;
82
+
83
+ return new_model;
84
+ },
85
+
86
+ buildAttributes: function(model, options) {
87
+ model = model || this;
88
+ var async = options.asynchronous;
89
+
90
+ if (async == null)
91
+ async = true;
92
+
93
+ var buildWork = bind(model, function(doc) {
94
+ if (this._format == "json")
95
+ this._attributes = this._attributesFromJSON(doc);
96
+ else
97
+ this._attributes = this._attributesFromTree(doc[this._singular_xml]);
98
+ });
99
+ model.requestAndParse(options.format, buildWork, model._new_url(), {asynchronous: async});
100
+ },
101
+
102
+ loadRemoteJSON : function(url, callback, user_callback) {
103
+ // tack on user_callback if there is one, and only if it's really a function
104
+ if (typeof(user_callback) == "function")
105
+ jesterCallback = function(doc) {user_callback(callback(doc));}
106
+ else
107
+ jesterCallback = callback;
108
+
109
+ var script = document.createElement("script");
110
+ script.type = "text/javascript";
111
+
112
+ if (url.indexOf("?") == -1)
113
+ url += "?";
114
+ else
115
+ url += "&";
116
+ url += "callback=jesterCallback";
117
+ script.src = url;
118
+
119
+ document.firstChild.appendChild(script);
120
+ },
121
+
122
+ requestAndParse : function(format, callback, url, options, user_callback, remote) {
123
+ if (remote && format == "json" && user_callback)
124
+ return this.loadRemoteJSON(url, callback, user_callback)
125
+
126
+ parse_and_callback = null;
127
+ if (format.toLowerCase() == "json") {
128
+ parse_and_callback = function(transport) {
129
+ if (transport.status == 500) return callback(null);
130
+ eval("var attributes = " + transport.responseText); // hashes need this kind of eval
131
+ return callback(attributes);
132
+ }
133
+ } else {
134
+ parse_and_callback = function(transport) {
135
+ if (transport.status == 500) return callback(null);
136
+ return callback(Jester.Tree.parseXML(transport.responseText));
137
+ }
138
+ }
139
+
140
+ // most parse requests are going to be a GET
141
+ if (!(options.postBody || options.parameters || options.postbody || options.method == "post")) {
142
+ options.method = "get";
143
+ }
144
+
145
+ return this.request(parse_and_callback, url, options, user_callback);
146
+ },
147
+
148
+ // Helper to aid in handling either async or synchronous requests
149
+ request : function(callback, url, options, user_callback) {
150
+ if (user_callback) {
151
+ options.asynchronous = true;
152
+ // if an options hash was given instead of a callback
153
+ if (typeof(user_callback) == "object") {
154
+ for (var x in user_callback)
155
+ options[x] = user_callback[x];
156
+ user_callback = options.onComplete;
157
+ }
158
+ }
159
+ else
160
+ user_callback = function(arg){return arg;}
161
+
162
+ if (options.asynchronous) {
163
+ options.onComplete = function(transport, json) {user_callback(callback(transport), json);}
164
+ return new Ajax.Request(url, options).transport;
165
+ }
166
+ else
167
+ {
168
+ options.asynchronous = false; // Make sure it's set, to avoid being overridden.
169
+ return callback(new Ajax.Request(url, options).transport);
170
+ }
171
+ },
172
+
173
+ find : function(id, params, callback) {
174
+ // allow a params hash to be omitted and a callback function given directly
175
+ if (!callback && typeof(params) == "function") {
176
+ callback = params;
177
+ params = null;
178
+ }
179
+
180
+ var findAllWork = bind(this, function(doc) {
181
+ if (!doc) return null;
182
+
183
+ var collection = this._loadCollection(doc);
184
+
185
+ if (!collection) return null;
186
+
187
+ // This is better than requiring the controller to support a "limit" parameter
188
+ if (id == "first")
189
+ return collection[0];
190
+
191
+ return collection;
192
+ });
193
+
194
+ var findOneWork = bind(this, function(doc) {
195
+ if (!doc) return null;
196
+
197
+ var base = this._loadSingle(doc);
198
+
199
+ // if there were no properties, it was probably not actually loaded
200
+ if (!base || base._properties.length == 0) return null;
201
+
202
+ // even if the ID didn't come back, we obviously knew the ID to search with, so set it
203
+ if (!base._properties.include("id")) base._setAttribute("id", parseInt(id))
204
+
205
+ return base;
206
+ });
207
+
208
+ if (id == "first" || id == "all") {
209
+ var url = this._list_url(params);
210
+ return this.requestAndParse(this._format, findAllWork, url, {}, callback, this._remote);
211
+ }
212
+ else {
213
+ if (isNaN(parseInt(id))) return null;
214
+ if (!params) params = {};
215
+ params.id = id;
216
+
217
+ var url = this._show_url(params);
218
+ return this.requestAndParse(this._format, findOneWork, url, {}, callback, this._remote);
219
+ }
220
+ },
221
+
222
+ build : function(attributes) {
223
+ return new this(attributes);
224
+ },
225
+
226
+ create : function(attributes, callback) {
227
+ var base = new this(attributes);
228
+
229
+ createWork = bind(this, function(saved) {
230
+ return callback(base);
231
+ });
232
+
233
+ if (callback) {
234
+ return base.save(createWork);
235
+ }
236
+ else {
237
+ base.save();
238
+ return base;
239
+ }
240
+ },
241
+
242
+ // Destroys a REST object. Can be used as follows:
243
+ // object.destroy() - when called on an instance of a model, destroys that instance
244
+ // Model.destroy(1) - destroys the Model object with ID 1
245
+ // Model.destroy({parent: 3, id: 1}) - destroys the Model object with Parent ID 3 and ID 1
246
+ //
247
+ // Any of these forms can also be passed a callback function as an additional parameter and it works as you expect.
248
+ destroy : function(params, callback) {
249
+ if (typeof(params) == "function") {
250
+ callback = params;
251
+ params = null;
252
+ }
253
+ if (typeof(params) == "number") {
254
+ params = {id: params};
255
+ }
256
+ params.id = params.id || this.id;
257
+ if (!params.id) return false;
258
+
259
+ var destroyWork = bind(this, function(transport) {
260
+ if (transport.status == 200) {
261
+ if (!params.id || this.id == params.id)
262
+ this.id = null;
263
+ return this;
264
+ }
265
+ else
266
+ return false;
267
+ });
268
+
269
+ return this.request(destroyWork, this._destroy_url(params), {method: "delete"}, callback);
270
+ },
271
+
272
+ _interpolate: function(string, params) {
273
+ if (!params) return string;
274
+
275
+ var result = string;
276
+ params.each(function(pair) {
277
+ var re = new RegExp(":" + pair.key, "g");
278
+ if (result.match(re)) {
279
+ result = result.replace(re, pair.value);
280
+ params.unset(pair.key);
281
+ }
282
+ });
283
+ return result;
284
+ },
285
+
286
+ _url_for : function(action, params) {
287
+ if (!this._urls[action]) return "";
288
+ // if an integer is sent, it's assumed just the ID is a parameter
289
+ if (typeof(params) == "number") params = {id: params}
290
+
291
+ if (params) params = $H(params);
292
+
293
+ var url = this._interpolate(this._prefix + this._urls[action], params)
294
+ return url + (params && params.any() ? "?" + params.toQueryString() : "");
295
+ },
296
+
297
+ _default_urls : function(options) {
298
+ urls = {
299
+ 'show' : "/" + options.plural + "/:id." + options.format,
300
+ 'list' : "/" + options.plural + "." + options.format,
301
+ 'new' : "/" + options.plural + "/new." + options.format
302
+ }
303
+ urls.create = urls.list;
304
+ urls.destroy = urls.update = urls.show;
305
+
306
+ return urls;
307
+ },
308
+
309
+ // Converts a JSON hash returns from ActiveRecord::Base#to_json into a hash of attribute values
310
+ // Does not handle associations, as AR's #to_json doesn't either
311
+ // Also, JSON doesn't include room to store types, so little auto-transforming is done here (just on 'id')
312
+ _attributesFromJSON : function(json) {
313
+ if (!json || json.constructor != Object) return false;
314
+ if (json.attributes) json = json.attributes;
315
+
316
+ var attributes = {};
317
+ var i = 0;
318
+ for (var attr in json) {
319
+ var value = json[attr];
320
+ if (attr == "id")
321
+ value = parseInt(value);
322
+ else if (attr.match(/(created_at|created_on|updated_at|updated_on)/)) {
323
+ var date = Date.parse(value);
324
+ if (date && !isNaN(date)) value = date;
325
+ }
326
+ attributes[attr] = value;
327
+ i += 1;
328
+ }
329
+ if (i == 0) return false; // empty hashes should just return false
330
+
331
+ return attributes;
332
+ },
333
+
334
+ // Converts the XML tree returned from a single object into a hash of attribute values
335
+ _attributesFromTree : function(elements) {
336
+ var attributes = {}
337
+ for (var attr in elements) {
338
+ // pull out the value
339
+ var value = elements[attr];
340
+ if (elements[attr] && elements[attr]["@type"]) {
341
+ if (elements[attr]["#text"])
342
+ value = elements[attr]["#text"];
343
+ else
344
+ value = undefined;
345
+ }
346
+
347
+ // handle empty value (pass it through)
348
+ if (!value) {}
349
+
350
+ // handle scalars
351
+ else if (typeof(value) == "string") {
352
+ // perform any useful type transformations
353
+ if (elements[attr]["@type"] == "integer") {
354
+ var num = parseInt(value);
355
+ if (!isNaN(num)) value = num;
356
+ }
357
+ else if (elements[attr]["@type"] == "boolean")
358
+ value = (value == "true");
359
+ else if (elements[attr]["@type"] == "datetime") {
360
+ var date = Date.parse(value);
361
+ if (!isNaN(date)) value = date;
362
+ }
363
+ }
364
+ // handle arrays (associations)
365
+ else {
366
+ var relation = value; // rename for clarity in the context of an association
367
+
368
+ // first, detect if it's has_one/belongs_to, or has_many
369
+ var i = 0;
370
+ var singular = null;
371
+ var has_many = false;
372
+ for (var val in relation) {
373
+ if (i == 0)
374
+ singular = val;
375
+ i += 1;
376
+ }
377
+
378
+ // has_many
379
+ if (relation[singular] && typeof(relation[singular]) == "object" && i == 1) {
380
+ var value = [];
381
+ var plural = attr;
382
+ var name = singular.camelize().capitalize();
383
+
384
+ // force array
385
+ if (!(elements[plural][singular].length > 0))
386
+ elements[plural][singular] = [elements[plural][singular]];
387
+
388
+ elements[plural][singular].each( bind(this, function(single) {
389
+ // if the association hasn't been modeled, do a default modeling here
390
+ // hosted object's prefix and format are inherited, singular and plural are set
391
+ // from the XML
392
+ if (eval("typeof(" + name + ")") == "undefined") {
393
+ Jester.Resource.model(name, {prefix: this._prefix, singular: singular, plural: plural, format: this._format});
394
+ }
395
+ var base = eval(name + ".build(this._attributesFromTree(single))");
396
+ value.push(base);
397
+ }));
398
+ }
399
+ // has_one or belongs_to
400
+ else {
401
+ singular = attr;
402
+ var name = singular.capitalize();
403
+
404
+ // if the association hasn't been modeled, do a default modeling here
405
+ // hosted object's prefix and format are inherited, singular is set from the XML
406
+ if (eval("typeof(" + name + ")") == "undefined") {
407
+ Jester.Resource.model(name, {prefix: this._prefix, singular: singular, format: this._format});
408
+ }
409
+ value = eval(name + ".build(this._attributesFromTree(value))");
410
+ }
411
+ }
412
+
413
+ // transform attribute name if needed
414
+ attribute = attr.replace(/-/g, "_");
415
+ attributes[attribute] = value;
416
+ }
417
+
418
+ return attributes;
419
+ },
420
+
421
+ _loadSingle : function(doc) {
422
+ var attributes;
423
+ if (this._format == "json")
424
+ attributes = this._attributesFromJSON(doc);
425
+ else
426
+ attributes = this._attributesFromTree(doc[this._singular_xml]);
427
+
428
+ return this.build(attributes);
429
+ },
430
+
431
+ _loadCollection : function(doc) {
432
+ var collection;
433
+ if (this._format == "json") {
434
+ collection = doc.map( bind(this, function(item) {
435
+ return this.build(this._attributesFromJSON(item));
436
+ }));
437
+ }
438
+ else {
439
+ // if only one result, wrap it in an array
440
+ if (!Jester.Resource.elementHasMany(doc[this._plural_xml]))
441
+ doc[this._plural_xml][this._singular_xml] = [doc[this._plural_xml][this._singular_xml]];
442
+
443
+ collection = doc[this._plural_xml][this._singular_xml].map( bind(this, function(elem) {
444
+ return this.build(this._attributesFromTree(elem));
445
+ }));
446
+ }
447
+ return collection;
448
+ }
449
+
450
+ });
451
+
452
+ Object.extend(Jester.Resource.prototype, {
453
+ initialize : function(attributes) {
454
+ // Initialize no attributes, no associations
455
+ this._properties = [];
456
+ this._associations = [];
457
+
458
+ this.setAttributes(this.klass._attributes || {});
459
+ this.setAttributes(attributes);
460
+
461
+ // Initialize with no errors
462
+ this.errors = [];
463
+
464
+ // Establish custom URL helpers
465
+ for (var url in this.klass._urls)
466
+ eval('this._' + url + '_url = function(params) {return this._url_for("' + url + '", params);}');
467
+ },
468
+ after_initialization: function(){},
469
+
470
+ new_record : function() {return !(this.id);},
471
+ valid : function() {return ! this.errors.any();},
472
+
473
+ reload : function(callback) {
474
+ var reloadWork = bind(this, function(copy) {
475
+ this._resetAttributes(copy.attributes(true));
476
+
477
+ if (callback)
478
+ return callback(this);
479
+ else
480
+ return this;
481
+ });
482
+
483
+ if (this.id) {
484
+ if (callback)
485
+ return this.klass.find(this.id, {}, reloadWork);
486
+ else
487
+ return reloadWork(this.klass.find(this.id));
488
+ }
489
+ else
490
+ return this;
491
+ },
492
+
493
+ // Destroys a REST object. Can be used as follows:
494
+ // object.destroy() - when called on an instance of a model, destroys that instance
495
+ // Model.destroy(1) - destroys the Model object with ID 1
496
+ // Model.destroy({parent: 3, id: 1}) - destroys the Model object with Parent ID 3 and ID 1
497
+ //
498
+ // Any of these forms can also be passed a callback function as an additional parameter and it works as you expect.
499
+ destroy : function(params, callback) {
500
+ if (params === undefined) {
501
+ params = {};
502
+ }
503
+ if (typeof(params) == "function") {
504
+ callback = params;
505
+ params = {};
506
+ }
507
+ if (typeof(params) == "number") {
508
+ params = {id: params};
509
+ }
510
+ if (!params.id) {
511
+ params.id = this.id;
512
+ }
513
+ if (!params.id) return false;
514
+
515
+ // collect params from instance if we're being called as an instance method
516
+ if (this._properties !== undefined) {
517
+ (this._properties).each( bind(this, function(value, i) {
518
+ if (params[value] === undefined) {
519
+ params[value] = this[value];
520
+ }
521
+ }));
522
+ }
523
+
524
+ var destroyWork = bind(this, function(transport) {
525
+ if (transport.status == 200) {
526
+ if (!params.id || this.id == params.id)
527
+ this.id = null;
528
+ return this;
529
+ }
530
+ else
531
+ return false;
532
+ });
533
+
534
+ return this.klass.request(destroyWork, this._destroy_url(params), {method: "delete"}, callback);
535
+ },
536
+
537
+ save : function(callback) {
538
+ var saveWork = bind(this, function(transport) {
539
+ var saved = false;
540
+
541
+ if (transport.responseText && (transport.responseText.strip() != "")) {
542
+ var errors = this._errorsFrom(transport.responseText);
543
+ if (errors)
544
+ this._setErrors(errors);
545
+ else {
546
+ var attributes;
547
+ if (this.klass._format == "json") {
548
+ attributes = this._attributesFromJSON(transport.responseText);
549
+ }
550
+ else {
551
+ var doc = Jester.Tree.parseXML(transport.responseText);
552
+ if (doc[this.klass._singular_xml])
553
+ attributes = this._attributesFromTree(doc[this.klass._singular_xml]);
554
+ }
555
+ if (attributes)
556
+ this._resetAttributes(attributes);
557
+ }
558
+ }
559
+
560
+ // Get ID from the location header if it's there
561
+ if (this.new_record() && transport.status == 201) {
562
+ loc = transport.getResponseHeader("location");
563
+ if (loc) {
564
+ id = parseInt(loc.match(/\/([^\/]*?)(\.\w+)?$/)[1]);
565
+ if (!isNaN(id))
566
+ this._setProperty("id", id)
567
+ }
568
+ }
569
+
570
+ return (transport.status >= 200 && transport.status < 300 && this.errors.length == 0);
571
+ });
572
+
573
+ // reset errors
574
+ this._setErrors([]);
575
+
576
+ var url = null;
577
+ var method = null;
578
+
579
+ // collect params
580
+ var params = {};
581
+ var urlParams = {};
582
+ (this._properties).each( bind(this, function(value, i) {
583
+ params[this.klass._singular + "[" + value + "]"] = this[value];
584
+ urlParams[value] = this[value];
585
+ }));
586
+
587
+ // distinguish between create and update
588
+ if (this.new_record()) {
589
+ url = this._create_url(urlParams);
590
+ method = "post";
591
+ }
592
+ else {
593
+ url = this._update_url(urlParams);
594
+ method = "put";
595
+ }
596
+
597
+
598
+ // send the request
599
+ return this.klass.request(saveWork, url, {parameters: params, method: method}, callback);
600
+ },
601
+
602
+ setAttributes : function(attributes)
603
+ {
604
+ $H(attributes).each(bind(this, function(attr){ this._setAttribute(attr.key, attr.value) }));
605
+ return attributes;
606
+ },
607
+
608
+ updateAttributes : function(attributes, callback)
609
+ {
610
+ this.setAttributes(attributes);
611
+ return this.save(callback);
612
+ },
613
+
614
+ // mimics ActiveRecord's behavior of omitting associations, but keeping foreign keys
615
+ attributes : function(include_associations) {
616
+ var attributes = {}
617
+ for (var i=0; i<this._properties.length; i++)
618
+ attributes[this._properties[i]] = this[this._properties[i]];
619
+ if (include_associations) {
620
+ for (var i=0; i<this._associations.length; i++)
621
+ attributes[this._associations[i]] = this[this._associations[i]];
622
+ }
623
+ return attributes;
624
+ },
625
+
626
+ /*
627
+ Internal methods.
628
+ */
629
+
630
+ _attributesFromJSON: function()
631
+ {
632
+ return this.klass._attributesFromJSON.apply(this.klass, arguments);
633
+ },
634
+
635
+ _attributesFromTree: function()
636
+ {
637
+ return this.klass._attributesFromTree.apply(this.klass, arguments);
638
+ },
639
+
640
+ _errorsFrom : function(raw) {
641
+ if (this.klass._format == "json")
642
+ return this._errorsFromJSON(raw);
643
+ else
644
+ return this._errorsFromXML(raw);
645
+ },
646
+
647
+ // Pulls errors from JSON
648
+ _errorsFromJSON : function(json) {
649
+ try {
650
+ json = eval(json); // okay for arrays
651
+ } catch(e) {
652
+ return false;
653
+ }
654
+
655
+ if (!(json && json.constructor == Array && json[0] && json[0].constructor == Array)) return false;
656
+
657
+ return json.map(function(pair) {
658
+ return pair[0].capitalize() + " " + pair[1];
659
+ });
660
+ },
661
+
662
+ // Pulls errors from XML
663
+ _errorsFromXML : function(xml) {
664
+ if (!xml) return false;
665
+ var doc = Jester.Tree.parseXML(xml);
666
+
667
+ if (doc && doc.errors) {
668
+ var errors = [];
669
+ if (typeof(doc.errors.error) == "string")
670
+ doc.errors.error = [doc.errors.error];
671
+
672
+ doc.errors.error.each(function(value, index) {
673
+ errors.push(value);
674
+ });
675
+
676
+ return errors;
677
+ }
678
+ else return false;
679
+ },
680
+
681
+ // Sets errors with an array. Could be extended at some point to include breaking error messages into pairs (attribute, msg).
682
+ _setErrors : function(errors) {
683
+ this.errors = errors;
684
+ },
685
+
686
+
687
+ // Sets all attributes and associations at once
688
+ // Deciding between the two on whether the attribute is a complex object or a scalar
689
+ _resetAttributes : function(attributes) {
690
+ this._clear();
691
+ for (var attr in attributes)
692
+ this._setAttribute(attr, attributes[attr]);
693
+ },
694
+
695
+ _setAttribute : function(attribute, value) {
696
+ if (value && typeof(value) == "object" && value.constructor != Date)
697
+ this._setAssociation(attribute, value);
698
+ else
699
+ this._setProperty(attribute, value);
700
+ },
701
+
702
+ _setProperties : function(properties) {
703
+ this._clearProperties();
704
+ for (var prop in properties)
705
+ this._setProperty(prop, properties[prop])
706
+ },
707
+
708
+ _setAssociations : function(associations) {
709
+ this._clearAssociations();
710
+ for (var assoc in associations)
711
+ this._setAssociation(assoc, associations[assoc])
712
+ },
713
+
714
+ _setProperty : function(property, value) {
715
+ this[property] = value;
716
+ if (!(this._properties.include(property)))
717
+ this._properties.push(property);
718
+ },
719
+
720
+ _setAssociation : function(association, value) {
721
+ this[association] = value;
722
+ if (!(this._associations.include(association)))
723
+ this._associations.push(association);
724
+ },
725
+
726
+ _clear : function() {
727
+ this._clearProperties();
728
+ this._clearAssociations();
729
+ },
730
+
731
+ _clearProperties : function() {
732
+ for (var i=0; i<this._properties.length; i++)
733
+ this[this._properties[i]] = null;
734
+ this._properties = [];
735
+ },
736
+
737
+ _clearAssociations : function() {
738
+ for (var i=0; i<this._associations.length; i++)
739
+ this[this._associations[i]] = null;
740
+ this._associations = [];
741
+ },
742
+
743
+ // helper URLs
744
+ _url_for : function(action, params) {
745
+ if (!params) params = this.id;
746
+ if (typeof(params) == "object" && !params.id)
747
+ params.id = this.id;
748
+
749
+ return this.klass._url_for(action, params);
750
+ }
751
+
752
+ });
753
+
754
+ // Returns true if the element has more objects beneath it, or just 1 or more attributes.
755
+ // It's not perfect, this would mess up if an object had only one attribute, and it was an array.
756
+ // For now, this is just one of the difficulties of dealing with ObjTree.
757
+ Jester.Resource.elementHasMany = function(element) {
758
+ var i = 0;
759
+ var singular = null;
760
+ var has_many = false;
761
+ for (var val in element) {
762
+ if (i == 0)
763
+ singular = val;
764
+ i += 1;
765
+ }
766
+
767
+ return (element[singular] && typeof(element[singular]) == "object" && element[singular].length != null && i == 1);
768
+ }
769
+
770
+ // This bind function is a modification of the standard Prototype bind function.
771
+ // Use this instead of Prototype's when running in XULRunner due to a longstanding
772
+ // bug in the javascript interpreter.
773
+
774
+ function bind(context, func) {
775
+ var __method = func, args = $A(func.arguments), object = context;
776
+
777
+ return function() {
778
+ return __method.apply(object, args.concat($A(arguments)));
779
+ }
780
+ }
781
+
782
+ // If there is no object already called Resource, we define one to make things a little cleaner for us.
783
+ if(typeof(Resource) == "undefined")
784
+ Resource = Jester.Resource;
785
+
786
+
787
+
788
+
789
+ /*
790
+ Inflector library, contributed graciously to Jester by Ryan Schuft.
791
+ The library in full is a complete port of Rails' Inflector, though Jester only uses its pluralization.
792
+ Its home page can be found at: http://code.google.com/p/inflection-js/
793
+ */
794
+
795
+ if (!String.prototype.pluralize) String.prototype.pluralize = function(plural) {
796
+ var str=this;
797
+ if(plural)str=plural;
798
+ else {
799
+ var uncountable_words=['equipment','information','rice','money','species','series','fish','sheep','moose'];
800
+ var uncountable=false;
801
+ for(var x=0;!uncountable&&x<uncountable_words.length;x++)uncountable=(uncountable_words[x].toLowerCase()==str.toLowerCase());
802
+ if(!uncountable) {
803
+ var rules=[
804
+ [new RegExp('(m)an$','gi'),'$1en'],
805
+ [new RegExp('(pe)rson$','gi'),'$1ople'],
806
+ [new RegExp('(child)$','gi'),'$1ren'],
807
+ [new RegExp('(ax|test)is$','gi'),'$1es'],
808
+ [new RegExp('(octop|vir)us$','gi'),'$1i'],
809
+ [new RegExp('(alias|status)$','gi'),'$1es'],
810
+ [new RegExp('(bu)s$','gi'),'$1ses'],
811
+ [new RegExp('(buffal|tomat)o$','gi'),'$1oes'],
812
+ [new RegExp('([ti])um$','gi'),'$1a'],
813
+ [new RegExp('sis$','gi'),'ses'],
814
+ [new RegExp('(?:([^f])fe|([lr])f)$','gi'),'$1$2ves'],
815
+ [new RegExp('(hive)$','gi'),'$1s'],
816
+ [new RegExp('([^aeiouy]|qu)y$','gi'),'$1ies'],
817
+ [new RegExp('(x|ch|ss|sh)$','gi'),'$1es'],
818
+ [new RegExp('(matr|vert|ind)ix|ex$','gi'),'$1ices'],
819
+ [new RegExp('([m|l])ouse$','gi'),'$1ice'],
820
+ [new RegExp('^(ox)$','gi'),'$1en'],
821
+ [new RegExp('(quiz)$','gi'),'$1zes'],
822
+ [new RegExp('s$','gi'),'s'],
823
+ [new RegExp('$','gi'),'s']
824
+ ];
825
+ var matched=false;
826
+ for(var x=0;!matched&&x<=rules.length;x++) {
827
+ matched=str.match(rules[x][0]);
828
+ if(matched)str=str.replace(rules[x][0],rules[x][1]);
829
+ }
830
+ }
831
+ }
832
+ return str;
833
+ };
834
+
835
+ /*
836
+
837
+ This is a lighter form of ObjTree, with parts Jester doesn't use removed.
838
+ Compressed using http://dean.edwards.name/packer/.
839
+ Homepage: http://www.kawa.net/works/js/xml/objtree-e.html
840
+
841
+ XML.ObjTree -- XML source code from/to JavaScript object like E4X
842
+
843
+ Copyright (c) 2005-2006 Yusuke Kawasaki. All rights reserved.
844
+ This program is free software; you can redistribute it and/or
845
+ modify it under the Artistic license. Or whatever license I choose,
846
+ which I will do instead of keeping this documentation like it is.
847
+
848
+ */
849
+
850
+ eval(function(p,a,c,k,e,r){e=function(c){return(c<a?'':e(parseInt(c/a)))+((c=c%a)>35?String.fromCharCode(c+29):c.toString(36))};if(!''.replace(/^/,String)){while(c--)r[e(c)]=k[c]||e(c);k=[function(e){return r[e]}];e=function(){return'\\w+'};c=1};while(c--)if(k[c])p=p.replace(new RegExp('\\b'+e(c)+'\\b','g'),k[c]);return p}('5(p(o)==\'w\')o=v(){};o.r=v(){m 9};o.r.1i="0.1b";o.r.u.14=\'<?L 1s="1.0" 1o="1n-8" ?>\\n\';o.r.u.Y=\'-\';o.r.u.1c=\'1a/L\';o.r.u.N=v(a){6 b;5(W.U){6 c=K U();6 d=c.1r(a,"1p/L");5(!d)m;b=d.A}q 5(W.10){c=K 10(\'1k.1h\');c.1g=z;c.1e(a);b=c.A}5(!b)m;m 9.E(b)};o.r.u.1d=v(c,d,e){6 f={};y(6 g 19 d){f[g]=d[g]}5(!f.M){5(p(f.18)=="w"&&p(f.17)=="w"&&p(f.16)=="w"){f.M="15"}q{f.M="13"}}5(e){f.X=V;6 h=9;6 i=e;6 j=f.T;f.T=v(a){6 b;5(a&&a.x&&a.x.A){b=h.E(a.x.A)}q 5(a&&a.J){b=h.N(a.J)}i(b,a);5(j)j(a)}}q{f.X=z}6 k;5(p(S)!="w"&&S.I){f.1q=c;6 l=K S.I(f);5(l)k=l.12}q 5(p(Q)!="w"&&Q.I){6 l=K Q.I(c,f);5(l)k=l.12}5(e)m k;5(k&&k.x&&k.x.A){m 9.E(k.x.A)}q 5(k&&k.J){m 9.N(k.J)}};o.r.u.E=v(a){5(!a)m;9.H={};5(9.P){y(6 i=0;i<9.P.t;i++){9.H[9.P[i]]=1}}6 b=9.O(a);5(9.H[a.F]){b=[b]}5(a.B!=11){6 c={};c[a.F]=b;b=c}m b};o.r.u.O=v(a){5(a.B==7){m}5(a.B==3||a.B==4){6 b=a.G.1j(/[^\\1f-\\1l]/);5(b==1m)m z;m a.G}6 c;6 d={};5(a.D&&a.D.t){c={};y(6 i=0;i<a.D.t;i++){6 e=a.D[i].F;5(p(e)!="Z")C;6 f=a.D[i].G;5(!f)C;e=9.Y+e;5(p(d[e])=="w")d[e]=0;d[e]++;9.R(c,e,d[e],f)}}5(a.s&&a.s.t){6 g=V;5(c)g=z;y(6 i=0;i<a.s.t&&g;i++){6 h=a.s[i].B;5(h==3||h==4)C;g=z}5(g){5(!c)c="";y(6 i=0;i<a.s.t;i++){c+=a.s[i].G}}q{5(!c)c={};y(6 i=0;i<a.s.t;i++){6 e=a.s[i].F;5(p(e)!="Z")C;6 f=9.O(a.s[i]);5(f==z)C;5(p(d[e])=="w")d[e]=0;d[e]++;9.R(c,e,d[e],f)}}}m c};o.r.u.R=v(a,b,c,d){5(9.H[b]){5(c==1)a[b]=[];a[b][a[b].t]=d}q 5(c==1){a[b]=d}q 5(c==2){a[b]=[a[b],d]}q{a[b][a[b].t]=d}};',62,91,'|||||if|var|||this|||||||||||||return||XML|typeof|else|ObjTree|childNodes|length|prototype|function|undefined|responseXML|for|false|documentElement|nodeType|continue|attributes|parseDOM|nodeName|nodeValue|__force_array|Request|responseText|new|xml|method|parseXML|parseElement|force_array|Ajax|addNode|HTTP|onComplete|DOMParser|true|window|asynchronous|attr_prefix|string|ActiveXObject||transport|post|xmlDecl|get|parameters|postbody|postBody|in|text|24|overrideMimeType|parseHTTP|loadXML|x00|async|XMLDOM|VERSION|match|Microsoft|x20|null|UTF|encoding|application|uri|parseFromString|version'.split('|'),0,{}))
851
+
852
+ /*
853
+
854
+ This is a Date parsing library by Nicholas Barthelemy, packed to keep jester.js light.
855
+ Homepage: https://svn.nbarthelemy.com/date-js/
856
+ Compressed using http://dean.edwards.name/packer/.
857
+
858
+ */
859
+
860
+ eval(function(p,a,c,k,e,r){e=function(c){return(c<a?'':e(parseInt(c/a)))+((c=c%a)>35?String.fromCharCode(c+29):c.toString(36))};if(!''.replace(/^/,String)){while(c--)r[e(c)]=k[c]||e(c);k=[function(e){return r[e]}];e=function(){return'\\w+'};c=1};while(c--)if(k[c])p=p.replace(new RegExp('\\b'+e(c)+'\\b','g'),k[c]);return p}('N.q.F||(N.q.F=t(a){o u.1d().F(a)});O.q.F||(O.q.F=t(a){o\'0\'.1H(a-u.K)+u});O.q.1H||(O.q.1H=t(a){v s=\'\',i=0;2k(i++<a){s+=u}o s});N.q.1j||(N.q.1j=t(){o u.1d().1j()});O.q.1j||(O.q.1j=t(){v n=u,l=n.K,i=-1;2k(i++<l){u.20(i,i+1)==0?n=n.20(1,n.K):i=l}o n});k.1m="2H 2F 2z 2y 2x 2u 2r 3q 3n 3k 3i 3d".1x(" ");k.1o="38 35 2Y 2U 2Q 2O 2M".1x(" ");k.2K="31 28 31 30 31 30 31 31 30 31 30 31".1x(" ");k.1A={2G:"%Y-%m-%d %H:%M:%S",2w:"%Y-%m-%2v%H:%M:%S%T",2s:"%a, %d %b %Y %H:%M:%S %Z",3p:"%d %b %H:%M",3o:"%B %d, %Y %H:%M"};k.3l=-1;k.3j=-2;(t(){v d=k;d["3h"]=1;d["2i"]=1t;d["2h"]=d["2i"]*19;d["2e"]=d["2h"]*19;d["P"]=d["2e"]*24;d["37"]=d["P"]*7;d["34"]=d["P"]*31;d["1q"]=d["P"]*2X;d["2W"]=d["1q"]*10;d["2R"]=d["1q"]*23;d["2P"]=d["1q"]*1t})();k.q.1D||(k.q.1D=t(){o D k(u.1k())});k.q.26||(k.q.26=t(a,b){u.1F(u.1k()+((a||k.P)*(b||1)));o u});k.q.2a||(k.q.2a=t(a,b){u.1F(u.1k()-((a||k.P)*(b||1)));o u});k.q.1Z||(k.q.1Z=t(){u.1Y(0);u.1X(0);u.1U(0);u.1T(0);o u});k.q.1I||(k.q.1I=t(a,b){C(1i a==\'1p\')a=k.1J(a);o 18.2l((u.1k()-a.1k())/(b|k.P))});k.q.1N||(k.q.1N=k.q.1I);k.q.2n||(k.q.2n=t(){d=O(u);o d.1f(-(18.1y(d.K,2)))>3&&d.1f(-(18.1y(d.K,2)))<21?"V":["V","17","16","1a","V"][18.1y(N(d)%10,4)]});k.q.1w||(k.q.1w=t(){v f=(D k(u.1h(),0,1)).1e();o 18.2t((u.1n()+(f>3?f-4:f+3))/7)});k.q.1M=t(){o u.1d().1v(/^.*? ([A-Z]{3}) [0-9]{4}.*$/,"$1").1v(/^.*?\\(([A-Z])[a-z]+ ([A-Z])[a-z]+ ([A-Z])[a-z]+\\)$/,"$1$2$3")};k.q.2p=t(){o(u.1u()>0?"-":"+")+O(18.2l(u.1u()/19)).F(2)+O(u.1u()%19,2,"0").F(2)};k.q.1n||(k.q.1n=t(){o((k.2o(u.1h(),u.1c(),u.1b()+1,0,0,0)-k.2o(u.1h(),0,1,0,0,0))/k.P)});k.q.2m||(k.q.2m=t(){v a=u.1D();a.15(a.1c()+1);a.L(0);o a.1b()});k.2j||(k.2j=t(a,b){a=(a+12)%12;C(k.1K(b)&&a==1)o 29;o k.3g.3f[a]});k.1K||(k.1K=t(a){o(((a%4)==0)&&((a%23)!=0)||((a%3e)==0))});k.q.1B||(k.q.1B=t(c){C(!u.3c())o\'&3b;\';v d=u;C(k.1A[c.2g()])c=k.1A[c.2g()];o c.1v(/\\%([3a])/g,t(a,b){39(b){E\'a\':o k.1l(d.1e()).1f(0,3);E\'A\':o k.1l(d.1e());E\'b\':o k.13(d.1c()).1f(0,3);E\'B\':o k.13(d.1c());E\'c\':o d.1d();E\'d\':o d.1b().F(2);E\'H\':o d.1G().F(2);E\'I\':o((h=d.1G()%12)?h:12).F(2);E\'j\':o d.1n().F(3);E\'m\':o(d.1c()+1).F(2);E\'M\':o d.36().F(2);E\'p\':o d.1G()<12?\'33\':\'32\';E\'S\':o d.2Z().F(2);E\'U\':o d.1w().F(2);E\'W\':R Q("%W 2V 2T 2S 25");E\'w\':o d.1e();E\'x\':o d.1r("%m/%d/%Y");E\'X\':o d.1r("%I:%M%p");E\'y\':o d.1h().1d().1f(2);E\'Y\':o d.1h();E\'T\':o d.2p();E\'Z\':o d.1M()}})});k.q.1r||(k.q.1r=k.q.1B);k.22=k.1J;k.1J=t(a){C(1i a!=\'1p\')o a;C(a.K==0||(/^\\s+$/).1E(a))o;2N(v i=0;i<k.1g.K;i++){v r=k.1g[i].J.2L(a);C(r)o k.1g[i].G(r)}o D k(k.22(a))};k.13||(k.13=t(c){v d=-1;C(1i c==\'2J\'){o k.1m[c.1c()]}2I C(1i c==\'27\'){d=c-1;C(d<0||d>11)R D Q("1s 1C 2b 2q 1W 1V 2d 1 2c 12:"+d);o k.1m[d]}v m=k.1m.1S(t(a,b){C(D 1O("^"+c,"i").1E(a)){d=b;o 1R}o 2f});C(m.K==0)R D Q("1s 1C 1p");C(m.K>1)R D Q("1Q 1C");o k.1m[d]});k.1l||(k.1l=t(c){v d=-1;C(1i c==\'27\'){d=c-1;C(d<0||d>6)R D Q("1s 1z 2b 2q 1W 1V 2d 1 2c 7");o k.1o[d]}v m=k.1o.1S(t(a,b){C(D 1O("^"+c,"i").1E(a)){d=b;o 1R}o 2f});C(m.K==0)R D Q("1s 1z 1p");C(m.K>1)R D Q("1Q 1z");o k.1o[d]});k.1g||(k.1g=[{J:/(\\d{1,2})\\/(\\d{1,2})\\/(\\d{2,4})/,G:t(a){v d=D k();d.1L(a[3]);d.L(14(a[2],10));d.15(14(a[1],10)-1);o d}},{J:/(\\d{4})(?:-?(\\d{2})(?:-?(\\d{2})(?:[T ](\\d{2})(?::?(\\d{2})(?::?(\\d{2})(?:\\.(\\d+))?)?)?(?:Z|(?:([-+])(\\d{2})(?::?(\\d{2}))?)?)?)?)?)?/,G:t(a){v b=0;v d=D k(a[1],0,1);C(a[2])d.15(a[2]-1);C(a[3])d.L(a[3]);C(a[4])d.1Y(a[4]);C(a[5])d.1X(a[5]);C(a[6])d.1U(a[6]);C(a[7])d.1T(N("0."+a[7])*1t);C(a[9]){b=(N(a[9])*19)+N(a[10]);b*=((a[8]==\'-\')?1:-1)}b-=d.1u();1P=(N(d)+(b*19*1t));d.1F(N(1P));o d}},{J:/^2E/i,G:t(){o D k()}},{J:/^2D/i,G:t(){v d=D k();d.L(d.1b()+1);o d}},{J:/^2C/i,G:t(){v d=D k();d.L(d.1b()-1);o d}},{J:/^(\\d{1,2})(17|16|1a|V)?$/i,G:t(a){v d=D k();d.L(14(a[1],10));o d}},{J:/^(\\d{1,2})(?:17|16|1a|V)? (\\w+)$/i,G:t(a){v d=D k();d.L(14(a[1],10));d.15(k.13(a[2]));o d}},{J:/^(\\d{1,2})(?:17|16|1a|V)? (\\w+),? (\\d{4})$/i,G:t(a){v d=D k();d.L(14(a[1],10));d.15(k.13(a[2]));d.1L(a[3]);o d}},{J:/^(\\w+) (\\d{1,2})(?:17|16|1a|V)?$/i,G:t(a){v d=D k();d.L(14(a[2],10));d.15(k.13(a[1]));o d}},{J:/^(\\w+) (\\d{1,2})(?:17|16|1a|V)?,? (\\d{4})$/i,G:t(a){v d=D k();d.L(14(a[2],10));d.15(k.13(a[1]));d.1L(a[3]);o d}},{J:/^3m (\\w+)$/i,G:t(a){v d=D k();v b=d.1e();v c=k.1l(a[1]);v e=c-b;C(c<=b){e+=7}d.L(d.1b()+e);o d}},{J:/^2B (\\w+)$/i,G:t(a){R D Q("2A 25 3r");}}]);',62,214,'||||||||||||||||||||Date||||return||prototype|||function|this|var|||||||if|new|case|zf|handler|||re|length|setDate||Number|String|DAY|Error|throw||||th||||||||parseMonth|parseInt|setMonth|nd|st|Math|60|rd|getDate|getMonth|toString|getDay|substr|__PARSE_PATTERNS|getFullYear|typeof|rz|getTime|parseDay|MONTH_NAMES|getDayOfYear|DAY_NAMES|string|YEAR|format|Invalid|1000|getTimezoneOffset|replace|getWeek|split|min|day|FORMATS|strftime|month|clone|test|setTime|getHours|str|diff|parse|isLeapYear|setYear|getTimezone|compare|RegExp|time|Ambiguous|true|findAll|setMilliseconds|setSeconds|be|must|setMinutes|setHours|clearTime|substring||__native_parse|100||yet|increment|number|||decrement|index|and|between|HOUR|false|toLowerCase|MINUTE|SECOND|daysInMonth|while|floor|lastDayOfMonth|getOrdinal|UTC|getGMTOffset|value|July|rfc822|round|June|dT|iso8601|May|April|March|Not|last|yes|tom|tod|February|db|January|else|object|DAYS_PER_MONTH|exec|Saturday|for|Friday|MILLENNIUM|Thursday|CENTURY|supported|not|Wednesday|is|DECADE|365|Tuesday|getSeconds|||PM|AM|MONTH|Monday|getMinutes|WEEK|Sunday|switch|aAbBcdHIjmMpSUWwxXyYTZ|nbsp|valueOf|December|400|DAYS_IN_MONTH|Convensions|MILLISECOND|November|ERA|October|EPOCH|next|September|long|short|August|implemented'.split('|'),0,{}))