brancusi 0.0.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/Rakefile +13 -0
- data/lib/assets/javascripts/brancusi/application/application.js.coffee +114 -0
- data/lib/assets/javascripts/brancusi/application/application_controller.js.coffee +17 -0
- data/lib/assets/javascripts/brancusi/application/application_module.js.coffee +29 -0
- data/lib/assets/javascripts/brancusi/application/bootstrapper.js.coffee +18 -0
- data/lib/assets/javascripts/brancusi/application/index.js.coffee +1 -0
- data/lib/assets/javascripts/brancusi/application/sandbox.js.coffee +14 -0
- data/lib/assets/javascripts/brancusi/container/container.js.coffee +139 -0
- data/lib/assets/javascripts/brancusi/container/dependent_module.js.coffee +30 -0
- data/lib/assets/javascripts/brancusi/container/dependent_object.js.coffee +9 -0
- data/lib/assets/javascripts/brancusi/container/index.js.coffee +1 -0
- data/lib/assets/javascripts/brancusi/events/event_object.js.coffee +10 -0
- data/lib/assets/javascripts/brancusi/events/events_module.js.coffee +14 -0
- data/lib/assets/javascripts/brancusi/events/index.js.coffee +1 -0
- data/lib/assets/javascripts/brancusi/events/mediator.js.coffee +70 -0
- data/lib/assets/javascripts/brancusi/index.js.coffee +2 -0
- data/lib/assets/javascripts/brancusi/namespace.js.coffee +13 -0
- data/lib/assets/javascripts/brancusi/object_model/base_object.js.coffee +27 -0
- data/lib/assets/javascripts/brancusi/object_model/decorate.js.coffee +14 -0
- data/lib/assets/javascripts/brancusi/object_model/extend.js.coffee +10 -0
- data/lib/assets/javascripts/brancusi/object_model/include.js.coffee +13 -0
- data/lib/assets/javascripts/brancusi/object_model/index.js.coffee +1 -0
- data/lib/assets/javascripts/brancusi/renderer/index.js.coffee +1 -0
- data/lib/assets/javascripts/brancusi/renderer/region_manager.js.coffee +30 -0
- data/lib/assets/javascripts/brancusi/renderer/renderer.js.coffee +41 -0
- data/lib/assets/javascripts/brancusi/renderer/template_manager.js.coffee +0 -0
- data/lib/assets/javascripts/brancusi/routes/dispatcher.js.coffee +10 -0
- data/lib/assets/javascripts/brancusi/routes/index.js.coffee +1 -0
- data/lib/assets/javascripts/brancusi/routes/mapper.js.coffee +41 -0
- data/lib/assets/javascripts/brancusi/routes/router.js.coffee +15 -0
- data/lib/assets/javascripts/brancusi/support/davis_router.js.coffee +19 -0
- data/lib/assets/javascripts/brancusi/support/knockout_renderer.js.coffee +12 -0
- data/lib/assets/javascripts/davis.js +1838 -0
- data/lib/assets/javascripts/knockout.js +3583 -0
- data/lib/assets/javascripts/underscore.js +1221 -0
- data/lib/assets/javascripts/underscore.string.js +600 -0
- data/lib/brancusi.rb +4 -0
- data/lib/brancusi/engine.rb +4 -0
- data/lib/brancusi/version.rb +3 -0
- metadata +282 -0
@@ -0,0 +1,27 @@
|
|
1
|
+
#= require ./include
|
2
|
+
#= require ./decorate
|
3
|
+
|
4
|
+
namespace "brancusi"
|
5
|
+
|
6
|
+
# Implements convenience methods to include mixins and decorate instances.
|
7
|
+
#
|
8
|
+
class brancusi.BaseObject
|
9
|
+
|
10
|
+
# Static method to include a module into the class.
|
11
|
+
#
|
12
|
+
# @param module [Object] the module to include.
|
13
|
+
#
|
14
|
+
@include: (module) ->
|
15
|
+
brancusi.include(@, module)
|
16
|
+
|
17
|
+
# Static method to extend the class with a module.
|
18
|
+
#
|
19
|
+
# @param module [Object] the module to include.
|
20
|
+
#
|
21
|
+
@extend: (module) ->
|
22
|
+
brancusi.extend(@, module)
|
23
|
+
|
24
|
+
# Decorates the instance with the given decorator.
|
25
|
+
#
|
26
|
+
decorate: (decorator, args...)->
|
27
|
+
brancusi.decorate(@, decorator, args...)
|
@@ -0,0 +1,14 @@
|
|
1
|
+
#= require ./extend
|
2
|
+
|
3
|
+
namespace "brancusi"
|
4
|
+
|
5
|
+
# Decorates the target object with methods from the given decorator, and invokes the constructor.
|
6
|
+
#
|
7
|
+
# @param target [Object] the object to decorate.
|
8
|
+
# @param decorator [Class] the decorator to use.
|
9
|
+
# @param args... [Array] the arguments to pass to the decorator's constructor.
|
10
|
+
#
|
11
|
+
brancusi.decorate = ( target, decorator, args... ) ->
|
12
|
+
|
13
|
+
brancusi.extend( target, decorator )
|
14
|
+
decorator.apply( target, args )
|
@@ -0,0 +1,10 @@
|
|
1
|
+
namespace "brancusi"
|
2
|
+
|
3
|
+
# Extends the target with methods from the given module's prototype.
|
4
|
+
#
|
5
|
+
# @param target [Object/Class] the object or class to extend.
|
6
|
+
# @param module [Class] the module to mixin.
|
7
|
+
#
|
8
|
+
brancusi.extend = ( target, module ) ->
|
9
|
+
for key, value of module::
|
10
|
+
target[key] = value
|
@@ -0,0 +1,13 @@
|
|
1
|
+
namespace "brancusi"
|
2
|
+
|
3
|
+
# Extends the target with methods from the given mixin.
|
4
|
+
#
|
5
|
+
# @param target [Class] the class to mix in the module.
|
6
|
+
# @param module [Class] the module to mixin.
|
7
|
+
#
|
8
|
+
brancusi.include = ( target, module ) ->
|
9
|
+
for key, value of module
|
10
|
+
target[key] = value
|
11
|
+
|
12
|
+
for key, value of module::
|
13
|
+
target::[key] = value
|
@@ -0,0 +1 @@
|
|
1
|
+
#= require_tree .
|
@@ -0,0 +1 @@
|
|
1
|
+
#= require_tree .
|
@@ -0,0 +1,30 @@
|
|
1
|
+
namespace "brancusi.renderer"
|
2
|
+
|
3
|
+
class brancusi.renderer.RegionManager
|
4
|
+
|
5
|
+
# Looks up and returns a region by name.
|
6
|
+
#
|
7
|
+
# @param region_name [String] the name of the region.
|
8
|
+
# @return [Array] the jQuery array of matching elements.
|
9
|
+
#
|
10
|
+
find: (region_name) ->
|
11
|
+
$("[data-region='#{region_name}']")
|
12
|
+
|
13
|
+
|
14
|
+
# Returns the child regions in the given document element.
|
15
|
+
#
|
16
|
+
# @param el [Element] the element to search within.
|
17
|
+
# @return [Array] a jQuery array of matching elements.
|
18
|
+
#
|
19
|
+
children: (el) ->
|
20
|
+
$(el).find("[data-region]")
|
21
|
+
|
22
|
+
|
23
|
+
# Returns the name of the given region element.
|
24
|
+
#
|
25
|
+
# @param region [Element] the region.
|
26
|
+
# @return [String] the name of the region.
|
27
|
+
#
|
28
|
+
region_name: (region) ->
|
29
|
+
$(region).data("region")
|
30
|
+
|
@@ -0,0 +1,41 @@
|
|
1
|
+
namespace "brancusi.renderer"
|
2
|
+
|
3
|
+
class brancusi.renderer.Renderer extends brancusi.ApplicationModule
|
4
|
+
|
5
|
+
@dependency region_manager: 'RegionManager'
|
6
|
+
|
7
|
+
constructor: ->
|
8
|
+
super('renderer')
|
9
|
+
@default_bindings = {}
|
10
|
+
|
11
|
+
# Renders a region with the given data/template bindings, then recursively renders any child regions.
|
12
|
+
#
|
13
|
+
# @param region [Element] the document region to render.
|
14
|
+
# @param bindings [Object] an object specifying the binding mappings.
|
15
|
+
#
|
16
|
+
render_region: (region, bindings) ->
|
17
|
+
region_name = @region_manager.region_name(region)
|
18
|
+
binding = bindings?[region_name]
|
19
|
+
|
20
|
+
template = binding?.template || region_name
|
21
|
+
data = binding?.data
|
22
|
+
|
23
|
+
@render_template(template, data, region)
|
24
|
+
|
25
|
+
for child in @region_manager.children(region)
|
26
|
+
@render_region($(child), bindings)
|
27
|
+
|
28
|
+
|
29
|
+
# Renders a page with the given template and data model.
|
30
|
+
#
|
31
|
+
# @param template [String] the name of the template to render the 'content' region.
|
32
|
+
# @param data [Object] the data model for the 'content' region.
|
33
|
+
# @param bindings [Object] any further data bindings (e.g. for other regions).
|
34
|
+
#
|
35
|
+
render_page: (template, data, bindings = {}) ->
|
36
|
+
bindings = _.defaults bindings, @default_bindings,
|
37
|
+
content: { template: template, data: data }
|
38
|
+
|
39
|
+
master_region = @region_manager.find('master')
|
40
|
+
@render_region(master_region, bindings)
|
41
|
+
|
File without changes
|
@@ -0,0 +1,10 @@
|
|
1
|
+
namespace "brancusi.routes"
|
2
|
+
|
3
|
+
class brancusi.routes.Dispatcher extends brancusi.DependentObject
|
4
|
+
|
5
|
+
constructor: (@app, @controller_name, @action_name) ->
|
6
|
+
|
7
|
+
dispatch: =>
|
8
|
+
@controller = @app.controllers["#{@controller_name}"] unless @controller?
|
9
|
+
@controller.begin_request(@action_name)
|
10
|
+
@controller[@action_name].apply(@controller, arguments)
|
@@ -0,0 +1 @@
|
|
1
|
+
#= require_tree .
|
@@ -0,0 +1,41 @@
|
|
1
|
+
namespace "brancusi.routes"
|
2
|
+
|
3
|
+
# given a url, maps to a controller + action
|
4
|
+
# given a resource, maps to a url
|
5
|
+
|
6
|
+
class brancusi.routes.Mapper
|
7
|
+
|
8
|
+
constructor: ->
|
9
|
+
@mappings = {}
|
10
|
+
|
11
|
+
draw: (block) ->
|
12
|
+
block.apply(@)
|
13
|
+
|
14
|
+
match: (url, opts) ->
|
15
|
+
@mappings[url] = @parse(opts)
|
16
|
+
|
17
|
+
parse: (opts) ->
|
18
|
+
if _.isString(opts)
|
19
|
+
[controller, action] = /(.*)#(.*)/.exec(opts)[1..2]
|
20
|
+
{ controller: controller, action: action }
|
21
|
+
else
|
22
|
+
opts
|
23
|
+
|
24
|
+
apply: (app, router) ->
|
25
|
+
for url, { controller, action } of @mappings
|
26
|
+
dispatcher = new brancusi.routes.Dispatcher(app, controller, action)
|
27
|
+
router.route(url, dispatcher.dispatch)
|
28
|
+
|
29
|
+
# resource: ( resource ) ->
|
30
|
+
# for action in ['view', 'edit']
|
31
|
+
# path_name = opt?.paths_names?[action]?
|
32
|
+
# path_name ?= action
|
33
|
+
# @router.route "#{resource}/:id/#{action}", resource, action
|
34
|
+
# for action in ['create']
|
35
|
+
# path_name = opt?.paths_names?[action]?
|
36
|
+
# path_name ?= action
|
37
|
+
# @router.route "#{resource}/#{action}", resource, action
|
38
|
+
|
39
|
+
#namespace: ( path, block ) ->
|
40
|
+
# @scope.push path
|
41
|
+
# @draw( block )
|
@@ -0,0 +1,15 @@
|
|
1
|
+
class brancusi.Router extends brancusi.ApplicationModule
|
2
|
+
|
3
|
+
constructor: ->
|
4
|
+
super('router')
|
5
|
+
|
6
|
+
initialize: (app) ->
|
7
|
+
console.log('initializing router')
|
8
|
+
@configure(app.constructor.config)
|
9
|
+
app.constructor.routes.apply(app, @)
|
10
|
+
|
11
|
+
route: (url, action) ->
|
12
|
+
throw new Error("Missing implementation for Router.route")
|
13
|
+
|
14
|
+
configure: (config) ->
|
15
|
+
throw new Error("Missing implementation for Router.configure")
|
@@ -0,0 +1,19 @@
|
|
1
|
+
namespace "brancusi.routes"
|
2
|
+
|
3
|
+
class brancusi.routes.DavisRouter extends brancusi.Router
|
4
|
+
|
5
|
+
constructor: ->
|
6
|
+
@davis = new Davis
|
7
|
+
|
8
|
+
route: (url, action) ->
|
9
|
+
@davis.get(url, action)
|
10
|
+
|
11
|
+
configure: (config) ->
|
12
|
+
@davis.configure(-> @generateRequestOnPageLoad = true) if config.route_on_load
|
13
|
+
|
14
|
+
@on "application.initialize", (app) ->
|
15
|
+
@initialize(app)
|
16
|
+
|
17
|
+
@on "application.ready", ->
|
18
|
+
@davis.start()
|
19
|
+
|
@@ -0,0 +1,12 @@
|
|
1
|
+
namespace "brancusi.renderer"
|
2
|
+
|
3
|
+
class brancusi.renderer.KnockoutRenderer extends brancusi.renderer.Renderer
|
4
|
+
|
5
|
+
render_template: (template, data, target) ->
|
6
|
+
target.html $("<div></div>").attr 'data-bind',
|
7
|
+
"template: 'template:#{template}'"
|
8
|
+
|
9
|
+
ko.applyBindings(data || {}, target[0])
|
10
|
+
|
11
|
+
|
12
|
+
|
@@ -0,0 +1,1838 @@
|
|
1
|
+
/*!
|
2
|
+
* Davis - http://davisjs.com - JavaScript Routing - 0.9.6
|
3
|
+
* Copyright (C) 2011 Oliver Nightingale
|
4
|
+
* MIT Licensed
|
5
|
+
*/
|
6
|
+
;
|
7
|
+
/**
|
8
|
+
* Convinience method for instantiating a new Davis app and configuring it to use the passed
|
9
|
+
* routes and subscriptions.
|
10
|
+
*
|
11
|
+
* @param {Function} config A function that will be run with a newly created Davis.App as its context,
|
12
|
+
* should be used to set up app routes, subscriptions and settings etc.
|
13
|
+
* @namespace
|
14
|
+
* @returns {Davis.App}
|
15
|
+
*/
|
16
|
+
Davis = function (config) {
|
17
|
+
var app = new Davis.App
|
18
|
+
config && config.call(app)
|
19
|
+
Davis.$(function () { app.start() })
|
20
|
+
return app
|
21
|
+
};
|
22
|
+
|
23
|
+
/**
|
24
|
+
* Stores the DOM library that Davis will use. Can be overriden to use libraries other than jQuery.
|
25
|
+
*/
|
26
|
+
if (window.jQuery) {
|
27
|
+
Davis.$ = jQuery
|
28
|
+
} else {
|
29
|
+
Davis.$ = null
|
30
|
+
};
|
31
|
+
|
32
|
+
/**
|
33
|
+
* Checks whether Davis is supported in the current browser
|
34
|
+
*
|
35
|
+
* @returns {Boolean}
|
36
|
+
*/
|
37
|
+
Davis.supported = function () {
|
38
|
+
return (typeof window.history.pushState == 'function')
|
39
|
+
}
|
40
|
+
|
41
|
+
/*!
|
42
|
+
* A function that does nothing, used as a default param for any callbacks.
|
43
|
+
*
|
44
|
+
* @private
|
45
|
+
* @returns {Function}
|
46
|
+
*/
|
47
|
+
Davis.noop = function () {}
|
48
|
+
|
49
|
+
/**
|
50
|
+
* Method to extend the Davis library with an extension.
|
51
|
+
*
|
52
|
+
* An extension is just a function that will modify the Davis framework in some way,
|
53
|
+
* for example changing how the routing works or adjusting where Davis thinks it is supported.
|
54
|
+
*
|
55
|
+
* Example:
|
56
|
+
* Davis.extend(Davis.hashBasedRouting)
|
57
|
+
*
|
58
|
+
* @param {Function} extension the function that will extend Davis
|
59
|
+
*
|
60
|
+
*/
|
61
|
+
Davis.extend = function (extension) {
|
62
|
+
extension(Davis)
|
63
|
+
}
|
64
|
+
|
65
|
+
/*!
|
66
|
+
* the version
|
67
|
+
*/
|
68
|
+
Davis.version = "0.9.6";/*!
|
69
|
+
* Davis - utils
|
70
|
+
* Copyright (C) 2011 Oliver Nightingale
|
71
|
+
* MIT Licensed
|
72
|
+
*/
|
73
|
+
|
74
|
+
/*!
|
75
|
+
* A module that provides wrappers around modern JavaScript so that native implementations are used
|
76
|
+
* whereever possible and JavaScript implementations are used in those browsers that do not natively
|
77
|
+
* support them.
|
78
|
+
*/
|
79
|
+
Davis.utils = (function () {
|
80
|
+
|
81
|
+
/*!
|
82
|
+
* A wrapper around native Array.prototype.every.
|
83
|
+
*
|
84
|
+
* Falls back to a pure JavaScript implementation in browsers that do not support Array.prototype.every.
|
85
|
+
* For more details see the full docs on MDC https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/every
|
86
|
+
*
|
87
|
+
* @private
|
88
|
+
* @param {array} the array to loop through
|
89
|
+
* @param {fn} the function to that performs the every check
|
90
|
+
* @param {thisp} an optional param that will be set as fn's this value
|
91
|
+
* @returns {Array}
|
92
|
+
*/
|
93
|
+
if (Array.prototype.every) {
|
94
|
+
var every = function (array, fn) {
|
95
|
+
return array.every(fn, arguments[2])
|
96
|
+
}
|
97
|
+
} else {
|
98
|
+
var every = function (array, fn) {
|
99
|
+
if (array === void 0 || array === null) throw new TypeError();
|
100
|
+
var t = Object(array);
|
101
|
+
var len = t.length >>> 0;
|
102
|
+
if (typeof fn !== "function") throw new TypeError();
|
103
|
+
|
104
|
+
var thisp = arguments[2];
|
105
|
+
for (var i = 0; i < len; i++) {
|
106
|
+
if (i in t && !fn.call(thisp, t[i], i, t)) return false;
|
107
|
+
}
|
108
|
+
|
109
|
+
return true;
|
110
|
+
}
|
111
|
+
};
|
112
|
+
|
113
|
+
/*!
|
114
|
+
* A wrapper around native Array.prototype.forEach.
|
115
|
+
*
|
116
|
+
* Falls back to a pure JavaScript implementation in browsers that do not support Array.prototype.forEach.
|
117
|
+
* For more details see the full docs on MDC https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/forEach
|
118
|
+
*
|
119
|
+
* @private
|
120
|
+
* @param {array} the array to loop through
|
121
|
+
* @param {fn} the function to apply to every element of the array
|
122
|
+
* @param {thisp} an optional param that will be set as fn's this value
|
123
|
+
* @returns {Array}
|
124
|
+
*/
|
125
|
+
if (Array.prototype.forEach) {
|
126
|
+
var forEach = function (array, fn) {
|
127
|
+
return array.forEach(fn, arguments[2])
|
128
|
+
}
|
129
|
+
} else {
|
130
|
+
var forEach = function (array, fn) {
|
131
|
+
if (array === void 0 || array === null) throw new TypeError();
|
132
|
+
var t = Object(array);
|
133
|
+
var len = t.length >>> 0;
|
134
|
+
if (typeof fn !== "function") throw new TypeError();
|
135
|
+
|
136
|
+
|
137
|
+
var thisp = arguments[2];
|
138
|
+
for (var i = 0; i < len; i++) {
|
139
|
+
if (i in t) fn.call(thisp, t[i], i, t);
|
140
|
+
}
|
141
|
+
};
|
142
|
+
};
|
143
|
+
|
144
|
+
/*!
|
145
|
+
* A wrapper around native Array.prototype.filter.
|
146
|
+
* Falls back to a pure JavaScript implementation in browsers that do not support Array.prototype.filter.
|
147
|
+
* For more details see the full docs on MDC https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/filter
|
148
|
+
*
|
149
|
+
* @private
|
150
|
+
* @param {array} the array to filter
|
151
|
+
* @param {fn} the function to do the filtering
|
152
|
+
* @param {thisp} an optional param that will be set as fn's this value
|
153
|
+
* @returns {Array}
|
154
|
+
*/
|
155
|
+
if (Array.prototype.filter) {
|
156
|
+
var filter = function (array, fn) {
|
157
|
+
return array.filter(fn, arguments[2])
|
158
|
+
}
|
159
|
+
} else {
|
160
|
+
var filter = function(array, fn) {
|
161
|
+
if (array === void 0 || array === null) throw new TypeError();
|
162
|
+
var t = Object(array);
|
163
|
+
var len = t.length >>> 0;
|
164
|
+
if (typeof fn !== "function") throw new TypeError();
|
165
|
+
|
166
|
+
|
167
|
+
var res = [];
|
168
|
+
var thisp = arguments[2];
|
169
|
+
for (var i = 0; i < len; i++) {
|
170
|
+
if (i in t) {
|
171
|
+
var val = t[i]; // in case fn mutates this
|
172
|
+
if (fn.call(thisp, val, i, t)) res.push(val);
|
173
|
+
}
|
174
|
+
}
|
175
|
+
|
176
|
+
return res;
|
177
|
+
};
|
178
|
+
};
|
179
|
+
|
180
|
+
/*!
|
181
|
+
* A wrapper around native Array.prototype.map.
|
182
|
+
* Falls back to a pure JavaScript implementation in browsers that do not support Array.prototype.map.
|
183
|
+
* For more details see the full docs on MDC https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/map
|
184
|
+
*
|
185
|
+
* @private
|
186
|
+
* @param {array} the array to map
|
187
|
+
* @param {fn} the function to do the mapping
|
188
|
+
* @param {thisp} an optional param that will be set as fn's this value
|
189
|
+
* @returns {Array}
|
190
|
+
*/
|
191
|
+
|
192
|
+
if (Array.prototype.map) {
|
193
|
+
var map = function (array, fn) {
|
194
|
+
return array.map(fn, arguments[2])
|
195
|
+
}
|
196
|
+
} else {
|
197
|
+
var map = function(array, fn) {
|
198
|
+
if (array === void 0 || array === null)
|
199
|
+
throw new TypeError();
|
200
|
+
|
201
|
+
var t = Object(array);
|
202
|
+
var len = t.length >>> 0;
|
203
|
+
if (typeof fn !== "function")
|
204
|
+
throw new TypeError();
|
205
|
+
|
206
|
+
var res = new Array(len);
|
207
|
+
var thisp = arguments[2];
|
208
|
+
for (var i = 0; i < len; i++) {
|
209
|
+
if (i in t)
|
210
|
+
res[i] = fn.call(thisp, t[i], i, t);
|
211
|
+
}
|
212
|
+
|
213
|
+
return res;
|
214
|
+
};
|
215
|
+
};
|
216
|
+
|
217
|
+
/*!
|
218
|
+
* A convinience function for converting arguments to a proper array
|
219
|
+
*
|
220
|
+
* @private
|
221
|
+
* @param {args} a functions arguments
|
222
|
+
* @param {start} an integer at which to start converting the arguments to an array
|
223
|
+
* @returns {Array}
|
224
|
+
*/
|
225
|
+
var toArray = function (args, start) {
|
226
|
+
return Array.prototype.slice.call(args, start || 0)
|
227
|
+
}
|
228
|
+
|
229
|
+
/*!
|
230
|
+
* Exposing the public interface to the Utils module
|
231
|
+
* @private
|
232
|
+
*/
|
233
|
+
return {
|
234
|
+
every: every,
|
235
|
+
forEach: forEach,
|
236
|
+
filter: filter,
|
237
|
+
toArray: toArray,
|
238
|
+
map: map
|
239
|
+
}
|
240
|
+
})()
|
241
|
+
|
242
|
+
/*!
|
243
|
+
* Davis - listener
|
244
|
+
* Copyright (C) 2011 Oliver Nightingale
|
245
|
+
* MIT Licensed
|
246
|
+
*/
|
247
|
+
|
248
|
+
/**
|
249
|
+
* A module to bind to link clicks and form submits and turn what would normally be http requests
|
250
|
+
* into instances of Davis.Request. These request objects are then pushed onto the history stack
|
251
|
+
* using the Davis.history module.
|
252
|
+
*
|
253
|
+
* This module uses Davis.$, which by defualt is jQuery for its event binding and event object normalization.
|
254
|
+
* To use Davis with any, or no, JavaScript framework be sure to provide support for all the methods called
|
255
|
+
* on Davis.$.
|
256
|
+
*
|
257
|
+
* @module
|
258
|
+
*/
|
259
|
+
Davis.listener = function () {
|
260
|
+
|
261
|
+
/*!
|
262
|
+
* Methods to check whether an element has an href or action that is local to this page
|
263
|
+
* @private
|
264
|
+
*/
|
265
|
+
var originChecks = {
|
266
|
+
A: function (elem) {
|
267
|
+
return elem.host !== window.location.host || elem.protocol !== window.location.protocol
|
268
|
+
},
|
269
|
+
|
270
|
+
FORM: function (elem) {
|
271
|
+
var a = document.createElement('a')
|
272
|
+
a.href = elem.action
|
273
|
+
return this.A(a)
|
274
|
+
}
|
275
|
+
}
|
276
|
+
|
277
|
+
/*!
|
278
|
+
* Checks whether the target of a click or submit event has an href or action that is local to the
|
279
|
+
* current page. Only links or targets with local hrefs or actions will be handled by davis, all
|
280
|
+
* others will be ignored.
|
281
|
+
* @private
|
282
|
+
*/
|
283
|
+
var differentOrigin = function (elem) {
|
284
|
+
if (!originChecks[elem.nodeName.toUpperCase()]) return true // the elem is neither a link or a form
|
285
|
+
return originChecks[elem.nodeName.toUpperCase()](elem)
|
286
|
+
}
|
287
|
+
|
288
|
+
var hasModifier = function (event) {
|
289
|
+
return (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey)
|
290
|
+
}
|
291
|
+
|
292
|
+
/*!
|
293
|
+
* A handler that creates a new Davis.Request and pushes it onto the history stack using Davis.history.
|
294
|
+
*
|
295
|
+
* @param {Function} targetExtractor a function that will be called with the event target jQuery object and should return an object with path, title and method.
|
296
|
+
* @private
|
297
|
+
*/
|
298
|
+
var handler = function (targetExtractor) {
|
299
|
+
return function (event) {
|
300
|
+
if (hasModifier(event)) return true
|
301
|
+
if (differentOrigin(this)) return true
|
302
|
+
|
303
|
+
var request = new Davis.Request (targetExtractor.call(Davis.$(this)));
|
304
|
+
Davis.location.assign(request)
|
305
|
+
event.stopPropagation()
|
306
|
+
event.preventDefault()
|
307
|
+
return false;
|
308
|
+
};
|
309
|
+
};
|
310
|
+
|
311
|
+
/*!
|
312
|
+
* A handler specialized for click events. Gets the request details from a link elem
|
313
|
+
* @private
|
314
|
+
*/
|
315
|
+
var clickHandler = handler(function () {
|
316
|
+
var self = this
|
317
|
+
|
318
|
+
return {
|
319
|
+
method: 'get',
|
320
|
+
fullPath: this.prop('href'),
|
321
|
+
title: this.attr('title'),
|
322
|
+
delegateToServer: function () {
|
323
|
+
window.location = self.prop('href')
|
324
|
+
}
|
325
|
+
};
|
326
|
+
});
|
327
|
+
|
328
|
+
/*!
|
329
|
+
* Decodes the url, including + characters.
|
330
|
+
* @private
|
331
|
+
*/
|
332
|
+
var decodeUrl = function (str) {
|
333
|
+
return decodeURIComponent(str.replace(/\+/g, '%20'))
|
334
|
+
};
|
335
|
+
|
336
|
+
/*!
|
337
|
+
* A handler specialized for submit events. Gets the request details from a form elem
|
338
|
+
* @private
|
339
|
+
*/
|
340
|
+
var submitHandler = handler(function () {
|
341
|
+
var self = this
|
342
|
+
return {
|
343
|
+
method: this.attr('method'),
|
344
|
+
fullPath: decodeUrl(this.serialize() ? [this.prop('action'), this.serialize()].join("?") : this.prop('action')),
|
345
|
+
title: this.attr('title'),
|
346
|
+
delegateToServer: function () {
|
347
|
+
self.submit()
|
348
|
+
}
|
349
|
+
};
|
350
|
+
});
|
351
|
+
|
352
|
+
/**
|
353
|
+
* Binds to both link clicks and form submits using jQuery's deleagate.
|
354
|
+
*
|
355
|
+
* Will catch all current and future links and forms. Uses the apps settings for the selector to use for links and forms
|
356
|
+
*
|
357
|
+
* @see Davis.App.settings
|
358
|
+
* @memberOf listener
|
359
|
+
*/
|
360
|
+
this.listen = function () {
|
361
|
+
Davis.$(document).delegate(this.settings.formSelector, 'submit', submitHandler)
|
362
|
+
Davis.$(document).delegate(this.settings.linkSelector, 'click', clickHandler)
|
363
|
+
}
|
364
|
+
|
365
|
+
/**
|
366
|
+
* Unbinds all click and submit handlers that were attatched with listen.
|
367
|
+
*
|
368
|
+
* Will efectivley stop the current app from processing any requests and all links and forms will have their default
|
369
|
+
* behaviour restored.
|
370
|
+
*
|
371
|
+
* @see Davis.App.settings
|
372
|
+
* @memberOf listener
|
373
|
+
*/
|
374
|
+
this.unlisten = function () {
|
375
|
+
Davis.$(document).undelegate(this.settings.linkSelector, 'click', clickHandler)
|
376
|
+
Davis.$(document).undelegate(this.settings.formSelector, 'submit', submitHandler)
|
377
|
+
}
|
378
|
+
}
|
379
|
+
/*!
|
380
|
+
* Davis - event
|
381
|
+
* Copyright (C) 2011 Oliver Nightingale
|
382
|
+
* MIT Licensed
|
383
|
+
*/
|
384
|
+
|
385
|
+
/**
|
386
|
+
* A plugin that adds basic event capabilities to a Davis app, it is included by default.
|
387
|
+
*
|
388
|
+
* @module
|
389
|
+
*/
|
390
|
+
Davis.event = function () {
|
391
|
+
|
392
|
+
/*!
|
393
|
+
* callback storage
|
394
|
+
*/
|
395
|
+
var callbacks = {}
|
396
|
+
|
397
|
+
/**
|
398
|
+
* Binds a callback to a named event.
|
399
|
+
*
|
400
|
+
* The following events are triggered internally by Davis and can be bound to
|
401
|
+
*
|
402
|
+
* * start : Triggered when the application is started
|
403
|
+
* * lookupRoute : Triggered before looking up a route. The request being looked up is passed as an argument
|
404
|
+
* * runRoute : Triggered before running a route. The request and route being run are passed as arguments
|
405
|
+
* * routeNotFound : Triggered if no route for the current request can be found. The current request is passed as an arugment
|
406
|
+
* * requestHalted : Triggered when a before filter halts the current request. The current request is passed as an argument
|
407
|
+
* * unsupported : Triggered when starting a Davis app in a browser that doesn't support html5 pushState
|
408
|
+
*
|
409
|
+
* Example
|
410
|
+
*
|
411
|
+
* app.bind('runRoute', function () {
|
412
|
+
* console.log('about to run a route')
|
413
|
+
* })
|
414
|
+
*
|
415
|
+
* @param {String} event event name
|
416
|
+
* @param {Function} fn callback
|
417
|
+
* @memberOf event
|
418
|
+
*/
|
419
|
+
this.bind = function (event, fn) {
|
420
|
+
(callbacks[event] = callbacks[event] || []).push(fn);
|
421
|
+
return this;
|
422
|
+
};
|
423
|
+
|
424
|
+
/**
|
425
|
+
* Triggers an event with the given arguments.
|
426
|
+
*
|
427
|
+
* @param {String} event event name
|
428
|
+
* @param {Mixed} ...
|
429
|
+
* @memberOf event
|
430
|
+
*/
|
431
|
+
this.trigger = function (event) {
|
432
|
+
var args = Davis.utils.toArray(arguments, 1),
|
433
|
+
handlers = callbacks[event];
|
434
|
+
|
435
|
+
if (!handlers) return this
|
436
|
+
|
437
|
+
for (var i = 0, len = handlers.length; i < len; ++i) {
|
438
|
+
handlers[i].apply(this, args)
|
439
|
+
}
|
440
|
+
|
441
|
+
return this;
|
442
|
+
};
|
443
|
+
}
|
444
|
+
/*!
|
445
|
+
* Davis - logger
|
446
|
+
* Copyright (C) 2011 Oliver Nightingale
|
447
|
+
* MIT Licensed
|
448
|
+
*/
|
449
|
+
|
450
|
+
/**
|
451
|
+
* A plugin for enhancing the standard logging available through the console object.
|
452
|
+
* Automatically included in all Davis apps.
|
453
|
+
*
|
454
|
+
* Generates log messages of varying severity in the format
|
455
|
+
*
|
456
|
+
* `[Sun Jan 23 2011 16:15:21 GMT+0000 (GMT)] <message>`
|
457
|
+
*
|
458
|
+
* @module
|
459
|
+
*/
|
460
|
+
Davis.logger = function () {
|
461
|
+
|
462
|
+
/*!
|
463
|
+
* Generating the timestamp portion of the log message
|
464
|
+
* @private
|
465
|
+
*/
|
466
|
+
function timestamp(){
|
467
|
+
return "[" + Date() + "]";
|
468
|
+
}
|
469
|
+
|
470
|
+
/*!
|
471
|
+
* Pushing the timestamp onto the front of the arguments to log
|
472
|
+
* @private
|
473
|
+
*/
|
474
|
+
function prepArgs(args) {
|
475
|
+
var a = Davis.utils.toArray(args)
|
476
|
+
a.unshift(timestamp())
|
477
|
+
return a.join(' ');
|
478
|
+
}
|
479
|
+
|
480
|
+
var logType = function (logLevel) {
|
481
|
+
return function () {
|
482
|
+
if (window.console) console[logLevel](prepArgs(arguments));
|
483
|
+
}
|
484
|
+
}
|
485
|
+
|
486
|
+
|
487
|
+
/**
|
488
|
+
* Prints an error message to the console if the console is available.
|
489
|
+
*
|
490
|
+
* @params {String} All arguments are combined and logged to the console.
|
491
|
+
* @memberOf logger
|
492
|
+
*/
|
493
|
+
var error = logType('error')
|
494
|
+
|
495
|
+
/**
|
496
|
+
* Prints an info message to the console if the console is available.
|
497
|
+
*
|
498
|
+
* @params {String} All arguments are combined and logged to the console.
|
499
|
+
* @memberOf logger
|
500
|
+
*/
|
501
|
+
var info = logType('info')
|
502
|
+
|
503
|
+
/**
|
504
|
+
* Prints a warning message to the console if the console is available.
|
505
|
+
*
|
506
|
+
* @params {String} All arguments are combined and logged to the console.
|
507
|
+
* @memberOf logger
|
508
|
+
*/
|
509
|
+
var warn = logType('warn')
|
510
|
+
|
511
|
+
/*!
|
512
|
+
* Exposes the public methods of the module
|
513
|
+
* @private
|
514
|
+
*/
|
515
|
+
this.logger = {
|
516
|
+
error: error,
|
517
|
+
info: info,
|
518
|
+
warn: warn
|
519
|
+
}
|
520
|
+
}/*!
|
521
|
+
* Davis - Route
|
522
|
+
* Copyright (C) 2011 Oliver Nightingale
|
523
|
+
* MIT Licensed
|
524
|
+
*/
|
525
|
+
|
526
|
+
Davis.Route = (function () {
|
527
|
+
|
528
|
+
var pathNameRegex = /:([\w\d]+)/g;
|
529
|
+
var pathNameReplacement = "([^\/]+)";
|
530
|
+
|
531
|
+
var splatNameRegex = /\*([\w\d]+)/g;
|
532
|
+
var splatNameReplacement = "(.*)";
|
533
|
+
|
534
|
+
var nameRegex = /[:|\*]([\w\d]+)/g
|
535
|
+
|
536
|
+
/**
|
537
|
+
* Davis.Routes are the main part of a Davis application. They consist of an HTTP method, a path
|
538
|
+
* and a callback function. When a link or a form that Davis has bound to are clicked or submitted
|
539
|
+
* a request is pushed on the history stack and a route that matches the path and method of the
|
540
|
+
* generated request is run.
|
541
|
+
*
|
542
|
+
* The path for the route can consist of placeholders for attributes, these will then be available
|
543
|
+
* on the request. Simple variables should be prefixed with a colan, and for splat style params use
|
544
|
+
* an asterisk.
|
545
|
+
*
|
546
|
+
* Inside the callback function 'this' is bound to the request.
|
547
|
+
*
|
548
|
+
* Example:
|
549
|
+
*
|
550
|
+
* var route = new Davis.Route ('get', '/foo/:id', function (req) {
|
551
|
+
* var id = req.params['id']
|
552
|
+
* // do something interesting!
|
553
|
+
* })
|
554
|
+
*
|
555
|
+
* var route = new Davis.Route ('get', '/foo/*splat', function (req) {
|
556
|
+
* var id = req.params['splat']
|
557
|
+
* // splat will contain everything after the /foo/ in the path.
|
558
|
+
* })
|
559
|
+
*
|
560
|
+
* You can include any number of route level 'middleware' when defining routes. These middlewares are
|
561
|
+
* run in order and need to explicitly call the next handler in the stack. Using route middleware allows
|
562
|
+
* you to share common logic between routes and is also a good place to load any data or do any async calls
|
563
|
+
* keeping your main handler simple and focused.
|
564
|
+
*
|
565
|
+
* Example:
|
566
|
+
*
|
567
|
+
* var loadUser = function (req, next) {
|
568
|
+
* $.get('/users/current', function (user) {
|
569
|
+
* req.user = user
|
570
|
+
* next(req)
|
571
|
+
* })
|
572
|
+
* }
|
573
|
+
*
|
574
|
+
* var route = new Davis.Route ('get', '/foo/:id', loadUser, function (req) {
|
575
|
+
* renderUser(req.user)
|
576
|
+
* })
|
577
|
+
*
|
578
|
+
* @constructor
|
579
|
+
* @param {String} method This should be one of either 'get', 'post', 'put', 'delete', 'before', 'after' or 'state'
|
580
|
+
* @param {String} path This string can contain place holders for variables, e.g. '/user/:id' or '/user/*splat'
|
581
|
+
* @param {Function} callback One or more callbacks that will be called in order when a request matching both the path and method is triggered.
|
582
|
+
*/
|
583
|
+
var Route = function (method, path, handlers) {
|
584
|
+
var convertPathToRegExp = function () {
|
585
|
+
if (!(path instanceof RegExp)) {
|
586
|
+
var str = path
|
587
|
+
.replace(pathNameRegex, pathNameReplacement)
|
588
|
+
.replace(splatNameRegex, splatNameReplacement);
|
589
|
+
|
590
|
+
// Most browsers will reset this to zero after a replace call. IE will
|
591
|
+
// set it to the index of the last matched character.
|
592
|
+
path.lastIndex = 0;
|
593
|
+
|
594
|
+
return new RegExp("^" + str + "$", "gi");
|
595
|
+
} else {
|
596
|
+
return path;
|
597
|
+
};
|
598
|
+
};
|
599
|
+
|
600
|
+
var convertMethodToRegExp = function () {
|
601
|
+
if (!(method instanceof RegExp)) {
|
602
|
+
return new RegExp("^" + method + "$", "i");
|
603
|
+
} else {
|
604
|
+
return method
|
605
|
+
};
|
606
|
+
}
|
607
|
+
|
608
|
+
var capturePathParamNames = function () {
|
609
|
+
var names = [], a;
|
610
|
+
while ((a = nameRegex.exec(path))) names.push(a[1]);
|
611
|
+
return names;
|
612
|
+
};
|
613
|
+
|
614
|
+
this.paramNames = capturePathParamNames();
|
615
|
+
this.path = convertPathToRegExp();
|
616
|
+
this.method = convertMethodToRegExp();
|
617
|
+
|
618
|
+
if (typeof handlers === 'function') {
|
619
|
+
this.handlers = [handlers]
|
620
|
+
} else {
|
621
|
+
this.handlers = handlers;
|
622
|
+
}
|
623
|
+
}
|
624
|
+
|
625
|
+
/**
|
626
|
+
* Tests whether or not a route matches a particular request.
|
627
|
+
*
|
628
|
+
* Example:
|
629
|
+
*
|
630
|
+
* route.match('get', '/foo/12')
|
631
|
+
*
|
632
|
+
* @param {String} method the method to match against
|
633
|
+
* @param {String} path the path to match against
|
634
|
+
* @returns {Boolean}
|
635
|
+
*/
|
636
|
+
Route.prototype.match = function (method, path) {
|
637
|
+
this.reset();
|
638
|
+
return (this.method.test(method)) && (this.path.test(path))
|
639
|
+
}
|
640
|
+
|
641
|
+
/**
|
642
|
+
* Resets the RegExps for method and path
|
643
|
+
*/
|
644
|
+
Route.prototype.reset = function () {
|
645
|
+
this.method.lastIndex = 0;
|
646
|
+
this.path.lastIndex = 0;
|
647
|
+
}
|
648
|
+
|
649
|
+
/**
|
650
|
+
* Runs the callback associated with a particular route against the passed request.
|
651
|
+
*
|
652
|
+
* Any named params in the request path are extracted, as per the routes path, and
|
653
|
+
* added onto the requests params object.
|
654
|
+
*
|
655
|
+
* Example:
|
656
|
+
*
|
657
|
+
* route.run(request)
|
658
|
+
*
|
659
|
+
* @params {Davis.Request} request
|
660
|
+
* @returns {Object} whatever the routes callback returns
|
661
|
+
*/
|
662
|
+
Route.prototype.run = function (request) {
|
663
|
+
this.reset();
|
664
|
+
var matches = this.path.exec(request.path);
|
665
|
+
if (matches) {
|
666
|
+
matches.shift();
|
667
|
+
for (var i=0; i < matches.length; i++) {
|
668
|
+
request.params[this.paramNames[i]] = matches[i];
|
669
|
+
};
|
670
|
+
};
|
671
|
+
|
672
|
+
var handlers = Davis.utils.map(this.handlers, function (handler, i) {
|
673
|
+
return function (req) {
|
674
|
+
return handler.call(req, req, handlers[i+1])
|
675
|
+
}
|
676
|
+
})
|
677
|
+
|
678
|
+
return handlers[0](request)
|
679
|
+
}
|
680
|
+
|
681
|
+
/**
|
682
|
+
* Converts the route to a string representation of itself by combining the method and path
|
683
|
+
* attributes.
|
684
|
+
*
|
685
|
+
* @returns {String} string representation of the route
|
686
|
+
*/
|
687
|
+
Route.prototype.toString = function () {
|
688
|
+
return [this.method, this.path].join(' ');
|
689
|
+
}
|
690
|
+
|
691
|
+
/*!
|
692
|
+
* exposing the constructor
|
693
|
+
* @private
|
694
|
+
*/
|
695
|
+
return Route;
|
696
|
+
})()
|
697
|
+
/*!
|
698
|
+
* Davis - router
|
699
|
+
* Copyright (C) 2011 Oliver Nightingale
|
700
|
+
* MIT Licensed
|
701
|
+
*/
|
702
|
+
|
703
|
+
/**
|
704
|
+
* A decorator that adds convinience methods to a Davis.App for easily creating instances
|
705
|
+
* of Davis.Route and looking up routes for a particular request.
|
706
|
+
*
|
707
|
+
* Provides get, post put and delete method shortcuts for creating instances of Davis.Routes
|
708
|
+
* with the corresponding method. This allows simple REST styled routing for a client side
|
709
|
+
* JavaScript application.
|
710
|
+
*
|
711
|
+
* ### Example
|
712
|
+
*
|
713
|
+
* app.get('/foo/:id', function (req) {
|
714
|
+
* // get the foo with id = req.params['id']
|
715
|
+
* })
|
716
|
+
*
|
717
|
+
* app.post('/foo', function (req) {
|
718
|
+
* // create a new instance of foo with req.params
|
719
|
+
* })
|
720
|
+
*
|
721
|
+
* app.put('/foo/:id', function (req) {
|
722
|
+
* // update the instance of foo with id = req.params['id']
|
723
|
+
* })
|
724
|
+
*
|
725
|
+
* app.del('/foo/:id', function (req) {
|
726
|
+
* // delete the instance of foo with id = req.params['id']
|
727
|
+
* })
|
728
|
+
*
|
729
|
+
* As well as providing convinience methods for creating instances of Davis.Routes the router
|
730
|
+
* also provides methods for creating special instances of routes called filters. Before filters
|
731
|
+
* run before any matching route is run, and after filters run after any matched route has run.
|
732
|
+
* A before filter can return false to halt the running of any matched routes or other before filters.
|
733
|
+
*
|
734
|
+
* A filter can take an optional path to match on, or without a path will match every request.
|
735
|
+
*
|
736
|
+
* ### Example
|
737
|
+
*
|
738
|
+
* app.before('/foo/:id', function (req) {
|
739
|
+
* // will only run before request matching '/foo/:id'
|
740
|
+
* })
|
741
|
+
*
|
742
|
+
* app.before(function (req) {
|
743
|
+
* // will run before all routes
|
744
|
+
* })
|
745
|
+
*
|
746
|
+
* app.after('/foo/:id', function (req) {
|
747
|
+
* // will only run after routes matching '/foo/:id'
|
748
|
+
* })
|
749
|
+
*
|
750
|
+
* app.after(function (req) {
|
751
|
+
* // will run after all routes
|
752
|
+
* })
|
753
|
+
*
|
754
|
+
* Another special kind of route, called state routes, are also generated using the router. State routes
|
755
|
+
* are for requests that will not change the current page location. Instead the page location will remain
|
756
|
+
* the same but the current state of the page has changed. This allows for states which the server will not
|
757
|
+
* be expected to know about and support.
|
758
|
+
*
|
759
|
+
* ### Example
|
760
|
+
*
|
761
|
+
* app.state('/foo/:id', function (req) {
|
762
|
+
* // will run when the app transitions into the '/foo/:id' state.
|
763
|
+
* })
|
764
|
+
*
|
765
|
+
* Using the `trans` method an app can transition to these kind of states without changing the url location.
|
766
|
+
*
|
767
|
+
* For convinience routes can be defined within a common base scope, this is useful for keeping your route
|
768
|
+
* definitions simpler and DRYer. A scope can either cover the whole app, or just a subset of the routes.
|
769
|
+
*
|
770
|
+
* ### Example
|
771
|
+
*
|
772
|
+
* app.scope('/foo', function () {
|
773
|
+
* this.get('/:id', function () {
|
774
|
+
* // will run for routes that match '/foo/:id'
|
775
|
+
* })
|
776
|
+
* })
|
777
|
+
*
|
778
|
+
* @module
|
779
|
+
*/
|
780
|
+
Davis.router = function () {
|
781
|
+
|
782
|
+
/**
|
783
|
+
* Low level method for adding routes to your application.
|
784
|
+
*
|
785
|
+
* If called with just a method will return a partially applied function that can create routes with
|
786
|
+
* that method. This is used internally to provide shortcuts for get, post, put, delete and state
|
787
|
+
* routes.
|
788
|
+
*
|
789
|
+
* You normally want to use the higher level methods such as get and post, but this can be useful for extending
|
790
|
+
* Davis to work with other kinds of requests.
|
791
|
+
*
|
792
|
+
* Example:
|
793
|
+
*
|
794
|
+
* app.route('get', '/foo', function (req) {
|
795
|
+
* // will run when a get request is made to '/foo'
|
796
|
+
* })
|
797
|
+
*
|
798
|
+
* app.patch = app.route('patch') // will return a function that can be used to handle requests with method of patch.
|
799
|
+
* app.patch('/bar', function (req) {
|
800
|
+
* // will run when a patch request is made to '/bar'
|
801
|
+
* })
|
802
|
+
*
|
803
|
+
* @param {String} method The method for this route.
|
804
|
+
* @param {String} path The path for this route.
|
805
|
+
* @param {Function} handler The handler for this route, will be called with the request that triggered the route.
|
806
|
+
* @returns {Davis.Route} the route that has just been created and added to the route list.
|
807
|
+
* @memberOf router
|
808
|
+
*/
|
809
|
+
this.route = function (method, path) {
|
810
|
+
var createRoute = function (path) {
|
811
|
+
var handlers = Davis.utils.toArray(arguments, 1),
|
812
|
+
scope = scopePaths.join(''),
|
813
|
+
fullPath, route
|
814
|
+
|
815
|
+
(typeof path == 'string') ? fullPath = scope + path : fullPath = path
|
816
|
+
|
817
|
+
route = new Davis.Route (method, fullPath, handlers)
|
818
|
+
|
819
|
+
routeCollection.push(route)
|
820
|
+
return route
|
821
|
+
}
|
822
|
+
|
823
|
+
return (arguments.length == 1) ? createRoute : createRoute.apply(this, Davis.utils.toArray(arguments, 1))
|
824
|
+
}
|
825
|
+
|
826
|
+
/**
|
827
|
+
* A convinience wrapper around `app.route` for creating get routes.
|
828
|
+
*
|
829
|
+
* @param {String} path The path for this route.
|
830
|
+
* @param {Function} handler The handler for this route, will be called with the request that triggered the route.
|
831
|
+
* @returns {Davis.Route} the route that has just been created and added to the route list.
|
832
|
+
* @see Davis.router.route
|
833
|
+
* @memberOf router
|
834
|
+
*/
|
835
|
+
this.get = this.route('get')
|
836
|
+
|
837
|
+
/**
|
838
|
+
* A convinience wrapper around `app.route` for creating post routes.
|
839
|
+
*
|
840
|
+
* @param {String} path The path for this route.
|
841
|
+
* @param {Function} handler The handler for this route, will be called with the request that triggered the route.
|
842
|
+
* @returns {Davis.Route} the route that has just been created and added to the route list.
|
843
|
+
* @see Davis.router.route
|
844
|
+
* @memberOf router
|
845
|
+
*/
|
846
|
+
this.post = this.route('post')
|
847
|
+
|
848
|
+
/**
|
849
|
+
* A convinience wrapper around `app.route` for creating put routes.
|
850
|
+
*
|
851
|
+
* @param {String} path The path for this route.
|
852
|
+
* @param {Function} handler The handler for this route, will be called with the request that triggered the route.
|
853
|
+
* @returns {Davis.Route} the route that has just been created and added to the route list.
|
854
|
+
* @see Davis.router.route
|
855
|
+
* @memberOf router
|
856
|
+
*/
|
857
|
+
this.put = this.route('put')
|
858
|
+
|
859
|
+
/**
|
860
|
+
* A convinience wrapper around `app.route` for creating delete routes.
|
861
|
+
*
|
862
|
+
* delete is a reserved word in javascript so use the `del` method when creating a Davis.Route with a method of delete.
|
863
|
+
*
|
864
|
+
* @param {String} path The path for this route.
|
865
|
+
* @param {Function} handler The handler for this route, will be called with the request that triggered the route.
|
866
|
+
* @returns {Davis.Route} the route that has just been created and added to the route list.
|
867
|
+
* @see Davis.router.route
|
868
|
+
* @memberOf router
|
869
|
+
*/
|
870
|
+
this.del = this.route('delete')
|
871
|
+
|
872
|
+
/**
|
873
|
+
* Adds a state route into the apps route collection.
|
874
|
+
*
|
875
|
+
* These special kind of routes are not triggered by clicking links or submitting forms, instead they
|
876
|
+
* are triggered manually by calling `trans`.
|
877
|
+
*
|
878
|
+
* Routes added using the state method act in the same way as other routes except that they generate
|
879
|
+
* a route that is listening for requests that will not change the page location.
|
880
|
+
*
|
881
|
+
* Example:
|
882
|
+
*
|
883
|
+
* app.state('/foo/:id', function (req) {
|
884
|
+
* // will run when the app transitions into the '/foo/:id' state.
|
885
|
+
* })
|
886
|
+
*
|
887
|
+
* @param {String} path The path for this route, this will never be seen in the url bar.
|
888
|
+
* @param {Function} handler The handler for this route, will be called with the request that triggered the route
|
889
|
+
* @memberOf router
|
890
|
+
*
|
891
|
+
*/
|
892
|
+
this.state = this.route('state');
|
893
|
+
|
894
|
+
/**
|
895
|
+
* Modifies the scope of the router.
|
896
|
+
*
|
897
|
+
* If you have many routes that share a common path prefix you can use scope to reduce repeating
|
898
|
+
* that path prefix.
|
899
|
+
*
|
900
|
+
* You can use `scope` in two ways, firstly you can set the scope for the whole app by calling scope
|
901
|
+
* before defining routes. You can also provide a function to the scope method, and the scope will
|
902
|
+
* only apply to those routes defined within this function. It is also possible to nest scopes within
|
903
|
+
* other scopes.
|
904
|
+
*
|
905
|
+
* Example
|
906
|
+
*
|
907
|
+
* // using scope with a function
|
908
|
+
* app.scope('/foo', function () {
|
909
|
+
* this.get('/bar', function (req) {
|
910
|
+
* // this route will have a path of '/foo/bar'
|
911
|
+
* })
|
912
|
+
* })
|
913
|
+
*
|
914
|
+
* // setting a global scope for the rest of the application
|
915
|
+
* app.scope('/bar')
|
916
|
+
*
|
917
|
+
* // using scope with a function
|
918
|
+
* app.scope('/foo', function () {
|
919
|
+
* this.scope('/bar', function () {
|
920
|
+
* this.get('/baz', function (req) {
|
921
|
+
* // this route will have a path of '/foo/bar/baz'
|
922
|
+
* })
|
923
|
+
* })
|
924
|
+
* })
|
925
|
+
*
|
926
|
+
* @memberOf router
|
927
|
+
* @param {String} path The prefix to use as the scope
|
928
|
+
* @param {Function} fn A function that will be executed with the router as its context and the path
|
929
|
+
* as a prefix
|
930
|
+
*
|
931
|
+
*/
|
932
|
+
this.scope = function (path, fn) {
|
933
|
+
scopePaths.push(path)
|
934
|
+
if (arguments.length == 1) return
|
935
|
+
|
936
|
+
fn.call(this, this)
|
937
|
+
scopePaths.pop()
|
938
|
+
}
|
939
|
+
|
940
|
+
/**
|
941
|
+
* Transitions the app into the state identified by the passed path parameter.
|
942
|
+
*
|
943
|
+
* This allows the app to enter states without changing the page path through a link click or form submit.
|
944
|
+
* If there are handlers registered for this state, added by the `state` method, they will be triggered.
|
945
|
+
*
|
946
|
+
* This method generates a request with a method of 'state', in all other ways this request is identical
|
947
|
+
* to those that are generated when clicking links etc.
|
948
|
+
*
|
949
|
+
* States transitioned to using this method will not be able to be revisited directly with a page load as
|
950
|
+
* there is no url that represents the state.
|
951
|
+
*
|
952
|
+
* An optional second parameter can be passed which will be available to any handlers in the requests
|
953
|
+
* params object.
|
954
|
+
*
|
955
|
+
* Example
|
956
|
+
*
|
957
|
+
* app.trans('/foo/1')
|
958
|
+
*
|
959
|
+
* app.trans('/foo/1', {
|
960
|
+
* "bar": "baz"
|
961
|
+
* })
|
962
|
+
*
|
963
|
+
*
|
964
|
+
* @param {String} path The path that represents this state. This will not be seen in the url bar.
|
965
|
+
* @param {Object} data Any additional data that should be sent with the request as params.
|
966
|
+
* @memberOf router
|
967
|
+
*/
|
968
|
+
this.trans = function (path, data) {
|
969
|
+
if (data) {
|
970
|
+
var fullPath = [path, decodeURIComponent(Davis.$.param(data))].join('?')
|
971
|
+
} else {
|
972
|
+
var fullPath = path
|
973
|
+
};
|
974
|
+
|
975
|
+
var req = new Davis.Request({
|
976
|
+
method: 'state',
|
977
|
+
fullPath: fullPath,
|
978
|
+
title: ''
|
979
|
+
})
|
980
|
+
|
981
|
+
Davis.location.assign(req)
|
982
|
+
}
|
983
|
+
|
984
|
+
/*!
|
985
|
+
* Generating convinience methods for creating filters using Davis.Routes and methods to
|
986
|
+
* lookup filters.
|
987
|
+
*/
|
988
|
+
this.filter = function (filterName) {
|
989
|
+
return function () {
|
990
|
+
var method = /.+/;
|
991
|
+
|
992
|
+
if (arguments.length == 1) {
|
993
|
+
var path = /.+/;
|
994
|
+
var handler = arguments[0];
|
995
|
+
} else if (arguments.length == 2) {
|
996
|
+
var path = scopePaths.join('') + arguments[0];
|
997
|
+
var handler = arguments[1];
|
998
|
+
};
|
999
|
+
|
1000
|
+
var route = new Davis.Route (method, path, handler)
|
1001
|
+
filterCollection[filterName].push(route);
|
1002
|
+
return route
|
1003
|
+
}
|
1004
|
+
}
|
1005
|
+
|
1006
|
+
this.lookupFilter = function (filterType) {
|
1007
|
+
return function (method, path) {
|
1008
|
+
return Davis.utils.filter(filterCollection[filterType], function (route) {
|
1009
|
+
return route.match(method, path)
|
1010
|
+
});
|
1011
|
+
}
|
1012
|
+
}
|
1013
|
+
|
1014
|
+
/**
|
1015
|
+
* A convinience wrapper around `app.filter` for creating before filters.
|
1016
|
+
*
|
1017
|
+
* @param {String} path The optionl path for this filter.
|
1018
|
+
* @param {Function} handler The handler for this filter, will be called with the request that triggered the route.
|
1019
|
+
* @returns {Davis.Route} the route that has just been created and added to the route list.
|
1020
|
+
* @memberOf router
|
1021
|
+
*/
|
1022
|
+
this.before = this.filter('before')
|
1023
|
+
|
1024
|
+
/**
|
1025
|
+
* A convinience wrapper around `app.filter` for creating after filters.
|
1026
|
+
*
|
1027
|
+
* @param {String} path The optionl path for this filter.
|
1028
|
+
* @param {Function} handler The handler for this filter, will be called with the request that triggered the route.
|
1029
|
+
* @returns {Davis.Route} the route that has just been created and added to the route list.
|
1030
|
+
* @memberOf router
|
1031
|
+
*/
|
1032
|
+
this.after = this.filter('after')
|
1033
|
+
|
1034
|
+
/**
|
1035
|
+
* A convinience wrapper around `app.lookupFilter` for looking up before filters.
|
1036
|
+
*
|
1037
|
+
* @param {String} path The optionl path for this filter.
|
1038
|
+
* @param {Function} handler The handler for this filter, will be called with the request that triggered the route.
|
1039
|
+
* @returns {Davis.Route} the route that has just been created and added to the route list.
|
1040
|
+
* @memberOf router
|
1041
|
+
*/
|
1042
|
+
this.lookupBeforeFilter = this.lookupFilter('before')
|
1043
|
+
|
1044
|
+
/**
|
1045
|
+
* A convinience wrapper around `app.lookupFilter` for looking up after filters.
|
1046
|
+
*
|
1047
|
+
* @param {String} path The optionl path for this filter.
|
1048
|
+
* @param {Function} handler The handler for this filter, will be called with the request that triggered the route.
|
1049
|
+
* @returns {Davis.Route} the route that has just been created and added to the route list.
|
1050
|
+
* @memberOf router
|
1051
|
+
*/
|
1052
|
+
this.lookupAfterFilter = this.lookupFilter('after')
|
1053
|
+
|
1054
|
+
/*!
|
1055
|
+
* collections of routes and filters
|
1056
|
+
* @private
|
1057
|
+
*/
|
1058
|
+
var routeCollection = [];
|
1059
|
+
var filterCollection = {
|
1060
|
+
before: [],
|
1061
|
+
after: []
|
1062
|
+
};
|
1063
|
+
var scopePaths = []
|
1064
|
+
|
1065
|
+
/**
|
1066
|
+
* Looks for the first route that matches the method and path from a request.
|
1067
|
+
* Will only find and return the first matched route.
|
1068
|
+
*
|
1069
|
+
* @param {String} method the method to use when looking up a route
|
1070
|
+
* @param {String} path the path to use when looking up a route
|
1071
|
+
* @returns {Davis.Route} route
|
1072
|
+
* @memberOf router
|
1073
|
+
*/
|
1074
|
+
this.lookupRoute = function (method, path) {
|
1075
|
+
return Davis.utils.filter(routeCollection, function (route) {
|
1076
|
+
return route.match(method, path)
|
1077
|
+
})[0];
|
1078
|
+
};
|
1079
|
+
}
|
1080
|
+
/*!
|
1081
|
+
* Davis - history
|
1082
|
+
* Copyright (C) 2011 Oliver Nightingale
|
1083
|
+
* MIT Licensed
|
1084
|
+
*/
|
1085
|
+
|
1086
|
+
/**
|
1087
|
+
* A module to normalize and enhance the window.pushState method and window.onpopstate event.
|
1088
|
+
*
|
1089
|
+
* Adds the ability to bind to whenever a new state is pushed onto the history stack and normalizes
|
1090
|
+
* both of these events into an onChange event.
|
1091
|
+
*
|
1092
|
+
* @module
|
1093
|
+
*/
|
1094
|
+
Davis.history = (function () {
|
1095
|
+
|
1096
|
+
/*!
|
1097
|
+
* storage for the push state handlers
|
1098
|
+
* @private
|
1099
|
+
*/
|
1100
|
+
var pushStateHandlers = [];
|
1101
|
+
|
1102
|
+
/*!
|
1103
|
+
* keep track of whether or not webkit like browsers have fired their initial
|
1104
|
+
* page load popstate
|
1105
|
+
* @private
|
1106
|
+
*/
|
1107
|
+
var popped = false
|
1108
|
+
|
1109
|
+
/*!
|
1110
|
+
* Add a handler to the push state event. This event is not a native event but is fired
|
1111
|
+
* every time a call to pushState is called.
|
1112
|
+
*
|
1113
|
+
* @param {Function} handler
|
1114
|
+
* @private
|
1115
|
+
*/
|
1116
|
+
function onPushState(handler) {
|
1117
|
+
pushStateHandlers.push(handler);
|
1118
|
+
};
|
1119
|
+
|
1120
|
+
/*!
|
1121
|
+
* Simple wrapper for the native onpopstate event.
|
1122
|
+
*
|
1123
|
+
* @param {Function} handler
|
1124
|
+
* @private
|
1125
|
+
*/
|
1126
|
+
function onPopState(handler) {
|
1127
|
+
window.addEventListener('popstate', handler, true);
|
1128
|
+
};
|
1129
|
+
|
1130
|
+
/*!
|
1131
|
+
* returns a handler that wraps the native event given onpopstate.
|
1132
|
+
* When the page first loads or going back to a time in the history that was not added
|
1133
|
+
* by pushState the event.state object will be null. This generates a request for the current
|
1134
|
+
* location in those cases
|
1135
|
+
*
|
1136
|
+
* @param {Function} handler
|
1137
|
+
* @private
|
1138
|
+
*/
|
1139
|
+
function wrapped(handler) {
|
1140
|
+
return function (event) {
|
1141
|
+
if (event.state && event.state._davis) {
|
1142
|
+
handler(new Davis.Request(event.state._davis))
|
1143
|
+
} else {
|
1144
|
+
if (popped) handler(Davis.Request.forPageLoad())
|
1145
|
+
};
|
1146
|
+
popped = true
|
1147
|
+
}
|
1148
|
+
}
|
1149
|
+
|
1150
|
+
/*!
|
1151
|
+
* provide a wrapper for any data that is going to be pushed into the history stack. All
|
1152
|
+
* data is wrapped in a "_davis" namespace.
|
1153
|
+
* @private
|
1154
|
+
*/
|
1155
|
+
function wrapStateData(data) {
|
1156
|
+
return {"_davis": data}
|
1157
|
+
}
|
1158
|
+
|
1159
|
+
/**
|
1160
|
+
* Bind to the history on change event.
|
1161
|
+
*
|
1162
|
+
* This is not a native event but is fired any time a new state is pushed onto the history stack,
|
1163
|
+
* the current history is replaced or a state is popped off the history stack.
|
1164
|
+
* The handler function will be called with a request param which is an instance of Davis.Request.
|
1165
|
+
*
|
1166
|
+
* @param {Function} handler a function that will be called on push and pop state.
|
1167
|
+
* @see Davis.Request
|
1168
|
+
* @memberOf history
|
1169
|
+
*/
|
1170
|
+
function onChange(handler) {
|
1171
|
+
onPushState(handler);
|
1172
|
+
onPopState(wrapped(handler));
|
1173
|
+
};
|
1174
|
+
|
1175
|
+
/*!
|
1176
|
+
* returns a function for manipulating the history state and optionally calling any associated
|
1177
|
+
* pushStateHandlers
|
1178
|
+
*
|
1179
|
+
* @param {String} methodName the name of the method to manipulate the history state with.
|
1180
|
+
* @private
|
1181
|
+
*/
|
1182
|
+
function changeStateWith (methodName) {
|
1183
|
+
return function (request, opts) {
|
1184
|
+
popped = true
|
1185
|
+
history[methodName](wrapStateData(request.toJSON()), request.title, request.location());
|
1186
|
+
if (opts && opts.silent) return
|
1187
|
+
Davis.utils.forEach(pushStateHandlers, function (handler) {
|
1188
|
+
handler(request);
|
1189
|
+
});
|
1190
|
+
}
|
1191
|
+
}
|
1192
|
+
|
1193
|
+
/**
|
1194
|
+
* Pushes a request onto the history stack.
|
1195
|
+
*
|
1196
|
+
* This is used internally by Davis to push a new request
|
1197
|
+
* resulting from either a form submit or a link click onto the history stack, it will also trigger
|
1198
|
+
* the onpushstate event.
|
1199
|
+
*
|
1200
|
+
* An instance of Davis.Request is expected to be passed, however any object that has a title
|
1201
|
+
* and a path property will also be accepted.
|
1202
|
+
*
|
1203
|
+
* @param {Davis.Request} request the location to be assinged as the current location.
|
1204
|
+
* @memberOf history
|
1205
|
+
*/
|
1206
|
+
var assign = changeStateWith('pushState')
|
1207
|
+
|
1208
|
+
/**
|
1209
|
+
* Replace the current state on the history stack.
|
1210
|
+
*
|
1211
|
+
* This is used internally by Davis when performing a redirect. This will trigger an onpushstate event.
|
1212
|
+
*
|
1213
|
+
* An instance of Davis.Request is expected to be passed, however any object that has a title
|
1214
|
+
* and a path property will also be accepted.
|
1215
|
+
*
|
1216
|
+
* @param {Davis.Request} request the location to replace the current location with.
|
1217
|
+
* @memberOf history
|
1218
|
+
*/
|
1219
|
+
var replace = changeStateWith('replaceState')
|
1220
|
+
|
1221
|
+
/**
|
1222
|
+
* Returns the current location for the application.
|
1223
|
+
*
|
1224
|
+
* Davis.location delegates to this method for getting the apps current location.
|
1225
|
+
*
|
1226
|
+
* @memberOf history
|
1227
|
+
*/
|
1228
|
+
function current() {
|
1229
|
+
return window.location.pathname + (window.location.search ? window.location.search : '')
|
1230
|
+
}
|
1231
|
+
|
1232
|
+
/*!
|
1233
|
+
* Exposing the public methods of this module
|
1234
|
+
* @private
|
1235
|
+
*/
|
1236
|
+
return {
|
1237
|
+
onChange: onChange,
|
1238
|
+
current: current,
|
1239
|
+
assign: assign,
|
1240
|
+
replace: replace
|
1241
|
+
}
|
1242
|
+
})()
|
1243
|
+
/*!
|
1244
|
+
* Davis - location
|
1245
|
+
* Copyright (C) 2011 Oliver Nightingale
|
1246
|
+
* MIT Licensed
|
1247
|
+
*/
|
1248
|
+
|
1249
|
+
/**
|
1250
|
+
* A module that acts as a delegator to any locationDelegate implementation. This abstracts the details of
|
1251
|
+
* what is being used for the apps routing away from the rest of the library. This allows any kind of routing
|
1252
|
+
* To be used with Davis as long as it can respond appropriatly to the given delegate methods.
|
1253
|
+
*
|
1254
|
+
* A routing module must respond to the following methods
|
1255
|
+
*
|
1256
|
+
* * __current__ : Should return the current location for the app
|
1257
|
+
* * __assign__ : Should set the current location of the app based on the location of the passed request.
|
1258
|
+
* * __replace__ : Should at least change the current location to the location of the passed request, for full compatibility it should not add any extra items in the history stack.
|
1259
|
+
* * __onChange__ : Should add calbacks that will be fired whenever the location is changed.
|
1260
|
+
*
|
1261
|
+
* @module
|
1262
|
+
*
|
1263
|
+
*/
|
1264
|
+
Davis.location = (function () {
|
1265
|
+
|
1266
|
+
/*!
|
1267
|
+
* By default the Davis uses the Davis.history module for its routing, this gives HTML5 based pushState routing
|
1268
|
+
* which is preferrable over location.hash based routing.
|
1269
|
+
*/
|
1270
|
+
var locationDelegate = Davis.history
|
1271
|
+
|
1272
|
+
/**
|
1273
|
+
* Sets the current location delegate.
|
1274
|
+
*
|
1275
|
+
* The passed delegate will be used for all Davis apps. The delegate
|
1276
|
+
* must respond to the following four methods `current`, `assign`, `replace` & `onChange`.
|
1277
|
+
*
|
1278
|
+
* @param {Object} the location delegate to use.
|
1279
|
+
* @memberOf location
|
1280
|
+
*/
|
1281
|
+
function setLocationDelegate(delegate) {
|
1282
|
+
locationDelegate = delegate
|
1283
|
+
}
|
1284
|
+
|
1285
|
+
/**
|
1286
|
+
* Delegates to the locationDelegate.current method.
|
1287
|
+
*
|
1288
|
+
* This should return the current location of the app.
|
1289
|
+
*
|
1290
|
+
* @memberOf location
|
1291
|
+
*/
|
1292
|
+
function current() {
|
1293
|
+
return locationDelegate.current()
|
1294
|
+
}
|
1295
|
+
|
1296
|
+
/*!
|
1297
|
+
* Creates a function which sends the location delegate the passed message name.
|
1298
|
+
* It handles converting a string path to an actual request
|
1299
|
+
*
|
1300
|
+
* @returns {Function} a function that calls the location delegate with the supplied method name
|
1301
|
+
* @memberOf location
|
1302
|
+
* @private
|
1303
|
+
*/
|
1304
|
+
function sendLocationDelegate (methodName) {
|
1305
|
+
return function (req) {
|
1306
|
+
if (typeof req == 'string') req = new Davis.Request (req)
|
1307
|
+
locationDelegate[methodName](req)
|
1308
|
+
}
|
1309
|
+
}
|
1310
|
+
|
1311
|
+
/**
|
1312
|
+
* Delegates to the locationDelegate.assign method.
|
1313
|
+
*
|
1314
|
+
* This should set the current location for the app to that of the passed request object.
|
1315
|
+
*
|
1316
|
+
* Can take either a Davis.Request or a string representing the path of the request to assign.
|
1317
|
+
*
|
1318
|
+
*
|
1319
|
+
*
|
1320
|
+
* @param {Request} req the request to replace the current location with, either a string or a Davis.Request.
|
1321
|
+
* @see Davis.Request
|
1322
|
+
* @memberOf location
|
1323
|
+
*/
|
1324
|
+
var assign = sendLocationDelegate('assign')
|
1325
|
+
|
1326
|
+
/**
|
1327
|
+
* Delegates to the locationDelegate.replace method.
|
1328
|
+
*
|
1329
|
+
* This should replace the current location with that of the passed request.
|
1330
|
+
* Ideally it should not create a new entry in the browsers history.
|
1331
|
+
*
|
1332
|
+
* Can take either a Davis.Request or a string representing the path of the request to assign.
|
1333
|
+
*
|
1334
|
+
* @param {Request} req the request to replace the current location with, either a string or a Davis.Request.
|
1335
|
+
* @see Davis.Request
|
1336
|
+
* @memberOf location
|
1337
|
+
*/
|
1338
|
+
var replace = sendLocationDelegate('replace')
|
1339
|
+
|
1340
|
+
/**
|
1341
|
+
* Delegates to the locationDelegate.onChange method.
|
1342
|
+
*
|
1343
|
+
* This should add a callback that will be called any time the location changes.
|
1344
|
+
* The handler function will be called with a request param which is an instance of Davis.Request.
|
1345
|
+
*
|
1346
|
+
* @param {Function} handler callback function to be called on location chnage.
|
1347
|
+
* @see Davis.Request
|
1348
|
+
* @memberOf location
|
1349
|
+
*
|
1350
|
+
*/
|
1351
|
+
function onChange(handler) {
|
1352
|
+
locationDelegate.onChange(handler)
|
1353
|
+
}
|
1354
|
+
|
1355
|
+
/*!
|
1356
|
+
* Exposing the public methods of this module
|
1357
|
+
* @private
|
1358
|
+
*/
|
1359
|
+
return {
|
1360
|
+
setLocationDelegate: setLocationDelegate,
|
1361
|
+
current: current,
|
1362
|
+
assign: assign,
|
1363
|
+
replace: replace,
|
1364
|
+
onChange: onChange
|
1365
|
+
}
|
1366
|
+
})()/*!
|
1367
|
+
* Davis - Request
|
1368
|
+
* Copyright (C) 2011 Oliver Nightingale
|
1369
|
+
* MIT Licensed
|
1370
|
+
*/
|
1371
|
+
|
1372
|
+
Davis.Request = (function () {
|
1373
|
+
|
1374
|
+
/**
|
1375
|
+
* Davis.Requests are created from click and submit events. Davis.Requests are passed to Davis.Routes
|
1376
|
+
* and are stored in the history stack. They are instantiated by the Davis.listener module.
|
1377
|
+
*
|
1378
|
+
* A request will have a params object which will contain all query params and form params, any named
|
1379
|
+
* params in a routes path will also be added to the requests params object. Also included is support
|
1380
|
+
* for rails style nested form params.
|
1381
|
+
*
|
1382
|
+
* By default the request method will be taken from the method attribute for forms or will be defaulted
|
1383
|
+
* to 'get' for links, however there is support for using a hidden field called _method in your forms
|
1384
|
+
* to set the correct reqeust method.
|
1385
|
+
*
|
1386
|
+
* Simple get requests can be created by just passing a path when initializing a request, to set the method
|
1387
|
+
* or title you have to pass in an object.
|
1388
|
+
*
|
1389
|
+
* Each request will have a timestamp property to make it easier to determine if the application is moving
|
1390
|
+
* forward or backward through the history stack.
|
1391
|
+
*
|
1392
|
+
* Example
|
1393
|
+
*
|
1394
|
+
* var request = new Davis.Request ("/foo/12")
|
1395
|
+
*
|
1396
|
+
* var request = new Davis.Request ("/foo/12", {title: 'foo', method: 'POST'})
|
1397
|
+
*
|
1398
|
+
* var request = new Davis.Request({
|
1399
|
+
* title: "foo",
|
1400
|
+
* fullPath: "/foo/12",
|
1401
|
+
* method: "get"
|
1402
|
+
* })
|
1403
|
+
*
|
1404
|
+
* @constructor
|
1405
|
+
* @param {String} fullPath
|
1406
|
+
* @param {Object} opts An optional object with a title or method proprty
|
1407
|
+
*
|
1408
|
+
*/
|
1409
|
+
var Request = function (fullPath, opts) {
|
1410
|
+
if (typeof fullPath == 'object') {
|
1411
|
+
opts = fullPath
|
1412
|
+
fullPath = opts.fullPath
|
1413
|
+
delete opts.fullPath
|
1414
|
+
}
|
1415
|
+
|
1416
|
+
var raw = Davis.$.extend({}, {
|
1417
|
+
title: "",
|
1418
|
+
fullPath: fullPath,
|
1419
|
+
method: "get",
|
1420
|
+
timestamp: +new Date ()
|
1421
|
+
}, opts)
|
1422
|
+
|
1423
|
+
var self = this;
|
1424
|
+
this.raw = raw;
|
1425
|
+
this.params = {};
|
1426
|
+
this.title = raw.title;
|
1427
|
+
this.queryString = raw.fullPath.split("?")[1];
|
1428
|
+
this.timestamp = raw.timestamp;
|
1429
|
+
this._staleCallback = function () {};
|
1430
|
+
|
1431
|
+
if (this.queryString) {
|
1432
|
+
Davis.utils.forEach(this.queryString.split("&"), function (keyval) {
|
1433
|
+
var paramName = keyval.split("=")[0],
|
1434
|
+
paramValue = keyval.split("=")[1],
|
1435
|
+
nestedParamRegex = /^(\w+)\[(\w+)?\](\[\])?/,
|
1436
|
+
nested;
|
1437
|
+
if (nested = nestedParamRegex.exec(paramName)) {
|
1438
|
+
var paramParent = nested[1];
|
1439
|
+
var paramName = nested[2];
|
1440
|
+
var isArray = !!nested[3];
|
1441
|
+
var parentParams = self.params[paramParent] || {};
|
1442
|
+
|
1443
|
+
if (isArray) {
|
1444
|
+
parentParams[paramName] = parentParams[paramName] || [];
|
1445
|
+
parentParams[paramName].push(decodeURIComponent(paramValue));
|
1446
|
+
self.params[paramParent] = parentParams;
|
1447
|
+
} else if (!paramName && !isArray) {
|
1448
|
+
parentParams = self.params[paramParent] || []
|
1449
|
+
parentParams.push(decodeURIComponent(paramValue))
|
1450
|
+
self.params[paramParent] = parentParams
|
1451
|
+
} else {
|
1452
|
+
parentParams[paramName] = decodeURIComponent(paramValue);
|
1453
|
+
self.params[paramParent] = parentParams;
|
1454
|
+
}
|
1455
|
+
} else {
|
1456
|
+
self.params[paramName] = decodeURIComponent(paramValue);
|
1457
|
+
};
|
1458
|
+
|
1459
|
+
});
|
1460
|
+
};
|
1461
|
+
|
1462
|
+
raw.fullPath = raw.fullPath.replace(/^https?:\/\/.+?\//, '/');
|
1463
|
+
|
1464
|
+
this.method = (this.params._method || raw.method).toLowerCase();
|
1465
|
+
|
1466
|
+
this.path = raw.fullPath
|
1467
|
+
.replace(/\?.+$/, "") // Remove the query string
|
1468
|
+
.replace(/^https?:\/\/[^\/]+/, ""); // Remove the protocol and host parts
|
1469
|
+
|
1470
|
+
this.fullPath = raw.fullPath;
|
1471
|
+
|
1472
|
+
this.delegateToServer = raw.delegateToServer || Davis.noop;
|
1473
|
+
this.isForPageLoad = raw.forPageLoad || false;
|
1474
|
+
|
1475
|
+
if (Request.prev) Request.prev.makeStale(this);
|
1476
|
+
Request.prev = this;
|
1477
|
+
|
1478
|
+
};
|
1479
|
+
|
1480
|
+
/**
|
1481
|
+
* Redirects the current request to a new location.
|
1482
|
+
*
|
1483
|
+
* Calling redirect on an instance of Davis.Request will create a new request using the path and
|
1484
|
+
* title of the current request. Redirected requests always have a method of 'get'.
|
1485
|
+
*
|
1486
|
+
* The request created will replace the current request in the history stack. Redirect is most
|
1487
|
+
* often useful inside a handler for a form submit. After succesfully handling the form the app
|
1488
|
+
* can redirect to another path. This means that the current form will not be re-submitted if
|
1489
|
+
* navigating through the history with the back or forward buttons because the request that the
|
1490
|
+
* submit generated has been replaced in the history stack.
|
1491
|
+
*
|
1492
|
+
* Example
|
1493
|
+
*
|
1494
|
+
* this.post('/foo', function (req) {
|
1495
|
+
* processFormRequest(req.params) // do something with the form request
|
1496
|
+
* req.redirect('/bar');
|
1497
|
+
* })
|
1498
|
+
*
|
1499
|
+
* @param {String} path The path to redirect the current request to
|
1500
|
+
* @memberOf Request
|
1501
|
+
*/
|
1502
|
+
Request.prototype.redirect = function (path) {
|
1503
|
+
Davis.location.replace(new Request ({
|
1504
|
+
method: 'get',
|
1505
|
+
fullPath: path,
|
1506
|
+
title: this.title
|
1507
|
+
}));
|
1508
|
+
};
|
1509
|
+
|
1510
|
+
/**
|
1511
|
+
* Adds a callback to be called when the request is stale.
|
1512
|
+
* A request becomes stale when it is no longer the current request, this normally occurs when a
|
1513
|
+
* new request is triggered. A request can be marked as stale manually if required. The callback
|
1514
|
+
* passed to whenStale will be called with the new request that is making the current request stale.
|
1515
|
+
*
|
1516
|
+
* Use the whenStale callback to 'teardown' the objects required for the current route, this gives
|
1517
|
+
* a chance for views to hide themselves and unbind any event handlers etc.
|
1518
|
+
*
|
1519
|
+
* Example
|
1520
|
+
*
|
1521
|
+
* this.get('/foo', function (req) {
|
1522
|
+
* var fooView = new FooView ()
|
1523
|
+
* fooView.render() // display the foo view
|
1524
|
+
* req.whenStale(function (nextReq) {
|
1525
|
+
* fooView.remove() // stop displaying foo view and unbind any events
|
1526
|
+
* })
|
1527
|
+
* })
|
1528
|
+
*
|
1529
|
+
* @param {Function} callback A single callback that will be called when the request becomes stale.
|
1530
|
+
* @memberOf Request
|
1531
|
+
*
|
1532
|
+
*/
|
1533
|
+
Request.prototype.whenStale = function (callback) {
|
1534
|
+
this._staleCallback = callback;
|
1535
|
+
}
|
1536
|
+
|
1537
|
+
/**
|
1538
|
+
* Mark the request as stale.
|
1539
|
+
*
|
1540
|
+
* This will cause the whenStale callback to be called.
|
1541
|
+
*
|
1542
|
+
* @param {Davis.Request} req The next request that has been recieved.
|
1543
|
+
* @memberOf Request
|
1544
|
+
*/
|
1545
|
+
Request.prototype.makeStale = function (req) {
|
1546
|
+
this._staleCallback.call(req, req);
|
1547
|
+
}
|
1548
|
+
|
1549
|
+
/**
|
1550
|
+
* Returns the location or path that should be pushed onto the history stack.
|
1551
|
+
*
|
1552
|
+
* For get requests this will be the same as the path, for post, put, delete and state requests this will
|
1553
|
+
* be blank as no location should be pushed onto the history stack.
|
1554
|
+
*
|
1555
|
+
* @returns {String} string The location that the url bar should display and should be pushed onto the history stack for this request.
|
1556
|
+
* @memberOf Request
|
1557
|
+
*/
|
1558
|
+
Request.prototype.location = function () {
|
1559
|
+
return (this.method === 'get') ? this.fullPath : ''
|
1560
|
+
}
|
1561
|
+
|
1562
|
+
/**
|
1563
|
+
* Converts the request to a string representation of itself by combining the method and fullPath
|
1564
|
+
* attributes.
|
1565
|
+
*
|
1566
|
+
* @returns {String} string representation of the request
|
1567
|
+
* @memberOf Request
|
1568
|
+
*/
|
1569
|
+
Request.prototype.toString = function () {
|
1570
|
+
return [this.method.toUpperCase(), this.path].join(" ")
|
1571
|
+
};
|
1572
|
+
|
1573
|
+
/**
|
1574
|
+
* Converts the request to a plain object which can be converted to a JSON string.
|
1575
|
+
*
|
1576
|
+
* Used when pushing a request onto the history stack.
|
1577
|
+
*
|
1578
|
+
* @returns {Object} a plain object representation of the request.
|
1579
|
+
* @memberOf Request
|
1580
|
+
*/
|
1581
|
+
Request.prototype.toJSON = function () {
|
1582
|
+
return {
|
1583
|
+
title: this.raw.title,
|
1584
|
+
fullPath: this.raw.fullPath,
|
1585
|
+
method: this.raw.method,
|
1586
|
+
timestamp: this.raw.timestamp
|
1587
|
+
}
|
1588
|
+
}
|
1589
|
+
|
1590
|
+
/**
|
1591
|
+
* Creates a new request for the page on page load.
|
1592
|
+
*
|
1593
|
+
* This is required because usually requests are generated from clicking links or submitting forms
|
1594
|
+
* however this doesn't happen on a page load but should still be considered a request that the
|
1595
|
+
* JavaScript app should handle.
|
1596
|
+
*
|
1597
|
+
* @returns {Davis.Request} A request representing the current page loading.
|
1598
|
+
* @memberOf Request
|
1599
|
+
*/
|
1600
|
+
Request.forPageLoad = function () {
|
1601
|
+
return new this ({
|
1602
|
+
method: 'get',
|
1603
|
+
// fullPath: window.location.pathname,
|
1604
|
+
fullPath: Davis.location.current(),
|
1605
|
+
title: document.title,
|
1606
|
+
forPageLoad: true
|
1607
|
+
});
|
1608
|
+
}
|
1609
|
+
|
1610
|
+
/*!
|
1611
|
+
* Stores the last request
|
1612
|
+
* @private
|
1613
|
+
*/
|
1614
|
+
Request.prev = null
|
1615
|
+
|
1616
|
+
return Request
|
1617
|
+
|
1618
|
+
})()
|
1619
|
+
/*!
|
1620
|
+
* Davis - App
|
1621
|
+
* Copyright (C) 2011 Oliver Nightingale
|
1622
|
+
* MIT Licensed
|
1623
|
+
*/
|
1624
|
+
|
1625
|
+
Davis.App = (function () {
|
1626
|
+
|
1627
|
+
/**
|
1628
|
+
* Constructor for Davis.App
|
1629
|
+
*
|
1630
|
+
* @constructor
|
1631
|
+
* @returns {Davis.App}
|
1632
|
+
*/
|
1633
|
+
function App() {
|
1634
|
+
this.running = false;
|
1635
|
+
this.boundToInternalEvents = false;
|
1636
|
+
|
1637
|
+
this.use(Davis.listener)
|
1638
|
+
this.use(Davis.event)
|
1639
|
+
this.use(Davis.router)
|
1640
|
+
this.use(Davis.logger)
|
1641
|
+
};
|
1642
|
+
|
1643
|
+
/**
|
1644
|
+
* A convinience function for changing the apps default settings.
|
1645
|
+
*
|
1646
|
+
* Should be used before starting the app to ensure any new settings
|
1647
|
+
* are picked up and used.
|
1648
|
+
*
|
1649
|
+
* Example:
|
1650
|
+
*
|
1651
|
+
* app.configure(function (config) {
|
1652
|
+
* config.linkSelector = 'a.davis'
|
1653
|
+
* config.formSelector = 'form.davis'
|
1654
|
+
* })
|
1655
|
+
*
|
1656
|
+
* @param {Function} config This function will be executed with the context bound to the apps setting object, this will also be passed as the first argument to the function.
|
1657
|
+
*/
|
1658
|
+
App.prototype.configure = function(config) {
|
1659
|
+
config.call(this.settings, this.settings);
|
1660
|
+
};
|
1661
|
+
|
1662
|
+
/**
|
1663
|
+
* Method to include a plugin in this app.
|
1664
|
+
*
|
1665
|
+
* A plugin is just a function that will be evaluated in the context of the app.
|
1666
|
+
*
|
1667
|
+
* Example:
|
1668
|
+
* app.use(Davis.title)
|
1669
|
+
*
|
1670
|
+
* @param {Function} plugin The plugin to use
|
1671
|
+
*
|
1672
|
+
*/
|
1673
|
+
App.prototype.use = function(plugin) {
|
1674
|
+
plugin.apply(this, Davis.utils.toArray(arguments, 1))
|
1675
|
+
};
|
1676
|
+
|
1677
|
+
/**
|
1678
|
+
* Method to add helper properties to all requests in the application.
|
1679
|
+
*
|
1680
|
+
* Helpers will be added to the Davis.Request.prototype. Care should be taken not to override any existing Davis.Request
|
1681
|
+
* methods.
|
1682
|
+
*
|
1683
|
+
* @param {Object} helpers An object containing helpers to mixin to the request
|
1684
|
+
*/
|
1685
|
+
App.prototype.helpers = function(helpers) {
|
1686
|
+
for (property in helpers) {
|
1687
|
+
if (helpers.hasOwnProperty(property)) Davis.Request.prototype[property] = helpers[property]
|
1688
|
+
}
|
1689
|
+
};
|
1690
|
+
|
1691
|
+
/**
|
1692
|
+
* Settings for the app. These may be overriden directly or by using the configure
|
1693
|
+
* convinience method.
|
1694
|
+
*
|
1695
|
+
* `linkSelector` is the jquery selector for all the links on the page that you want
|
1696
|
+
* Davis to respond to. These links will not trigger a normal http request.
|
1697
|
+
*
|
1698
|
+
* `formSelector` is similar to link selector but for all the forms that davis will bind to
|
1699
|
+
*
|
1700
|
+
* `throwErrors` decides whether or not any errors will be caugth by Davis. If this is set to true
|
1701
|
+
* errors will be thrown so that the request will not be handled by JavaScript, the server will have
|
1702
|
+
* to provide a response. When set to false errors in a route will be caught and the server will not
|
1703
|
+
* receive the request.
|
1704
|
+
*
|
1705
|
+
* `handleRouteNotFound` determines whether or not Davis should handle requests when there is no matching
|
1706
|
+
* route. If set to false Davis will allow the request to be passed to your server to handle if no matching
|
1707
|
+
* route can be found.
|
1708
|
+
*
|
1709
|
+
* `generateRequestOnPageLoad` determines whether a request should be generated for the initial page load.
|
1710
|
+
* by default this is set to false. A Davis.Request will not be generated with the path of the current
|
1711
|
+
* page. Setting this to true will cause a request to be passed to your app for the inital page load.
|
1712
|
+
*
|
1713
|
+
* @see #configure
|
1714
|
+
*/
|
1715
|
+
|
1716
|
+
App.prototype.settings = {
|
1717
|
+
linkSelector: 'a',
|
1718
|
+
formSelector: 'form',
|
1719
|
+
throwErrors: true,
|
1720
|
+
handleRouteNotFound: false,
|
1721
|
+
generateRequestOnPageLoad: false
|
1722
|
+
};
|
1723
|
+
|
1724
|
+
/**
|
1725
|
+
* Starts the app's routing.
|
1726
|
+
*
|
1727
|
+
* Apps created using the convinience Davis() function are automatically started.
|
1728
|
+
*
|
1729
|
+
* Starting the app binds all links and forms, so clicks and submits
|
1730
|
+
* create Davis requests that will be pushed onto the browsers history stack. Browser history change
|
1731
|
+
* events will be picked up and the request that caused the change will be matched against the apps
|
1732
|
+
* routes and filters.
|
1733
|
+
*/
|
1734
|
+
App.prototype.start = function(){
|
1735
|
+
var self = this;
|
1736
|
+
|
1737
|
+
if (this.running) return
|
1738
|
+
|
1739
|
+
if (!Davis.supported()) {
|
1740
|
+
this.trigger('unsupported')
|
1741
|
+
return
|
1742
|
+
};
|
1743
|
+
|
1744
|
+
var runFilterWith = function (request) {
|
1745
|
+
return function (filter) {
|
1746
|
+
var result = filter.run(request, request);
|
1747
|
+
return (typeof result === "undefined" || result);
|
1748
|
+
}
|
1749
|
+
}
|
1750
|
+
|
1751
|
+
var beforeFiltersPass = function (request) {
|
1752
|
+
return Davis.utils.every(
|
1753
|
+
self.lookupBeforeFilter(request.method, request.path),
|
1754
|
+
runFilterWith(request)
|
1755
|
+
)
|
1756
|
+
}
|
1757
|
+
|
1758
|
+
var handleRequest = function (request) {
|
1759
|
+
if (beforeFiltersPass(request)) {
|
1760
|
+
self.trigger('lookupRoute', request)
|
1761
|
+
var route = self.lookupRoute(request.method, request.path);
|
1762
|
+
if (route) {
|
1763
|
+
self.trigger('runRoute', request, route);
|
1764
|
+
|
1765
|
+
try {
|
1766
|
+
route.run(request)
|
1767
|
+
self.trigger('routeComplete', request, route)
|
1768
|
+
} catch (error) {
|
1769
|
+
self.trigger('routeError', request, route, error)
|
1770
|
+
}
|
1771
|
+
|
1772
|
+
Davis.utils.every(
|
1773
|
+
self.lookupAfterFilter(request.method, request.path),
|
1774
|
+
runFilterWith(request)
|
1775
|
+
);
|
1776
|
+
|
1777
|
+
} else {
|
1778
|
+
self.trigger('routeNotFound', request);
|
1779
|
+
}
|
1780
|
+
} else {
|
1781
|
+
self.trigger('requestHalted', request)
|
1782
|
+
}
|
1783
|
+
}
|
1784
|
+
|
1785
|
+
var bindToInternalEvents = function () {
|
1786
|
+
self
|
1787
|
+
.bind('runRoute', function (request) {
|
1788
|
+
self.logger.info("runRoute: " + request.toString());
|
1789
|
+
})
|
1790
|
+
.bind('routeNotFound', function (request) {
|
1791
|
+
if (!self.settings.handleRouteNotFound && !request.isForPageLoad) {
|
1792
|
+
self.stop()
|
1793
|
+
request.delegateToServer()
|
1794
|
+
};
|
1795
|
+
self.logger.warn("routeNotFound: " + request.toString());
|
1796
|
+
})
|
1797
|
+
.bind('start', function () {
|
1798
|
+
self.logger.info("application started")
|
1799
|
+
})
|
1800
|
+
.bind('stop', function () {
|
1801
|
+
self.logger.info("application stopped")
|
1802
|
+
})
|
1803
|
+
.bind('routeError', function (request, route, error) {
|
1804
|
+
if (self.settings.throwErrors) throw(error)
|
1805
|
+
self.logger.error(error.message, error.stack)
|
1806
|
+
});
|
1807
|
+
|
1808
|
+
Davis.location.onChange(function (req) {
|
1809
|
+
handleRequest(req)
|
1810
|
+
});
|
1811
|
+
|
1812
|
+
self.boundToInternalEvents = true
|
1813
|
+
}
|
1814
|
+
|
1815
|
+
if (!this.boundToInternalEvents) bindToInternalEvents()
|
1816
|
+
|
1817
|
+
this.listen();
|
1818
|
+
this.trigger('start')
|
1819
|
+
this.running = true;
|
1820
|
+
|
1821
|
+
if (this.settings.generateRequestOnPageLoad) handleRequest(Davis.Request.forPageLoad())
|
1822
|
+
|
1823
|
+
};
|
1824
|
+
|
1825
|
+
/**
|
1826
|
+
* Stops the app's routing.
|
1827
|
+
*
|
1828
|
+
* Stops the app listening to clicks and submits on all forms and links found using the current
|
1829
|
+
* apps settings.
|
1830
|
+
*/
|
1831
|
+
App.prototype.stop = function() {
|
1832
|
+
this.unlisten();
|
1833
|
+
this.trigger('stop')
|
1834
|
+
this.running = false
|
1835
|
+
};
|
1836
|
+
|
1837
|
+
return App;
|
1838
|
+
})()
|