├── .gitignore
├── .gitmodules
├── .travis.yml
├── Cakefile
├── LICENSE
├── README.md
├── app
├── assets
│ ├── css
│ │ ├── lib
│ │ │ ├── accordion.less
│ │ │ ├── alerts.less
│ │ │ ├── bootstrap.less
│ │ │ ├── breadcrumbs.less
│ │ │ ├── button-groups.less
│ │ │ ├── buttons.less
│ │ │ ├── carousel.less
│ │ │ ├── close.less
│ │ │ ├── code.less
│ │ │ ├── component-animations.less
│ │ │ ├── dropdowns.less
│ │ │ ├── forms.less
│ │ │ ├── grid.less
│ │ │ ├── hero-unit.less
│ │ │ ├── labels-badges.less
│ │ │ ├── layouts.less
│ │ │ ├── mixins.less
│ │ │ ├── modals.less
│ │ │ ├── navbar.less
│ │ │ ├── navs.less
│ │ │ ├── pager.less
│ │ │ ├── pagination.less
│ │ │ ├── patterns.less
│ │ │ ├── popovers.less
│ │ │ ├── progress-bars.less
│ │ │ ├── reset.less
│ │ │ ├── responsive-1200px-min.less
│ │ │ ├── responsive-767px-max.less
│ │ │ ├── responsive-768px-979px.less
│ │ │ ├── responsive-navbar.less
│ │ │ ├── responsive-utilities.less
│ │ │ ├── responsive.less
│ │ │ ├── scaffolding.less
│ │ │ ├── sprites.less
│ │ │ ├── tables.less
│ │ │ ├── thumbnails.less
│ │ │ ├── tooltip.less
│ │ │ ├── type.less
│ │ │ ├── utilities.less
│ │ │ ├── variables.less
│ │ │ └── wells.less
│ │ └── main.css.less
│ └── js
│ │ ├── bootstrap.js
│ │ ├── bootstrap
│ │ ├── bootstrap-affix.js
│ │ ├── bootstrap-alert.js
│ │ ├── bootstrap-button.js
│ │ ├── bootstrap-carousel.js
│ │ ├── bootstrap-collapse.js
│ │ ├── bootstrap-dropdown.js
│ │ ├── bootstrap-modal.js
│ │ ├── bootstrap-popover.js
│ │ ├── bootstrap-scrollspy.js
│ │ ├── bootstrap-tab.js
│ │ ├── bootstrap-tooltip.js
│ │ ├── bootstrap-transition.js
│ │ └── bootstrap-typeahead.js
│ │ ├── home.js
│ │ ├── html5-shiv.js
│ │ ├── jquery-1.8.2.js
│ │ ├── jsonlint.js
│ │ └── topics.js
├── controllers
│ ├── .gitkeep
│ ├── home.coffee
│ ├── http_api.coffee
│ ├── mqtt_api.coffee
│ └── websocket_api.coffee
├── helpers
│ ├── .gitkeep
│ ├── asset_pipeline.coffee
│ ├── json.coffee
│ ├── markdown.coffee
│ └── notest.coffee
├── models
│ ├── .gitkeep
│ └── data.coffee
└── views
│ ├── home.hbs
│ ├── layout.hbs
│ ├── network_button.hbs
│ └── topic.hbs
├── benchmarks
├── bench_http_mqtt_multiple_pub.coffee
├── bench_http_mqtt_multiple_sub.coffee
├── bench_mqtt.coffee
├── haproxy.cfg
└── mqtt_client_pool.coffee
├── features
├── homepage.feature
├── http_pub_sub.feature
├── mqtt_pub_sub.feature
├── steps
│ ├── clients_steps.coffee
│ ├── http_steps.coffee
│ ├── web_client_steps.coffee
│ └── world_overrider.coffee
├── support
│ ├── clients
│ │ ├── http.coffee
│ │ ├── http_json.coffee
│ │ ├── http_txt.coffee
│ │ └── mqtt.coffee
│ ├── clients_helpers.coffee
│ ├── reset_db_hook.coffee
│ └── world.coffee
└── web_interface.feature
├── npm-shrinkwrap.json
├── package.json
├── public
├── favicon.ico
├── images
│ ├── grey.png
│ └── schema.png
├── mu-942f19e0-e626e170-21f6d708-caa97289.txt
└── stylesheets
│ └── .gitkeep
├── qest.coffee
├── qest.js
├── redis.conf
└── test
├── mocha.opts
├── models
└── data_spec.coffee
└── spec_helper.coffee
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/*
2 | *~
3 | *.rdb
4 | public/stylesheets/*.css
5 |
--------------------------------------------------------------------------------
/.gitmodules:
--------------------------------------------------------------------------------
1 | [submodule "node_modules/jasmine-node"]
2 | path = node_modules/jasmine-node
3 | url = git@github.com:mcollina/jasmine-node.git
4 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - 0.8
4 | services:
5 | - redis-server
6 |
--------------------------------------------------------------------------------
/Cakefile:
--------------------------------------------------------------------------------
1 |
2 | child_process = require('child_process')
3 | process = global.process
4 | path = require('path')
5 |
6 | runExternal = (command, callback) ->
7 | console.log("Running #{command}")
8 | child = child_process.spawn("/bin/sh", ["-c", command])
9 | child.stdout.on "data", (data) -> process.stdout.write(data)
10 | child.stderr.on "data", (data) -> process.stderr.write(data)
11 | child.on('exit', callback) if callback?
12 |
13 | launchSpec = (args, callback) ->
14 | runExternal "NODE_ENV=test ./node_modules/.bin/mocha --compilers coffee:coffee-script #{args}", callback
15 |
16 | task "spec", ->
17 | launchSpec "--recursive test", (result) ->
18 | process.exit(result)
19 |
20 | task "spec:ci", ->
21 | launchSpec "--watch --recursive test"
22 |
23 | task "features", ->
24 | runExternal "NODE_ENV=test ./node_modules/.bin/cucumber.js -t ~@wip", (result) ->
25 | if result != 0
26 | console.log "FAIL: scenarios should not fail"
27 | process.exit(result)
28 |
29 | task "features:wip", ->
30 | runExternal "NODE_ENV=test ./node_modules/.bin/cucumber.js -t @wip", (result) ->
31 | if result == 0
32 | console.log "FAIL: wip scenarios should fail"
33 | process.exit(1)
34 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2012 Matteo Collina
2 |
3 | Permission is hereby granted, free of charge, to any person
4 | obtaining a copy of this software and associated documentation
5 | files (the "Software"), to deal in the Software without
6 | restriction, including without limitation the rights to use,
7 | copy, modify, merge, publish, distribute, sublicense, and/or sell
8 | copies of the Software, and to permit persons to whom the
9 | Software is furnished to do so, subject to the following
10 | conditions:
11 |
12 | The above copyright notice and this permission notice shall be
13 | included in all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22 | OTHER DEALINGS IN THE SOFTWARE.
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | # QEST
3 |
4 | [](https://travis-ci.org/mcollina/qest)
6 |
7 | Hello geeks!
8 |
9 | What you are seeing here is a prototype of a distributed [MQTT](http://mqtt.org) broker which is accessible through REST.
10 | That's a lot of jargon, so let me show you the whole picture!
11 |
12 | Here we are dreaming a Web of Things, where you can reach (and interact) with each of your "real" devices using the web,
13 | as it's the Way everybody interacts with a computer these days.
14 | However it's somewhat hard to build these kind of apps, so researchers have written custom protocols for communicating
15 | with the devices.
16 | The state-of-the-art seems to be [MQTT](http://mqtt.org), which is standard, free of royalties, and widespread:
17 | there are libraries for all the major platforms.
18 |
19 | QEST is a stargate between the universe of devices which speak MQTT, and the universe of apps which
20 | speak HTTP.
21 | In this way you don't have to deal any custom protocol, you just GET and PUT the topic URI, like these:
22 |
23 | $ curl -X PUT -d '{ "hello": 555 }' \
24 | -H "Content-Type: application/json" \
25 | http://mqtt.matteocollina.com/topics/prova
26 | $ curl http://mqtt.matteocollina.com/topics/prova
27 | { "hello": 555 }
28 |
29 | Let's build cool things with MQTT, REST and Arduino!
30 |
31 | ## Usage
32 |
33 | Install [Node.js](http://nodejs.org) version 0.8, and
34 | [Redis](http://redis.io).
35 |
36 | ```
37 | $ git clone git@github.com:mcollina/qest.git
38 | $ cd qest
39 | $ npm install
40 | $ ./qest.js
41 | ```
42 |
43 | ## Examples
44 |
45 | * [NetworkButton](https://github.com/mcollina/qest/wiki/Network-Button-Example)
46 | * [NetworkButtonJSON](https://gist.github.com/mcollina/5337389), same as
47 | before, but exchanging JSONs.
48 |
49 | ## Contribute
50 |
51 | * Check out the latest master to make sure the feature hasn't been
52 | implemented or the bug hasn't been fixed yet
53 | * Check out the issue tracker to make sure someone already hasn't
54 | requested it and/or contributed it
55 | * Fork the project
56 | * Start a feature/bugfix branch
57 | * Commit and push until you are happy with your contribution
58 | * Make sure to add tests for it. This is important so I don't break it
59 | in a future version unintentionally.
60 | * Please try not to mess with the Cakefile and package.json. If you
61 | want to have your own version, or is otherwise necessary, that is
62 | fine, but please isolate to its own commit so I can cherry-pick around
63 | it.
64 |
65 | ## Thanks
66 |
67 | This work would not have been possible without the support
68 | of the University of Bologna, which funded my Ph.D. program.
69 | Moreover I would like to thank my professors, Giovanni
70 | Emanuele Corazza and Alessandro Vanelli-Coralli for the support
71 | and the feedbacks.
72 |
73 | ## License
74 |
75 | Copyright (c) 2012 Matteo Collina, http://matteocollina.com
76 |
77 | Permission is hereby granted, free of charge, to any person
78 | obtaining a copy of this software and associated documentation
79 | files (the "Software"), to deal in the Software without
80 | restriction, including without limitation the rights to use,
81 | copy, modify, merge, publish, distribute, sublicense, and/or sell
82 | copies of the Software, and to permit persons to whom the
83 | Software is furnished to do so, subject to the following
84 | conditions:
85 |
86 | The above copyright notice and this permission notice shall be
87 | included in all copies or substantial portions of the Software.
88 |
89 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
90 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
91 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
92 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
93 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
94 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
95 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
96 | OTHER DEALINGS IN THE SOFTWARE.
97 |
--------------------------------------------------------------------------------
/app/assets/css/lib/accordion.less:
--------------------------------------------------------------------------------
1 | //
2 | // Accordion
3 | // --------------------------------------------------
4 |
5 |
6 | // Parent container
7 | .accordion {
8 | margin-bottom: @baseLineHeight;
9 | }
10 |
11 | // Group == heading + body
12 | .accordion-group {
13 | margin-bottom: 2px;
14 | border: 1px solid #e5e5e5;
15 | .border-radius(4px);
16 | }
17 | .accordion-heading {
18 | border-bottom: 0;
19 | }
20 | .accordion-heading .accordion-toggle {
21 | display: block;
22 | padding: 8px 15px;
23 | }
24 |
25 | // General toggle styles
26 | .accordion-toggle {
27 | cursor: pointer;
28 | }
29 |
30 | // Inner needs the styles because you can't animate properly with any styles on the element
31 | .accordion-inner {
32 | padding: 9px 15px;
33 | border-top: 1px solid #e5e5e5;
34 | }
35 |
--------------------------------------------------------------------------------
/app/assets/css/lib/alerts.less:
--------------------------------------------------------------------------------
1 | //
2 | // Alerts
3 | // --------------------------------------------------
4 |
5 |
6 | // Base styles
7 | // -------------------------
8 |
9 | .alert {
10 | padding: 8px 35px 8px 14px;
11 | margin-bottom: @baseLineHeight;
12 | text-shadow: 0 1px 0 rgba(255,255,255,.5);
13 | background-color: @warningBackground;
14 | border: 1px solid @warningBorder;
15 | .border-radius(4px);
16 | color: @warningText;
17 | }
18 | .alert h4 {
19 | margin: 0;
20 | }
21 |
22 | // Adjust close link position
23 | .alert .close {
24 | position: relative;
25 | top: -2px;
26 | right: -21px;
27 | line-height: @baseLineHeight;
28 | }
29 |
30 |
31 | // Alternate styles
32 | // -------------------------
33 |
34 | .alert-success {
35 | background-color: @successBackground;
36 | border-color: @successBorder;
37 | color: @successText;
38 | }
39 | .alert-danger,
40 | .alert-error {
41 | background-color: @errorBackground;
42 | border-color: @errorBorder;
43 | color: @errorText;
44 | }
45 | .alert-info {
46 | background-color: @infoBackground;
47 | border-color: @infoBorder;
48 | color: @infoText;
49 | }
50 |
51 |
52 | // Block alerts
53 | // -------------------------
54 |
55 | .alert-block {
56 | padding-top: 14px;
57 | padding-bottom: 14px;
58 | }
59 | .alert-block > p,
60 | .alert-block > ul {
61 | margin-bottom: 0;
62 | }
63 | .alert-block p + p {
64 | margin-top: 5px;
65 | }
66 |
--------------------------------------------------------------------------------
/app/assets/css/lib/bootstrap.less:
--------------------------------------------------------------------------------
1 | /*!
2 | * Bootstrap v2.1.1
3 | *
4 | * Copyright 2012 Twitter, Inc
5 | * Licensed under the Apache License v2.0
6 | * http://www.apache.org/licenses/LICENSE-2.0
7 | *
8 | * Designed and built with all the love in the world @twitter by @mdo and @fat.
9 | */
10 |
11 | // CSS Reset
12 | @import "reset.less";
13 |
14 | // Core variables and mixins
15 | @import "variables.less"; // Modify this for custom colors, font-sizes, etc
16 | @import "mixins.less";
17 |
18 | // Grid system and page structure
19 | @import "scaffolding.less";
20 | @import "grid.less";
21 | @import "layouts.less";
22 |
23 | // Base CSS
24 | @import "type.less";
25 | @import "code.less";
26 | @import "forms.less";
27 | @import "tables.less";
28 |
29 | // Components: common
30 | @import "sprites.less";
31 | @import "dropdowns.less";
32 | @import "wells.less";
33 | @import "component-animations.less";
34 | @import "close.less";
35 |
36 | // Components: Buttons & Alerts
37 | @import "buttons.less";
38 | @import "button-groups.less";
39 | @import "alerts.less"; // Note: alerts share common CSS with buttons and thus have styles in buttons.less
40 |
41 | // Components: Nav
42 | @import "navs.less";
43 | @import "navbar.less";
44 | @import "breadcrumbs.less";
45 | @import "pagination.less";
46 | @import "pager.less";
47 |
48 | // Components: Popovers
49 | @import "modals.less";
50 | @import "tooltip.less";
51 | @import "popovers.less";
52 |
53 | // Components: Misc
54 | @import "thumbnails.less";
55 | @import "labels-badges.less";
56 | @import "progress-bars.less";
57 | @import "accordion.less";
58 | @import "carousel.less";
59 | @import "hero-unit.less";
60 |
61 | // Utility classes
62 | @import "utilities.less"; // Has to be last to override when necessary
63 |
--------------------------------------------------------------------------------
/app/assets/css/lib/breadcrumbs.less:
--------------------------------------------------------------------------------
1 | //
2 | // Breadcrumbs
3 | // --------------------------------------------------
4 |
5 |
6 | .breadcrumb {
7 | padding: 8px 15px;
8 | margin: 0 0 @baseLineHeight;
9 | list-style: none;
10 | background-color: #f5f5f5;
11 | .border-radius(4px);
12 | li {
13 | display: inline-block;
14 | .ie7-inline-block();
15 | text-shadow: 0 1px 0 @white;
16 | }
17 | .divider {
18 | padding: 0 5px;
19 | color: #ccc;
20 | }
21 | .active {
22 | color: @grayLight;
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/app/assets/css/lib/button-groups.less:
--------------------------------------------------------------------------------
1 | //
2 | // Button groups
3 | // --------------------------------------------------
4 |
5 |
6 | // Make the div behave like a button
7 | .btn-group {
8 | position: relative;
9 | font-size: 0; // remove as part 1 of font-size inline-block hack
10 | vertical-align: middle; // match .btn alignment given font-size hack above
11 | white-space: nowrap; // prevent buttons from wrapping when in tight spaces (e.g., the table on the tests page)
12 | .ie7-restore-left-whitespace();
13 | }
14 |
15 | // Space out series of button groups
16 | .btn-group + .btn-group {
17 | margin-left: 5px;
18 | }
19 |
20 | // Optional: Group multiple button groups together for a toolbar
21 | .btn-toolbar {
22 | font-size: 0; // Hack to remove whitespace that results from using inline-block
23 | margin-top: @baseLineHeight / 2;
24 | margin-bottom: @baseLineHeight / 2;
25 | .btn-group {
26 | display: inline-block;
27 | .ie7-inline-block();
28 | }
29 | .btn + .btn,
30 | .btn-group + .btn,
31 | .btn + .btn-group {
32 | margin-left: 5px;
33 | }
34 | }
35 |
36 | // Float them, remove border radius, then re-add to first and last elements
37 | .btn-group > .btn {
38 | position: relative;
39 | .border-radius(0);
40 | }
41 | .btn-group > .btn + .btn {
42 | margin-left: -1px;
43 | }
44 | .btn-group > .btn,
45 | .btn-group > .dropdown-menu {
46 | font-size: @baseFontSize; // redeclare as part 2 of font-size inline-block hack
47 | }
48 |
49 | // Reset fonts for other sizes
50 | .btn-group > .btn-mini {
51 | font-size: 11px;
52 | }
53 | .btn-group > .btn-small {
54 | font-size: 12px;
55 | }
56 | .btn-group > .btn-large {
57 | font-size: 16px;
58 | }
59 |
60 | // Set corners individual because sometimes a single button can be in a .btn-group and we need :first-child and :last-child to both match
61 | .btn-group > .btn:first-child {
62 | margin-left: 0;
63 | -webkit-border-top-left-radius: 4px;
64 | -moz-border-radius-topleft: 4px;
65 | border-top-left-radius: 4px;
66 | -webkit-border-bottom-left-radius: 4px;
67 | -moz-border-radius-bottomleft: 4px;
68 | border-bottom-left-radius: 4px;
69 | }
70 | // Need .dropdown-toggle since :last-child doesn't apply given a .dropdown-menu immediately after it
71 | .btn-group > .btn:last-child,
72 | .btn-group > .dropdown-toggle {
73 | -webkit-border-top-right-radius: 4px;
74 | -moz-border-radius-topright: 4px;
75 | border-top-right-radius: 4px;
76 | -webkit-border-bottom-right-radius: 4px;
77 | -moz-border-radius-bottomright: 4px;
78 | border-bottom-right-radius: 4px;
79 | }
80 | // Reset corners for large buttons
81 | .btn-group > .btn.large:first-child {
82 | margin-left: 0;
83 | -webkit-border-top-left-radius: 6px;
84 | -moz-border-radius-topleft: 6px;
85 | border-top-left-radius: 6px;
86 | -webkit-border-bottom-left-radius: 6px;
87 | -moz-border-radius-bottomleft: 6px;
88 | border-bottom-left-radius: 6px;
89 | }
90 | .btn-group > .btn.large:last-child,
91 | .btn-group > .large.dropdown-toggle {
92 | -webkit-border-top-right-radius: 6px;
93 | -moz-border-radius-topright: 6px;
94 | border-top-right-radius: 6px;
95 | -webkit-border-bottom-right-radius: 6px;
96 | -moz-border-radius-bottomright: 6px;
97 | border-bottom-right-radius: 6px;
98 | }
99 |
100 | // On hover/focus/active, bring the proper btn to front
101 | .btn-group > .btn:hover,
102 | .btn-group > .btn:focus,
103 | .btn-group > .btn:active,
104 | .btn-group > .btn.active {
105 | z-index: 2;
106 | }
107 |
108 | // On active and open, don't show outline
109 | .btn-group .dropdown-toggle:active,
110 | .btn-group.open .dropdown-toggle {
111 | outline: 0;
112 | }
113 |
114 |
115 |
116 | // Split button dropdowns
117 | // ----------------------
118 |
119 | // Give the line between buttons some depth
120 | .btn-group > .btn + .dropdown-toggle {
121 | padding-left: 8px;
122 | padding-right: 8px;
123 | .box-shadow(inset 1px 0 0 rgba(255,255,255,.125), inset 0 1px 0 rgba(255,255,255,.2), 0 1px 2px rgba(0,0,0,.05));
124 | *padding-top: 5px;
125 | *padding-bottom: 5px;
126 | }
127 | .btn-group > .btn-mini + .dropdown-toggle {
128 | padding-left: 5px;
129 | padding-right: 5px;
130 | *padding-top: 2px;
131 | *padding-bottom: 2px;
132 | }
133 | .btn-group > .btn-small + .dropdown-toggle {
134 | *padding-top: 5px;
135 | *padding-bottom: 4px;
136 | }
137 | .btn-group > .btn-large + .dropdown-toggle {
138 | padding-left: 12px;
139 | padding-right: 12px;
140 | *padding-top: 7px;
141 | *padding-bottom: 7px;
142 | }
143 |
144 | .btn-group.open {
145 |
146 | // The clickable button for toggling the menu
147 | // Remove the gradient and set the same inset shadow as the :active state
148 | .dropdown-toggle {
149 | background-image: none;
150 | .box-shadow(inset 0 2px 4px rgba(0,0,0,.15), 0 1px 2px rgba(0,0,0,.05));
151 | }
152 |
153 | // Keep the hover's background when dropdown is open
154 | .btn.dropdown-toggle {
155 | background-color: @btnBackgroundHighlight;
156 | }
157 | .btn-primary.dropdown-toggle {
158 | background-color: @btnPrimaryBackgroundHighlight;
159 | }
160 | .btn-warning.dropdown-toggle {
161 | background-color: @btnWarningBackgroundHighlight;
162 | }
163 | .btn-danger.dropdown-toggle {
164 | background-color: @btnDangerBackgroundHighlight;
165 | }
166 | .btn-success.dropdown-toggle {
167 | background-color: @btnSuccessBackgroundHighlight;
168 | }
169 | .btn-info.dropdown-toggle {
170 | background-color: @btnInfoBackgroundHighlight;
171 | }
172 | .btn-inverse.dropdown-toggle {
173 | background-color: @btnInverseBackgroundHighlight;
174 | }
175 | }
176 |
177 |
178 | // Reposition the caret
179 | .btn .caret {
180 | margin-top: 8px;
181 | margin-left: 0;
182 | }
183 | // Carets in other button sizes
184 | .btn-mini .caret,
185 | .btn-small .caret,
186 | .btn-large .caret {
187 | margin-top: 6px;
188 | }
189 | .btn-large .caret {
190 | border-left-width: 5px;
191 | border-right-width: 5px;
192 | border-top-width: 5px;
193 | }
194 | // Upside down carets for .dropup
195 | .dropup .btn-large .caret {
196 | border-bottom: 5px solid @black;
197 | border-top: 0;
198 | }
199 |
200 |
201 |
202 | // Account for other colors
203 | .btn-primary,
204 | .btn-warning,
205 | .btn-danger,
206 | .btn-info,
207 | .btn-success,
208 | .btn-inverse {
209 | .caret {
210 | border-top-color: @white;
211 | border-bottom-color: @white;
212 | }
213 | }
214 |
215 |
216 |
217 | // Vertical button groups
218 | // ----------------------
219 |
220 | .btn-group-vertical {
221 | display: inline-block; // makes buttons only take up the width they need
222 | .ie7-inline-block();
223 | }
224 | .btn-group-vertical .btn {
225 | display: block;
226 | float: none;
227 | width: 100%;
228 | .border-radius(0);
229 | }
230 | .btn-group-vertical .btn + .btn {
231 | margin-left: 0;
232 | margin-top: -1px;
233 | }
234 | .btn-group-vertical .btn:first-child {
235 | .border-radius(4px 4px 0 0);
236 | }
237 | .btn-group-vertical .btn:last-child {
238 | .border-radius(0 0 4px 4px);
239 | }
240 | .btn-group-vertical .btn-large:first-child {
241 | .border-radius(6px 6px 0 0);
242 | }
243 | .btn-group-vertical .btn-large:last-child {
244 | .border-radius(0 0 6px 6px);
245 | }
246 |
--------------------------------------------------------------------------------
/app/assets/css/lib/buttons.less:
--------------------------------------------------------------------------------
1 | //
2 | // Buttons
3 | // --------------------------------------------------
4 |
5 |
6 | // Base styles
7 | // --------------------------------------------------
8 |
9 | // Core
10 | .btn {
11 | display: inline-block;
12 | .ie7-inline-block();
13 | padding: 4px 14px;
14 | margin-bottom: 0; // For input.btn
15 | font-size: @baseFontSize;
16 | line-height: @baseLineHeight;
17 | *line-height: @baseLineHeight;
18 | text-align: center;
19 | vertical-align: middle;
20 | cursor: pointer;
21 | .buttonBackground(@btnBackground, @btnBackgroundHighlight, @grayDark, 0 1px 1px rgba(255,255,255,.75));
22 | border: 1px solid @btnBorder;
23 | *border: 0; // Remove the border to prevent IE7's black border on input:focus
24 | border-bottom-color: darken(@btnBorder, 10%);
25 | .border-radius(4px);
26 | .ie7-restore-left-whitespace(); // Give IE7 some love
27 | .box-shadow(inset 0 1px 0 rgba(255,255,255,.2), 0 1px 2px rgba(0,0,0,.05));
28 |
29 | // Hover state
30 | &:hover {
31 | color: @grayDark;
32 | text-decoration: none;
33 | background-color: darken(@white, 10%);
34 | *background-color: darken(@white, 15%); /* Buttons in IE7 don't get borders, so darken on hover */
35 | background-position: 0 -15px;
36 |
37 | // transition is only when going to hover, otherwise the background
38 | // behind the gradient (there for IE<=9 fallback) gets mismatched
39 | .transition(background-position .1s linear);
40 | }
41 |
42 | // Focus state for keyboard and accessibility
43 | &:focus {
44 | .tab-focus();
45 | }
46 |
47 | // Active state
48 | &.active,
49 | &:active {
50 | background-color: darken(@white, 10%);
51 | background-color: darken(@white, 15%) e("\9");
52 | background-image: none;
53 | outline: 0;
54 | .box-shadow(inset 0 2px 4px rgba(0,0,0,.15), 0 1px 2px rgba(0,0,0,.05));
55 | }
56 |
57 | // Disabled state
58 | &.disabled,
59 | &[disabled] {
60 | cursor: default;
61 | background-color: darken(@white, 10%);
62 | background-image: none;
63 | .opacity(65);
64 | .box-shadow(none);
65 | }
66 |
67 | }
68 |
69 |
70 |
71 | // Button Sizes
72 | // --------------------------------------------------
73 |
74 | // Large
75 | .btn-large {
76 | padding: 9px 14px;
77 | font-size: @baseFontSize + 2px;
78 | line-height: normal;
79 | .border-radius(5px);
80 | }
81 | .btn-large [class^="icon-"] {
82 | margin-top: 2px;
83 | }
84 |
85 | // Small
86 | .btn-small {
87 | padding: 3px 9px;
88 | font-size: @baseFontSize - 2px;
89 | line-height: @baseLineHeight - 2px;
90 | }
91 | .btn-small [class^="icon-"] {
92 | margin-top: 0;
93 | }
94 |
95 | // Mini
96 | .btn-mini {
97 | padding: 2px 6px;
98 | font-size: @baseFontSize - 3px;
99 | line-height: @baseLineHeight - 3px;
100 | }
101 |
102 |
103 | // Block button
104 | // -------------------------
105 |
106 | .btn-block {
107 | display: block;
108 | width: 100%;
109 | padding-left: 0;
110 | padding-right: 0;
111 | .box-sizing(border-box);
112 | }
113 |
114 | // Vertically space out multiple block buttons
115 | .btn-block + .btn-block {
116 | margin-top: 5px;
117 | }
118 |
119 | // Specificity overrides
120 | input[type="submit"],
121 | input[type="reset"],
122 | input[type="button"] {
123 | &.btn-block {
124 | width: 100%;
125 | }
126 | }
127 |
128 |
129 |
130 | // Alternate buttons
131 | // --------------------------------------------------
132 |
133 | // Provide *some* extra contrast for those who can get it
134 | .btn-primary.active,
135 | .btn-warning.active,
136 | .btn-danger.active,
137 | .btn-success.active,
138 | .btn-info.active,
139 | .btn-inverse.active {
140 | color: rgba(255,255,255,.75);
141 | }
142 |
143 | // Set the backgrounds
144 | // -------------------------
145 | .btn {
146 | // reset here as of 2.0.3 due to Recess property order
147 | border-color: #c5c5c5;
148 | border-color: rgba(0,0,0,.15) rgba(0,0,0,.15) rgba(0,0,0,.25);
149 | }
150 | .btn-primary {
151 | .buttonBackground(@btnPrimaryBackground, @btnPrimaryBackgroundHighlight);
152 | }
153 | // Warning appears are orange
154 | .btn-warning {
155 | .buttonBackground(@btnWarningBackground, @btnWarningBackgroundHighlight);
156 | }
157 | // Danger and error appear as red
158 | .btn-danger {
159 | .buttonBackground(@btnDangerBackground, @btnDangerBackgroundHighlight);
160 | }
161 | // Success appears as green
162 | .btn-success {
163 | .buttonBackground(@btnSuccessBackground, @btnSuccessBackgroundHighlight);
164 | }
165 | // Info appears as a neutral blue
166 | .btn-info {
167 | .buttonBackground(@btnInfoBackground, @btnInfoBackgroundHighlight);
168 | }
169 | // Inverse appears as dark gray
170 | .btn-inverse {
171 | .buttonBackground(@btnInverseBackground, @btnInverseBackgroundHighlight);
172 | }
173 |
174 |
175 | // Cross-browser Jank
176 | // --------------------------------------------------
177 |
178 | button.btn,
179 | input[type="submit"].btn {
180 |
181 | // Firefox 3.6 only I believe
182 | &::-moz-focus-inner {
183 | padding: 0;
184 | border: 0;
185 | }
186 |
187 | // IE7 has some default padding on button controls
188 | *padding-top: 3px;
189 | *padding-bottom: 3px;
190 |
191 | &.btn-large {
192 | *padding-top: 7px;
193 | *padding-bottom: 7px;
194 | }
195 | &.btn-small {
196 | *padding-top: 3px;
197 | *padding-bottom: 3px;
198 | }
199 | &.btn-mini {
200 | *padding-top: 1px;
201 | *padding-bottom: 1px;
202 | }
203 | }
204 |
205 |
206 | // Link buttons
207 | // --------------------------------------------------
208 |
209 | // Make a button look and behave like a link
210 | .btn-link,
211 | .btn-link:active,
212 | .btn-link[disabled] {
213 | background-color: transparent;
214 | background-image: none;
215 | .box-shadow(none);
216 | }
217 | .btn-link {
218 | border-color: transparent;
219 | cursor: pointer;
220 | color: @linkColor;
221 | .border-radius(0);
222 | }
223 | .btn-link:hover {
224 | color: @linkColorHover;
225 | text-decoration: underline;
226 | background-color: transparent;
227 | }
228 | .btn-link[disabled]:hover {
229 | color: @grayDark;
230 | text-decoration: none;
231 | }
232 |
--------------------------------------------------------------------------------
/app/assets/css/lib/carousel.less:
--------------------------------------------------------------------------------
1 | //
2 | // Carousel
3 | // --------------------------------------------------
4 |
5 |
6 | .carousel {
7 | position: relative;
8 | margin-bottom: @baseLineHeight;
9 | line-height: 1;
10 | }
11 |
12 | .carousel-inner {
13 | overflow: hidden;
14 | width: 100%;
15 | position: relative;
16 | }
17 |
18 | .carousel {
19 |
20 | .item {
21 | display: none;
22 | position: relative;
23 | .transition(.6s ease-in-out left);
24 | }
25 |
26 | // Account for jankitude on images
27 | .item > img {
28 | display: block;
29 | line-height: 1;
30 | }
31 |
32 | .active,
33 | .next,
34 | .prev { display: block; }
35 |
36 | .active {
37 | left: 0;
38 | }
39 |
40 | .next,
41 | .prev {
42 | position: absolute;
43 | top: 0;
44 | width: 100%;
45 | }
46 |
47 | .next {
48 | left: 100%;
49 | }
50 | .prev {
51 | left: -100%;
52 | }
53 | .next.left,
54 | .prev.right {
55 | left: 0;
56 | }
57 |
58 | .active.left {
59 | left: -100%;
60 | }
61 | .active.right {
62 | left: 100%;
63 | }
64 |
65 | }
66 |
67 | // Left/right controls for nav
68 | // ---------------------------
69 |
70 | .carousel-control {
71 | position: absolute;
72 | top: 40%;
73 | left: 15px;
74 | width: 40px;
75 | height: 40px;
76 | margin-top: -20px;
77 | font-size: 60px;
78 | font-weight: 100;
79 | line-height: 30px;
80 | color: @white;
81 | text-align: center;
82 | background: @grayDarker;
83 | border: 3px solid @white;
84 | .border-radius(23px);
85 | .opacity(50);
86 |
87 | // we can't have this transition here
88 | // because webkit cancels the carousel
89 | // animation if you trip this while
90 | // in the middle of another animation
91 | // ;_;
92 | // .transition(opacity .2s linear);
93 |
94 | // Reposition the right one
95 | &.right {
96 | left: auto;
97 | right: 15px;
98 | }
99 |
100 | // Hover state
101 | &:hover {
102 | color: @white;
103 | text-decoration: none;
104 | .opacity(90);
105 | }
106 | }
107 |
108 |
109 | // Caption for text below images
110 | // -----------------------------
111 |
112 | .carousel-caption {
113 | position: absolute;
114 | left: 0;
115 | right: 0;
116 | bottom: 0;
117 | padding: 15px;
118 | background: @grayDark;
119 | background: rgba(0,0,0,.75);
120 | }
121 | .carousel-caption h4,
122 | .carousel-caption p {
123 | color: @white;
124 | line-height: @baseLineHeight;
125 | }
126 | .carousel-caption h4 {
127 | margin: 0 0 5px;
128 | }
129 | .carousel-caption p {
130 | margin-bottom: 0;
131 | }
132 |
--------------------------------------------------------------------------------
/app/assets/css/lib/close.less:
--------------------------------------------------------------------------------
1 | //
2 | // Close icons
3 | // --------------------------------------------------
4 |
5 |
6 | .close {
7 | float: right;
8 | font-size: 20px;
9 | font-weight: bold;
10 | line-height: @baseLineHeight;
11 | color: @black;
12 | text-shadow: 0 1px 0 rgba(255,255,255,1);
13 | .opacity(20);
14 | &:hover {
15 | color: @black;
16 | text-decoration: none;
17 | cursor: pointer;
18 | .opacity(40);
19 | }
20 | }
21 |
22 | // Additional properties for button version
23 | // iOS requires the button element instead of an anchor tag.
24 | // If you want the anchor version, it requires `href="#"`.
25 | button.close {
26 | padding: 0;
27 | cursor: pointer;
28 | background: transparent;
29 | border: 0;
30 | -webkit-appearance: none;
31 | }
--------------------------------------------------------------------------------
/app/assets/css/lib/code.less:
--------------------------------------------------------------------------------
1 | //
2 | // Code (inline and blocK)
3 | // --------------------------------------------------
4 |
5 |
6 | // Inline and block code styles
7 | code,
8 | pre {
9 | padding: 0 3px 2px;
10 | #font > #family > .monospace;
11 | font-size: @baseFontSize - 2;
12 | color: @grayDark;
13 | .border-radius(3px);
14 | }
15 |
16 | // Inline code
17 | code {
18 | padding: 2px 4px;
19 | color: #d14;
20 | background-color: #f7f7f9;
21 | border: 1px solid #e1e1e8;
22 | }
23 |
24 | // Blocks of code
25 | pre {
26 | display: block;
27 | padding: (@baseLineHeight - 1) / 2;
28 | margin: 0 0 @baseLineHeight / 2;
29 | font-size: @baseFontSize - 1; // 14px to 13px
30 | line-height: @baseLineHeight;
31 | word-break: break-all;
32 | word-wrap: break-word;
33 | white-space: pre;
34 | white-space: pre-wrap;
35 | background-color: #f5f5f5;
36 | border: 1px solid #ccc; // fallback for IE7-8
37 | border: 1px solid rgba(0,0,0,.15);
38 | .border-radius(4px);
39 |
40 | // Make prettyprint styles more spaced out for readability
41 | &.prettyprint {
42 | margin-bottom: @baseLineHeight;
43 | }
44 |
45 | // Account for some code outputs that place code tags in pre tags
46 | code {
47 | padding: 0;
48 | color: inherit;
49 | background-color: transparent;
50 | border: 0;
51 | }
52 | }
53 |
54 | // Enable scrollable blocks of code
55 | .pre-scrollable {
56 | max-height: 340px;
57 | overflow-y: scroll;
58 | }
--------------------------------------------------------------------------------
/app/assets/css/lib/component-animations.less:
--------------------------------------------------------------------------------
1 | //
2 | // Component animations
3 | // --------------------------------------------------
4 |
5 |
6 | .fade {
7 | opacity: 0;
8 | .transition(opacity .15s linear);
9 | &.in {
10 | opacity: 1;
11 | }
12 | }
13 |
14 | .collapse {
15 | position: relative;
16 | height: 0;
17 | overflow: hidden;
18 | .transition(height .35s ease);
19 | &.in {
20 | height: auto;
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/app/assets/css/lib/dropdowns.less:
--------------------------------------------------------------------------------
1 | //
2 | // Dropdown menus
3 | // --------------------------------------------------
4 |
5 |
6 | // Use the .menu class on any
element within the topbar or ul.tabs and you'll get some superfancy dropdowns
7 | .dropup,
8 | .dropdown {
9 | position: relative;
10 | }
11 | .dropdown-toggle {
12 | // The caret makes the toggle a bit too tall in IE7
13 | *margin-bottom: -3px;
14 | }
15 | .dropdown-toggle:active,
16 | .open .dropdown-toggle {
17 | outline: 0;
18 | }
19 |
20 | // Dropdown arrow/caret
21 | // --------------------
22 | .caret {
23 | display: inline-block;
24 | width: 0;
25 | height: 0;
26 | vertical-align: top;
27 | border-top: 4px solid @black;
28 | border-right: 4px solid transparent;
29 | border-left: 4px solid transparent;
30 | content: "";
31 | }
32 |
33 | // Place the caret
34 | .dropdown .caret {
35 | margin-top: 8px;
36 | margin-left: 2px;
37 | }
38 |
39 | // The dropdown menu (ul)
40 | // ----------------------
41 | .dropdown-menu {
42 | position: absolute;
43 | top: 100%;
44 | left: 0;
45 | z-index: @zindexDropdown;
46 | display: none; // none by default, but block on "open" of the menu
47 | float: left;
48 | min-width: 160px;
49 | padding: 5px 0;
50 | margin: 2px 0 0; // override default ul
51 | list-style: none;
52 | background-color: @dropdownBackground;
53 | border: 1px solid #ccc; // Fallback for IE7-8
54 | border: 1px solid @dropdownBorder;
55 | *border-right-width: 2px;
56 | *border-bottom-width: 2px;
57 | .border-radius(6px);
58 | .box-shadow(0 5px 10px rgba(0,0,0,.2));
59 | -webkit-background-clip: padding-box;
60 | -moz-background-clip: padding;
61 | background-clip: padding-box;
62 |
63 | // Aligns the dropdown menu to right
64 | &.pull-right {
65 | right: 0;
66 | left: auto;
67 | }
68 |
69 | // Dividers (basically an hr) within the dropdown
70 | .divider {
71 | .nav-divider(@dropdownDividerTop, @dropdownDividerBottom);
72 | }
73 |
74 | // Links within the dropdown menu
75 | a {
76 | display: block;
77 | padding: 3px 20px;
78 | clear: both;
79 | font-weight: normal;
80 | line-height: @baseLineHeight;
81 | color: @dropdownLinkColor;
82 | white-space: nowrap;
83 | }
84 | }
85 |
86 | // Hover state
87 | // -----------
88 | .dropdown-menu li > a:hover,
89 | .dropdown-menu li > a:focus,
90 | .dropdown-submenu:hover > a {
91 | text-decoration: none;
92 | color: @dropdownLinkColorHover;
93 | background-color: @dropdownLinkBackgroundHover;
94 | #gradient > .vertical(@dropdownLinkBackgroundHover, darken(@dropdownLinkBackgroundHover, 5%));
95 | }
96 |
97 | // Active state
98 | // ------------
99 | .dropdown-menu .active > a,
100 | .dropdown-menu .active > a:hover {
101 | color: @dropdownLinkColorHover;
102 | text-decoration: none;
103 | outline: 0;
104 | background-color: @dropdownLinkBackgroundActive;
105 | #gradient > .vertical(@dropdownLinkBackgroundActive, darken(@dropdownLinkBackgroundActive, 5%));
106 | }
107 |
108 | // Disabled state
109 | // --------------
110 | // Gray out text and ensure the hover state remains gray
111 | .dropdown-menu .disabled > a,
112 | .dropdown-menu .disabled > a:hover {
113 | color: @grayLight;
114 | }
115 | // Nuke hover effects
116 | .dropdown-menu .disabled > a:hover {
117 | text-decoration: none;
118 | background-color: transparent;
119 | cursor: default;
120 | }
121 |
122 | // Open state for the dropdown
123 | // ---------------------------
124 | .open {
125 | // IE7's z-index only goes to the nearest positioned ancestor, which would
126 | // make the menu appear below buttons that appeared later on the page
127 | *z-index: @zindexDropdown;
128 |
129 | & > .dropdown-menu {
130 | display: block;
131 | }
132 | }
133 |
134 | // Right aligned dropdowns
135 | // ---------------------------
136 | .pull-right > .dropdown-menu {
137 | right: 0;
138 | left: auto;
139 | }
140 |
141 | // Allow for dropdowns to go bottom up (aka, dropup-menu)
142 | // ------------------------------------------------------
143 | // Just add .dropup after the standard .dropdown class and you're set, bro.
144 | // TODO: abstract this so that the navbar fixed styles are not placed here?
145 | .dropup,
146 | .navbar-fixed-bottom .dropdown {
147 | // Reverse the caret
148 | .caret {
149 | border-top: 0;
150 | border-bottom: 4px solid @black;
151 | content: "";
152 | }
153 | // Different positioning for bottom up menu
154 | .dropdown-menu {
155 | top: auto;
156 | bottom: 100%;
157 | margin-bottom: 1px;
158 | }
159 | }
160 |
161 | // Sub menus
162 | // ---------------------------
163 | .dropdown-submenu {
164 | position: relative;
165 | }
166 | .dropdown-submenu > .dropdown-menu {
167 | top: 0;
168 | left: 100%;
169 | margin-top: -6px;
170 | margin-left: -1px;
171 | -webkit-border-radius: 0 6px 6px 6px;
172 | -moz-border-radius: 0 6px 6px 6px;
173 | border-radius: 0 6px 6px 6px;
174 | }
175 | .dropdown-submenu:hover > .dropdown-menu {
176 | display: block;
177 | }
178 |
179 | .dropdown-submenu > a:after {
180 | display: block;
181 | content: " ";
182 | float: right;
183 | width: 0;
184 | height: 0;
185 | border-color: transparent;
186 | border-style: solid;
187 | border-width: 5px 0 5px 5px;
188 | border-left-color: darken(@dropdownBackground, 20%);
189 | margin-top: 5px;
190 | margin-right: -10px;
191 | }
192 | .dropdown-submenu:hover > a:after {
193 | border-left-color: @dropdownLinkColorHover;
194 | }
195 |
196 |
197 | // Tweak nav headers
198 | // -----------------
199 | // Increase padding from 15px to 20px on sides
200 | .dropdown .dropdown-menu .nav-header {
201 | padding-left: 20px;
202 | padding-right: 20px;
203 | }
204 |
205 | // Typeahead
206 | // ---------
207 | .typeahead {
208 | margin-top: 2px; // give it some space to breathe
209 | .border-radius(4px);
210 | }
211 |
--------------------------------------------------------------------------------
/app/assets/css/lib/grid.less:
--------------------------------------------------------------------------------
1 | //
2 | // Grid system
3 | // --------------------------------------------------
4 |
5 |
6 | // Fixed (940px)
7 | #grid > .core(@gridColumnWidth, @gridGutterWidth);
8 |
9 | // Fluid (940px)
10 | #grid > .fluid(@fluidGridColumnWidth, @fluidGridGutterWidth);
11 |
12 | // Reset utility classes due to specificity
13 | [class*="span"].hide,
14 | .row-fluid [class*="span"].hide {
15 | display: none;
16 | }
17 |
18 | [class*="span"].pull-right,
19 | .row-fluid [class*="span"].pull-right {
20 | float: right;
21 | }
22 |
--------------------------------------------------------------------------------
/app/assets/css/lib/hero-unit.less:
--------------------------------------------------------------------------------
1 | //
2 | // Hero unit
3 | // --------------------------------------------------
4 |
5 |
6 | .hero-unit {
7 | padding: 60px;
8 | margin-bottom: 30px;
9 | background-color: @heroUnitBackground;
10 | .border-radius(6px);
11 | h1 {
12 | margin-bottom: 0;
13 | font-size: 60px;
14 | line-height: 1;
15 | color: @heroUnitHeadingColor;
16 | letter-spacing: -1px;
17 | }
18 | p {
19 | font-size: 18px;
20 | font-weight: 200;
21 | line-height: @baseLineHeight * 1.5;
22 | color: @heroUnitLeadColor;
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/app/assets/css/lib/labels-badges.less:
--------------------------------------------------------------------------------
1 | //
2 | // Labels and badges
3 | // --------------------------------------------------
4 |
5 |
6 | // Base classes
7 | .label,
8 | .badge {
9 | font-size: @baseFontSize * .846;
10 | font-weight: bold;
11 | line-height: 14px; // ensure proper line-height if floated
12 | color: @white;
13 | vertical-align: baseline;
14 | white-space: nowrap;
15 | text-shadow: 0 -1px 0 rgba(0,0,0,.25);
16 | background-color: @grayLight;
17 | }
18 | // Set unique padding and border-radii
19 | .label {
20 | padding: 1px 4px 2px;
21 | .border-radius(3px);
22 | }
23 | .badge {
24 | padding: 1px 9px 2px;
25 | .border-radius(9px);
26 | }
27 |
28 | // Hover state, but only for links
29 | a {
30 | &.label:hover,
31 | &.badge:hover {
32 | color: @white;
33 | text-decoration: none;
34 | cursor: pointer;
35 | }
36 | }
37 |
38 | // Colors
39 | // Only give background-color difference to links (and to simplify, we don't qualifty with `a` but [href] attribute)
40 | .label,
41 | .badge {
42 | // Important (red)
43 | &-important { background-color: @errorText; }
44 | &-important[href] { background-color: darken(@errorText, 10%); }
45 | // Warnings (orange)
46 | &-warning { background-color: @orange; }
47 | &-warning[href] { background-color: darken(@orange, 10%); }
48 | // Success (green)
49 | &-success { background-color: @successText; }
50 | &-success[href] { background-color: darken(@successText, 10%); }
51 | // Info (turquoise)
52 | &-info { background-color: @infoText; }
53 | &-info[href] { background-color: darken(@infoText, 10%); }
54 | // Inverse (black)
55 | &-inverse { background-color: @grayDark; }
56 | &-inverse[href] { background-color: darken(@grayDark, 10%); }
57 | }
58 |
59 | // Quick fix for labels/badges in buttons
60 | .btn {
61 | .label,
62 | .badge {
63 | position: relative;
64 | top: -1px;
65 | }
66 | }
67 | .btn-mini {
68 | .label,
69 | .badge {
70 | top: 0;
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/app/assets/css/lib/layouts.less:
--------------------------------------------------------------------------------
1 | //
2 | // Layouts
3 | // --------------------------------------------------
4 |
5 |
6 | // Container (centered, fixed-width layouts)
7 | .container {
8 | .container-fixed();
9 | }
10 |
11 | // Fluid layouts (left aligned, with sidebar, min- & max-width content)
12 | .container-fluid {
13 | padding-right: @gridGutterWidth;
14 | padding-left: @gridGutterWidth;
15 | .clearfix();
16 | }
--------------------------------------------------------------------------------
/app/assets/css/lib/modals.less:
--------------------------------------------------------------------------------
1 | //
2 | // Modals
3 | // --------------------------------------------------
4 |
5 |
6 | // Recalculate z-index where appropriate,
7 | // but only apply to elements within modal
8 | .modal-open .modal {
9 | .dropdown-menu { z-index: @zindexDropdown + @zindexModal; }
10 | .dropdown.open { *z-index: @zindexDropdown + @zindexModal; }
11 | .popover { z-index: @zindexPopover + @zindexModal; }
12 | .tooltip { z-index: @zindexTooltip + @zindexModal; }
13 | }
14 |
15 | // Background
16 | .modal-backdrop {
17 | position: fixed;
18 | top: 0;
19 | right: 0;
20 | bottom: 0;
21 | left: 0;
22 | z-index: @zindexModalBackdrop;
23 | background-color: @black;
24 | // Fade for backdrop
25 | &.fade { opacity: 0; }
26 | }
27 |
28 | .modal-backdrop,
29 | .modal-backdrop.fade.in {
30 | .opacity(80);
31 | }
32 |
33 | // Base modal
34 | .modal {
35 | position: fixed;
36 | top: 50%;
37 | left: 50%;
38 | z-index: @zindexModal;
39 | overflow: auto;
40 | width: 560px;
41 | margin: -250px 0 0 -280px;
42 | background-color: @white;
43 | border: 1px solid #999;
44 | border: 1px solid rgba(0,0,0,.3);
45 | *border: 1px solid #999; /* IE6-7 */
46 | .border-radius(6px);
47 | .box-shadow(0 3px 7px rgba(0,0,0,0.3));
48 | .background-clip(padding-box);
49 | &.fade {
50 | .transition(e('opacity .3s linear, top .3s ease-out'));
51 | top: -25%;
52 | }
53 | &.fade.in { top: 50%; }
54 | }
55 | .modal-header {
56 | padding: 9px 15px;
57 | border-bottom: 1px solid #eee;
58 | // Close icon
59 | .close { margin-top: 2px; }
60 | // Heading
61 | h3 {
62 | margin: 0;
63 | line-height: 30px;
64 | }
65 | }
66 |
67 | // Body (where all modal content resides)
68 | .modal-body {
69 | overflow-y: auto;
70 | max-height: 400px;
71 | padding: 15px;
72 | }
73 | // Remove bottom margin if need be
74 | .modal-form {
75 | margin-bottom: 0;
76 | }
77 |
78 | // Footer (for actions)
79 | .modal-footer {
80 | padding: 14px 15px 15px;
81 | margin-bottom: 0;
82 | text-align: right; // right align buttons
83 | background-color: #f5f5f5;
84 | border-top: 1px solid #ddd;
85 | .border-radius(0 0 6px 6px);
86 | .box-shadow(inset 0 1px 0 @white);
87 | .clearfix(); // clear it in case folks use .pull-* classes on buttons
88 |
89 | // Properly space out buttons
90 | .btn + .btn {
91 | margin-left: 5px;
92 | margin-bottom: 0; // account for input[type="submit"] which gets the bottom margin like all other inputs
93 | }
94 | // but override that for button groups
95 | .btn-group .btn + .btn {
96 | margin-left: -1px;
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/app/assets/css/lib/pager.less:
--------------------------------------------------------------------------------
1 | //
2 | // Pager pagination
3 | // --------------------------------------------------
4 |
5 |
6 | .pager {
7 | margin: @baseLineHeight 0;
8 | list-style: none;
9 | text-align: center;
10 | .clearfix();
11 | }
12 | .pager li {
13 | display: inline;
14 | }
15 | .pager a,
16 | .pager span {
17 | display: inline-block;
18 | padding: 5px 14px;
19 | background-color: #fff;
20 | border: 1px solid #ddd;
21 | .border-radius(15px);
22 | }
23 | .pager a:hover {
24 | text-decoration: none;
25 | background-color: #f5f5f5;
26 | }
27 | .pager .next a,
28 | .pager .next span {
29 | float: right;
30 | }
31 | .pager .previous a {
32 | float: left;
33 | }
34 | .pager .disabled a,
35 | .pager .disabled a:hover,
36 | .pager .disabled span {
37 | color: @grayLight;
38 | background-color: #fff;
39 | cursor: default;
40 | }
--------------------------------------------------------------------------------
/app/assets/css/lib/pagination.less:
--------------------------------------------------------------------------------
1 | //
2 | // Pagination (multiple pages)
3 | // --------------------------------------------------
4 |
5 |
6 | .pagination {
7 | height: @baseLineHeight * 2;
8 | margin: @baseLineHeight 0;
9 | }
10 | .pagination ul {
11 | display: inline-block;
12 | .ie7-inline-block();
13 | margin-left: 0;
14 | margin-bottom: 0;
15 | .border-radius(3px);
16 | .box-shadow(0 1px 2px rgba(0,0,0,.05));
17 | }
18 | .pagination ul > li {
19 | display: inline;
20 | }
21 | .pagination ul > li > a,
22 | .pagination ul > li > span {
23 | float: left;
24 | padding: 0 14px;
25 | line-height: (@baseLineHeight * 2) - 2;
26 | text-decoration: none;
27 | background-color: @paginationBackground;
28 | border: 1px solid @paginationBorder;
29 | border-left-width: 0;
30 | }
31 | .pagination ul > li > a:hover,
32 | .pagination ul > .active > a,
33 | .pagination ul > .active > span {
34 | background-color: #f5f5f5;
35 | }
36 | .pagination ul > .active > a,
37 | .pagination ul > .active > span {
38 | color: @grayLight;
39 | cursor: default;
40 | }
41 | .pagination ul > .disabled > span,
42 | .pagination ul > .disabled > a,
43 | .pagination ul > .disabled > a:hover {
44 | color: @grayLight;
45 | background-color: transparent;
46 | cursor: default;
47 | }
48 | .pagination ul > li:first-child > a,
49 | .pagination ul > li:first-child > span {
50 | border-left-width: 1px;
51 | .border-radius(3px 0 0 3px);
52 | }
53 | .pagination ul > li:last-child > a,
54 | .pagination ul > li:last-child > span {
55 | .border-radius(0 3px 3px 0);
56 | }
57 |
58 | // Centered
59 | .pagination-centered {
60 | text-align: center;
61 | }
62 | .pagination-right {
63 | text-align: right;
64 | }
65 |
--------------------------------------------------------------------------------
/app/assets/css/lib/popovers.less:
--------------------------------------------------------------------------------
1 | //
2 | // Popovers
3 | // --------------------------------------------------
4 |
5 |
6 | .popover {
7 | position: absolute;
8 | top: 0;
9 | left: 0;
10 | z-index: @zindexPopover;
11 | display: none;
12 | width: 236px;
13 | padding: 1px;
14 | background-color: @popoverBackground;
15 | -webkit-background-clip: padding-box;
16 | -moz-background-clip: padding;
17 | background-clip: padding-box;
18 | border: 1px solid #ccc;
19 | border: 1px solid rgba(0,0,0,.2);
20 | .border-radius(6px);
21 | .box-shadow(0 5px 10px rgba(0,0,0,.2));
22 |
23 | // Offset the popover to account for the popover arrow
24 | &.top { margin-bottom: 10px; }
25 | &.right { margin-left: 10px; }
26 | &.bottom { margin-top: 10px; }
27 | &.left { margin-right: 10px; }
28 |
29 | }
30 |
31 | .popover-title {
32 | margin: 0; // reset heading margin
33 | padding: 8px 14px;
34 | font-size: 14px;
35 | font-weight: normal;
36 | line-height: 18px;
37 | background-color: @popoverTitleBackground;
38 | border-bottom: 1px solid darken(@popoverTitleBackground, 5%);
39 | .border-radius(5px 5px 0 0);
40 | }
41 |
42 | .popover-content {
43 | padding: 9px 14px;
44 | p, ul, ol {
45 | margin-bottom: 0;
46 | }
47 | }
48 |
49 | // Arrows
50 | .popover .arrow,
51 | .popover .arrow:after {
52 | position: absolute;
53 | display: inline-block;
54 | width: 0;
55 | height: 0;
56 | border-color: transparent;
57 | border-style: solid;
58 | }
59 | .popover .arrow:after {
60 | content: "";
61 | z-index: -1;
62 | }
63 |
64 | .popover {
65 | &.top .arrow {
66 | bottom: -@popoverArrowWidth;
67 | left: 50%;
68 | margin-left: -@popoverArrowWidth;
69 | border-width: @popoverArrowWidth @popoverArrowWidth 0;
70 | border-top-color: @popoverArrowColor;
71 | &:after {
72 | border-width: @popoverArrowOuterWidth @popoverArrowOuterWidth 0;
73 | border-top-color: @popoverArrowOuterColor;
74 | bottom: -1px;
75 | left: -@popoverArrowOuterWidth;
76 | }
77 | }
78 | &.right .arrow {
79 | top: 50%;
80 | left: -@popoverArrowWidth;
81 | margin-top: -@popoverArrowWidth;
82 | border-width: @popoverArrowWidth @popoverArrowWidth @popoverArrowWidth 0;
83 | border-right-color: @popoverArrowColor;
84 | &:after {
85 | border-width: @popoverArrowOuterWidth @popoverArrowOuterWidth @popoverArrowOuterWidth 0;
86 | border-right-color: @popoverArrowOuterColor;
87 | bottom: -@popoverArrowOuterWidth;
88 | left: -1px;
89 | }
90 | }
91 | &.bottom .arrow {
92 | top: -@popoverArrowWidth;
93 | left: 50%;
94 | margin-left: -@popoverArrowWidth;
95 | border-width: 0 @popoverArrowWidth @popoverArrowWidth;
96 | border-bottom-color: @popoverArrowColor;
97 | &:after {
98 | border-width: 0 @popoverArrowOuterWidth @popoverArrowOuterWidth;
99 | border-bottom-color: @popoverArrowOuterColor;
100 | top: -1px;
101 | left: -@popoverArrowOuterWidth;
102 | }
103 | }
104 | &.left .arrow {
105 | top: 50%;
106 | right: -@popoverArrowWidth;
107 | margin-top: -@popoverArrowWidth;
108 | border-width: @popoverArrowWidth 0 @popoverArrowWidth @popoverArrowWidth;
109 | border-left-color: @popoverArrowColor;
110 | &:after {
111 | border-width: @popoverArrowOuterWidth 0 @popoverArrowOuterWidth @popoverArrowOuterWidth;
112 | border-left-color: @popoverArrowOuterColor;
113 | bottom: -@popoverArrowOuterWidth;
114 | right: -1px;
115 | }
116 | }
117 | }
118 |
--------------------------------------------------------------------------------
/app/assets/css/lib/progress-bars.less:
--------------------------------------------------------------------------------
1 | //
2 | // Progress bars
3 | // --------------------------------------------------
4 |
5 |
6 | // ANIMATIONS
7 | // ----------
8 |
9 | // Webkit
10 | @-webkit-keyframes progress-bar-stripes {
11 | from { background-position: 40px 0; }
12 | to { background-position: 0 0; }
13 | }
14 |
15 | // Firefox
16 | @-moz-keyframes progress-bar-stripes {
17 | from { background-position: 40px 0; }
18 | to { background-position: 0 0; }
19 | }
20 |
21 | // IE9
22 | @-ms-keyframes progress-bar-stripes {
23 | from { background-position: 40px 0; }
24 | to { background-position: 0 0; }
25 | }
26 |
27 | // Opera
28 | @-o-keyframes progress-bar-stripes {
29 | from { background-position: 0 0; }
30 | to { background-position: 40px 0; }
31 | }
32 |
33 | // Spec
34 | @keyframes progress-bar-stripes {
35 | from { background-position: 40px 0; }
36 | to { background-position: 0 0; }
37 | }
38 |
39 |
40 |
41 | // THE BARS
42 | // --------
43 |
44 | // Outer container
45 | .progress {
46 | overflow: hidden;
47 | height: @baseLineHeight;
48 | margin-bottom: @baseLineHeight;
49 | #gradient > .vertical(#f5f5f5, #f9f9f9);
50 | .box-shadow(inset 0 1px 2px rgba(0,0,0,.1));
51 | .border-radius(4px);
52 | }
53 |
54 | // Bar of progress
55 | .progress .bar {
56 | width: 0%;
57 | height: 100%;
58 | color: @white;
59 | float: left;
60 | font-size: 12px;
61 | text-align: center;
62 | text-shadow: 0 -1px 0 rgba(0,0,0,.25);
63 | #gradient > .vertical(#149bdf, #0480be);
64 | .box-shadow(inset 0 -1px 0 rgba(0,0,0,.15));
65 | .box-sizing(border-box);
66 | .transition(width .6s ease);
67 | }
68 | .progress .bar + .bar {
69 | .box-shadow(inset 1px 0 0 rgba(0,0,0,.15), inset 0 -1px 0 rgba(0,0,0,.15));
70 | }
71 |
72 | // Striped bars
73 | .progress-striped .bar {
74 | #gradient > .striped(#149bdf);
75 | .background-size(40px 40px);
76 | }
77 |
78 | // Call animation for the active one
79 | .progress.active .bar {
80 | -webkit-animation: progress-bar-stripes 2s linear infinite;
81 | -moz-animation: progress-bar-stripes 2s linear infinite;
82 | -ms-animation: progress-bar-stripes 2s linear infinite;
83 | -o-animation: progress-bar-stripes 2s linear infinite;
84 | animation: progress-bar-stripes 2s linear infinite;
85 | }
86 |
87 |
88 |
89 | // COLORS
90 | // ------
91 |
92 | // Danger (red)
93 | .progress-danger .bar, .progress .bar-danger {
94 | #gradient > .vertical(#ee5f5b, #c43c35);
95 | }
96 | .progress-danger.progress-striped .bar, .progress-striped .bar-danger {
97 | #gradient > .striped(#ee5f5b);
98 | }
99 |
100 | // Success (green)
101 | .progress-success .bar, .progress .bar-success {
102 | #gradient > .vertical(#62c462, #57a957);
103 | }
104 | .progress-success.progress-striped .bar, .progress-striped .bar-success {
105 | #gradient > .striped(#62c462);
106 | }
107 |
108 | // Info (teal)
109 | .progress-info .bar, .progress .bar-info {
110 | #gradient > .vertical(#5bc0de, #339bb9);
111 | }
112 | .progress-info.progress-striped .bar, .progress-striped .bar-info {
113 | #gradient > .striped(#5bc0de);
114 | }
115 |
116 | // Warning (orange)
117 | .progress-warning .bar, .progress .bar-warning {
118 | #gradient > .vertical(lighten(@orange, 15%), @orange);
119 | }
120 | .progress-warning.progress-striped .bar, .progress-striped .bar-warning {
121 | #gradient > .striped(lighten(@orange, 15%));
122 | }
123 |
--------------------------------------------------------------------------------
/app/assets/css/lib/reset.less:
--------------------------------------------------------------------------------
1 | //
2 | // Modals
3 | // Adapted from http://github.com/necolas/normalize.css
4 | // --------------------------------------------------
5 |
6 |
7 | // Display in IE6-9 and FF3
8 | // -------------------------
9 |
10 | article,
11 | aside,
12 | details,
13 | figcaption,
14 | figure,
15 | footer,
16 | header,
17 | hgroup,
18 | nav,
19 | section {
20 | display: block;
21 | }
22 |
23 | // Display block in IE6-9 and FF3
24 | // -------------------------
25 |
26 | audio,
27 | canvas,
28 | video {
29 | display: inline-block;
30 | *display: inline;
31 | *zoom: 1;
32 | }
33 |
34 | // Prevents modern browsers from displaying 'audio' without controls
35 | // -------------------------
36 |
37 | audio:not([controls]) {
38 | display: none;
39 | }
40 |
41 | // Base settings
42 | // -------------------------
43 |
44 | html {
45 | font-size: 100%;
46 | -webkit-text-size-adjust: 100%;
47 | -ms-text-size-adjust: 100%;
48 | }
49 | // Focus states
50 | a:focus {
51 | .tab-focus();
52 | }
53 | // Hover & Active
54 | a:hover,
55 | a:active {
56 | outline: 0;
57 | }
58 |
59 | // Prevents sub and sup affecting line-height in all browsers
60 | // -------------------------
61 |
62 | sub,
63 | sup {
64 | position: relative;
65 | font-size: 75%;
66 | line-height: 0;
67 | vertical-align: baseline;
68 | }
69 | sup {
70 | top: -0.5em;
71 | }
72 | sub {
73 | bottom: -0.25em;
74 | }
75 |
76 | // Img border in a's and image quality
77 | // -------------------------
78 |
79 | img {
80 | /* Responsive images (ensure images don't scale beyond their parents) */
81 | max-width: 100%; /* Part 1: Set a maxium relative to the parent */
82 | width: auto\9; /* IE7-8 need help adjusting responsive images */
83 | height: auto; /* Part 2: Scale the height according to the width, otherwise you get stretching */
84 |
85 | vertical-align: middle;
86 | border: 0;
87 | -ms-interpolation-mode: bicubic;
88 | }
89 |
90 | // Prevent max-width from affecting Google Maps
91 | #map_canvas img {
92 | max-width: none;
93 | }
94 |
95 | // Forms
96 | // -------------------------
97 |
98 | // Font size in all browsers, margin changes, misc consistency
99 | button,
100 | input,
101 | select,
102 | textarea {
103 | margin: 0;
104 | font-size: 100%;
105 | vertical-align: middle;
106 | }
107 | button,
108 | input {
109 | *overflow: visible; // Inner spacing ie IE6/7
110 | line-height: normal; // FF3/4 have !important on line-height in UA stylesheet
111 | }
112 | button::-moz-focus-inner,
113 | input::-moz-focus-inner { // Inner padding and border oddities in FF3/4
114 | padding: 0;
115 | border: 0;
116 | }
117 | button,
118 | input[type="button"],
119 | input[type="reset"],
120 | input[type="submit"] {
121 | cursor: pointer; // Cursors on all buttons applied consistently
122 | -webkit-appearance: button; // Style clickable inputs in iOS
123 | }
124 | input[type="search"] { // Appearance in Safari/Chrome
125 | -webkit-box-sizing: content-box;
126 | -moz-box-sizing: content-box;
127 | box-sizing: content-box;
128 | -webkit-appearance: textfield;
129 | }
130 | input[type="search"]::-webkit-search-decoration,
131 | input[type="search"]::-webkit-search-cancel-button {
132 | -webkit-appearance: none; // Inner-padding issues in Chrome OSX, Safari 5
133 | }
134 | textarea {
135 | overflow: auto; // Remove vertical scrollbar in IE6-9
136 | vertical-align: top; // Readability and alignment cross-browser
137 | }
138 |
--------------------------------------------------------------------------------
/app/assets/css/lib/responsive-1200px-min.less:
--------------------------------------------------------------------------------
1 | //
2 | // Responsive: Large desktop and up
3 | // --------------------------------------------------
4 |
5 |
6 | @media (min-width: 1200px) {
7 |
8 | // Fixed grid
9 | #grid > .core(@gridColumnWidth1200, @gridGutterWidth1200);
10 |
11 | // Fluid grid
12 | #grid > .fluid(@fluidGridColumnWidth1200, @fluidGridGutterWidth1200);
13 |
14 | // Input grid
15 | #grid > .input(@gridColumnWidth1200, @gridGutterWidth1200);
16 |
17 | // Thumbnails
18 | .thumbnails {
19 | margin-left: -@gridGutterWidth1200;
20 | }
21 | .thumbnails > li {
22 | margin-left: @gridGutterWidth1200;
23 | }
24 | .row-fluid .thumbnails {
25 | margin-left: 0;
26 | }
27 |
28 | }
29 |
--------------------------------------------------------------------------------
/app/assets/css/lib/responsive-767px-max.less:
--------------------------------------------------------------------------------
1 | //
2 | // Responsive: Landscape phone to desktop/tablet
3 | // --------------------------------------------------
4 |
5 |
6 | @media (max-width: 767px) {
7 |
8 | // Padding to set content in a bit
9 | body {
10 | padding-left: 20px;
11 | padding-right: 20px;
12 | }
13 | // Negative indent the now static "fixed" navbar
14 | .navbar-fixed-top,
15 | .navbar-fixed-bottom,
16 | .navbar-static-top {
17 | margin-left: -20px;
18 | margin-right: -20px;
19 | }
20 | // Remove padding on container given explicit padding set on body
21 | .container-fluid {
22 | padding: 0;
23 | }
24 |
25 | // TYPOGRAPHY
26 | // ----------
27 | // Reset horizontal dl
28 | .dl-horizontal {
29 | dt {
30 | float: none;
31 | clear: none;
32 | width: auto;
33 | text-align: left;
34 | }
35 | dd {
36 | margin-left: 0;
37 | }
38 | }
39 |
40 | // GRID & CONTAINERS
41 | // -----------------
42 | // Remove width from containers
43 | .container {
44 | width: auto;
45 | }
46 | // Fluid rows
47 | .row-fluid {
48 | width: 100%;
49 | }
50 | // Undo negative margin on rows and thumbnails
51 | .row,
52 | .thumbnails {
53 | margin-left: 0;
54 | }
55 | .thumbnails > li {
56 | float: none;
57 | margin-left: 0; // Reset the default margin for all li elements when no .span* classes are present
58 | }
59 | // Make all grid-sized elements block level again
60 | [class*="span"],
61 | .row-fluid [class*="span"] {
62 | float: none;
63 | display: block;
64 | width: 100%;
65 | margin-left: 0;
66 | .box-sizing(border-box);
67 | }
68 | .span12,
69 | .row-fluid .span12 {
70 | width: 100%;
71 | .box-sizing(border-box);
72 | }
73 |
74 | // FORM FIELDS
75 | // -----------
76 | // Make span* classes full width
77 | .input-large,
78 | .input-xlarge,
79 | .input-xxlarge,
80 | input[class*="span"],
81 | select[class*="span"],
82 | textarea[class*="span"],
83 | .uneditable-input {
84 | .input-block-level();
85 | }
86 | // But don't let it screw up prepend/append inputs
87 | .input-prepend input,
88 | .input-append input,
89 | .input-prepend input[class*="span"],
90 | .input-append input[class*="span"] {
91 | display: inline-block; // redeclare so they don't wrap to new lines
92 | width: auto;
93 | }
94 | .controls-row [class*="span"] + [class*="span"] {
95 | margin-left: 0;
96 | }
97 |
98 | // Modals
99 | .modal {
100 | position: fixed;
101 | top: 20px;
102 | left: 20px;
103 | right: 20px;
104 | width: auto;
105 | margin: 0;
106 | &.fade.in { top: auto; }
107 | }
108 |
109 | }
110 |
111 |
112 |
113 | // UP TO LANDSCAPE PHONE
114 | // ---------------------
115 |
116 | @media (max-width: 480px) {
117 |
118 | // Smooth out the collapsing/expanding nav
119 | .nav-collapse {
120 | -webkit-transform: translate3d(0, 0, 0); // activate the GPU
121 | }
122 |
123 | // Block level the page header small tag for readability
124 | .page-header h1 small {
125 | display: block;
126 | line-height: @baseLineHeight;
127 | }
128 |
129 | // Update checkboxes for iOS
130 | input[type="checkbox"],
131 | input[type="radio"] {
132 | border: 1px solid #ccc;
133 | }
134 |
135 | // Remove the horizontal form styles
136 | .form-horizontal {
137 | .control-label {
138 | float: none;
139 | width: auto;
140 | padding-top: 0;
141 | text-align: left;
142 | }
143 | // Move over all input controls and content
144 | .controls {
145 | margin-left: 0;
146 | }
147 | // Move the options list down to align with labels
148 | .control-list {
149 | padding-top: 0; // has to be padding because margin collaspes
150 | }
151 | // Move over buttons in .form-actions to align with .controls
152 | .form-actions {
153 | padding-left: 10px;
154 | padding-right: 10px;
155 | }
156 | }
157 |
158 | // Modals
159 | .modal {
160 | top: 10px;
161 | left: 10px;
162 | right: 10px;
163 | }
164 | .modal-header .close {
165 | padding: 10px;
166 | margin: -10px;
167 | }
168 |
169 | // Carousel
170 | .carousel-caption {
171 | position: static;
172 | }
173 |
174 | }
175 |
--------------------------------------------------------------------------------
/app/assets/css/lib/responsive-768px-979px.less:
--------------------------------------------------------------------------------
1 | //
2 | // Responsive: Tablet to desktop
3 | // --------------------------------------------------
4 |
5 |
6 | @media (min-width: 768px) and (max-width: 979px) {
7 |
8 | // Fixed grid
9 | #grid > .core(@gridColumnWidth768, @gridGutterWidth768);
10 |
11 | // Fluid grid
12 | #grid > .fluid(@fluidGridColumnWidth768, @fluidGridGutterWidth768);
13 |
14 | // Input grid
15 | #grid > .input(@gridColumnWidth768, @gridGutterWidth768);
16 |
17 | // No need to reset .thumbnails here since it's the same @gridGutterWidth
18 |
19 | }
20 |
--------------------------------------------------------------------------------
/app/assets/css/lib/responsive-navbar.less:
--------------------------------------------------------------------------------
1 | //
2 | // Responsive: Navbar
3 | // --------------------------------------------------
4 |
5 |
6 | // TABLETS AND BELOW
7 | // -----------------
8 | @media (max-width: @navbarCollapseWidth) {
9 |
10 | // UNFIX THE TOPBAR
11 | // ----------------
12 | // Remove any padding from the body
13 | body {
14 | padding-top: 0;
15 | }
16 | // Unfix the navbars
17 | .navbar-fixed-top,
18 | .navbar-fixed-bottom {
19 | position: static;
20 | }
21 | .navbar-fixed-top {
22 | margin-bottom: @baseLineHeight;
23 | }
24 | .navbar-fixed-bottom {
25 | margin-top: @baseLineHeight;
26 | }
27 | .navbar-fixed-top .navbar-inner,
28 | .navbar-fixed-bottom .navbar-inner {
29 | padding: 5px;
30 | }
31 | .navbar .container {
32 | width: auto;
33 | padding: 0;
34 | }
35 | // Account for brand name
36 | .navbar .brand {
37 | padding-left: 10px;
38 | padding-right: 10px;
39 | margin: 0 0 0 -5px;
40 | }
41 |
42 | // COLLAPSIBLE NAVBAR
43 | // ------------------
44 | // Nav collapse clears brand
45 | .nav-collapse {
46 | clear: both;
47 | }
48 | // Block-level the nav
49 | .nav-collapse .nav {
50 | float: none;
51 | margin: 0 0 (@baseLineHeight / 2);
52 | }
53 | .nav-collapse .nav > li {
54 | float: none;
55 | }
56 | .nav-collapse .nav > li > a {
57 | margin-bottom: 2px;
58 | }
59 | .nav-collapse .nav > .divider-vertical {
60 | display: none;
61 | }
62 | .nav-collapse .nav .nav-header {
63 | color: @navbarText;
64 | text-shadow: none;
65 | }
66 | // Nav and dropdown links in navbar
67 | .nav-collapse .nav > li > a,
68 | .nav-collapse .dropdown-menu a {
69 | padding: 9px 15px;
70 | font-weight: bold;
71 | color: @navbarLinkColor;
72 | .border-radius(3px);
73 | }
74 | // Buttons
75 | .nav-collapse .btn {
76 | padding: 4px 10px 4px;
77 | font-weight: normal;
78 | .border-radius(4px);
79 | }
80 | .nav-collapse .dropdown-menu li + li a {
81 | margin-bottom: 2px;
82 | }
83 | .nav-collapse .nav > li > a:hover,
84 | .nav-collapse .dropdown-menu a:hover {
85 | background-color: @navbarBackground;
86 | }
87 | .navbar-inverse .nav-collapse .nav > li > a:hover,
88 | .navbar-inverse .nav-collapse .dropdown-menu a:hover {
89 | background-color: @navbarInverseBackground;
90 | }
91 | // Buttons in the navbar
92 | .nav-collapse.in .btn-group {
93 | margin-top: 5px;
94 | padding: 0;
95 | }
96 | // Dropdowns in the navbar
97 | .nav-collapse .dropdown-menu {
98 | position: static;
99 | top: auto;
100 | left: auto;
101 | float: none;
102 | display: block;
103 | max-width: none;
104 | margin: 0 15px;
105 | padding: 0;
106 | background-color: transparent;
107 | border: none;
108 | .border-radius(0);
109 | .box-shadow(none);
110 | }
111 | .nav-collapse .dropdown-menu:before,
112 | .nav-collapse .dropdown-menu:after {
113 | display: none;
114 | }
115 | .nav-collapse .dropdown-menu .divider {
116 | display: none;
117 | }
118 | .nav-collapse .nav > li > .dropdown-menu {
119 | &:before,
120 | &:after {
121 | display: none;
122 | }
123 | }
124 | // Forms in navbar
125 | .nav-collapse .navbar-form,
126 | .nav-collapse .navbar-search {
127 | float: none;
128 | padding: (@baseLineHeight / 2) 15px;
129 | margin: (@baseLineHeight / 2) 0;
130 | border-top: 1px solid @navbarBackground;
131 | border-bottom: 1px solid @navbarBackground;
132 | .box-shadow(inset 0 1px 0 rgba(255,255,255,.1), 0 1px 0 rgba(255,255,255,.1));
133 | }
134 | .navbar-inverse .nav-collapse .navbar-form,
135 | .navbar-inverse .nav-collapse .navbar-search {
136 | border-top-color: @navbarInverseBackground;
137 | border-bottom-color: @navbarInverseBackground;
138 | }
139 | // Pull right (secondary) nav content
140 | .navbar .nav-collapse .nav.pull-right {
141 | float: none;
142 | margin-left: 0;
143 | }
144 | // Hide everything in the navbar save .brand and toggle button */
145 | .nav-collapse,
146 | .nav-collapse.collapse {
147 | overflow: hidden;
148 | height: 0;
149 | }
150 | // Navbar button
151 | .navbar .btn-navbar {
152 | display: block;
153 | }
154 |
155 | // STATIC NAVBAR
156 | // -------------
157 | .navbar-static .navbar-inner {
158 | padding-left: 10px;
159 | padding-right: 10px;
160 | }
161 |
162 |
163 | }
164 |
165 |
166 | // DEFAULT DESKTOP
167 | // ---------------
168 |
169 | @media (min-width: 980px) {
170 |
171 | // Required to make the collapsing navbar work on regular desktops
172 | .nav-collapse.collapse {
173 | height: auto !important;
174 | overflow: visible !important;
175 | }
176 |
177 | }
178 |
--------------------------------------------------------------------------------
/app/assets/css/lib/responsive-utilities.less:
--------------------------------------------------------------------------------
1 | //
2 | // Responsive: Utility classes
3 | // --------------------------------------------------
4 |
5 |
6 | // Hide from screenreaders and browsers
7 | // Credit: HTML5 Boilerplate
8 | .hidden {
9 | display: none;
10 | visibility: hidden;
11 | }
12 |
13 | // Visibility utilities
14 |
15 | // For desktops
16 | .visible-phone { display: none !important; }
17 | .visible-tablet { display: none !important; }
18 | .hidden-phone { }
19 | .hidden-tablet { }
20 | .hidden-desktop { display: none !important; }
21 | .visible-desktop { display: inherit !important; }
22 |
23 | // Tablets & small desktops only
24 | @media (min-width: 768px) and (max-width: 979px) {
25 | // Hide everything else
26 | .hidden-desktop { display: inherit !important; }
27 | .visible-desktop { display: none !important ; }
28 | // Show
29 | .visible-tablet { display: inherit !important; }
30 | // Hide
31 | .hidden-tablet { display: none !important; }
32 | }
33 |
34 | // Phones only
35 | @media (max-width: 767px) {
36 | // Hide everything else
37 | .hidden-desktop { display: inherit !important; }
38 | .visible-desktop { display: none !important; }
39 | // Show
40 | .visible-phone { display: inherit !important; } // Use inherit to restore previous behavior
41 | // Hide
42 | .hidden-phone { display: none !important; }
43 | }
44 |
--------------------------------------------------------------------------------
/app/assets/css/lib/responsive.less:
--------------------------------------------------------------------------------
1 | /*!
2 | * Bootstrap Responsive v2.1.1
3 | *
4 | * Copyright 2012 Twitter, Inc
5 | * Licensed under the Apache License v2.0
6 | * http://www.apache.org/licenses/LICENSE-2.0
7 | *
8 | * Designed and built with all the love in the world @twitter by @mdo and @fat.
9 | */
10 |
11 |
12 | // Responsive.less
13 | // For phone and tablet devices
14 | // -------------------------------------------------------------
15 |
16 |
17 | // REPEAT VARIABLES & MIXINS
18 | // -------------------------
19 | // Required since we compile the responsive stuff separately
20 |
21 | @import "variables.less"; // Modify this for custom colors, font-sizes, etc
22 | @import "mixins.less";
23 |
24 |
25 | // RESPONSIVE CLASSES
26 | // ------------------
27 |
28 | @import "responsive-utilities.less";
29 |
30 |
31 | // MEDIA QUERIES
32 | // ------------------
33 |
34 | // Large desktops
35 | @import "responsive-1200px-min.less";
36 |
37 | // Tablets to regular desktops
38 | @import "responsive-768px-979px.less";
39 |
40 | // Phones to portrait tablets and narrow desktops
41 | @import "responsive-767px-max.less";
42 |
43 |
44 | // RESPONSIVE NAVBAR
45 | // ------------------
46 |
47 | // From 979px and below, show a button to toggle navbar contents
48 | @import "responsive-navbar.less";
49 |
--------------------------------------------------------------------------------
/app/assets/css/lib/scaffolding.less:
--------------------------------------------------------------------------------
1 | //
2 | // Scaffolding
3 | // --------------------------------------------------
4 |
5 |
6 | // Body reset
7 | // -------------------------
8 |
9 | body {
10 | margin: 0;
11 | font-family: @baseFontFamily;
12 | font-size: @baseFontSize;
13 | line-height: @baseLineHeight;
14 | color: @textColor;
15 | background-color: @bodyBackground;
16 | }
17 |
18 |
19 | // Links
20 | // -------------------------
21 |
22 | a {
23 | color: @linkColor;
24 | text-decoration: none;
25 | }
26 | a:hover {
27 | color: @linkColorHover;
28 | text-decoration: underline;
29 | }
30 |
31 |
32 | // Images
33 | // -------------------------
34 |
35 | // Rounded corners
36 | .img-rounded {
37 | .border-radius(6px);
38 | }
39 |
40 | // Add polaroid-esque trim
41 | .img-polaroid {
42 | padding: 4px;
43 | background-color: #fff;
44 | border: 1px solid #ccc;
45 | border: 1px solid rgba(0,0,0,.2);
46 | .box-shadow(0 1px 3px rgba(0,0,0,.1));
47 | }
48 |
49 | // Perfect circle
50 | .img-circle {
51 | .border-radius(500px); // crank the border-radius so it works with most reasonably sized images
52 | }
53 |
--------------------------------------------------------------------------------
/app/assets/css/lib/tables.less:
--------------------------------------------------------------------------------
1 | //
2 | // Tables
3 | // --------------------------------------------------
4 |
5 |
6 | // BASE TABLES
7 | // -----------------
8 |
9 | table {
10 | max-width: 100%;
11 | background-color: @tableBackground;
12 | border-collapse: collapse;
13 | border-spacing: 0;
14 | }
15 |
16 | // BASELINE STYLES
17 | // ---------------
18 |
19 | .table {
20 | width: 100%;
21 | margin-bottom: @baseLineHeight;
22 | // Cells
23 | th,
24 | td {
25 | padding: 8px;
26 | line-height: @baseLineHeight;
27 | text-align: left;
28 | vertical-align: top;
29 | border-top: 1px solid @tableBorder;
30 | }
31 | th {
32 | font-weight: bold;
33 | }
34 | // Bottom align for column headings
35 | thead th {
36 | vertical-align: bottom;
37 | }
38 | // Remove top border from thead by default
39 | caption + thead tr:first-child th,
40 | caption + thead tr:first-child td,
41 | colgroup + thead tr:first-child th,
42 | colgroup + thead tr:first-child td,
43 | thead:first-child tr:first-child th,
44 | thead:first-child tr:first-child td {
45 | border-top: 0;
46 | }
47 | // Account for multiple tbody instances
48 | tbody + tbody {
49 | border-top: 2px solid @tableBorder;
50 | }
51 | }
52 |
53 |
54 |
55 | // CONDENSED TABLE W/ HALF PADDING
56 | // -------------------------------
57 |
58 | .table-condensed {
59 | th,
60 | td {
61 | padding: 4px 5px;
62 | }
63 | }
64 |
65 |
66 | // BORDERED VERSION
67 | // ----------------
68 |
69 | .table-bordered {
70 | border: 1px solid @tableBorder;
71 | border-collapse: separate; // Done so we can round those corners!
72 | *border-collapse: collapse; // IE7 can't round corners anyway
73 | border-left: 0;
74 | .border-radius(4px);
75 | th,
76 | td {
77 | border-left: 1px solid @tableBorder;
78 | }
79 | // Prevent a double border
80 | caption + thead tr:first-child th,
81 | caption + tbody tr:first-child th,
82 | caption + tbody tr:first-child td,
83 | colgroup + thead tr:first-child th,
84 | colgroup + tbody tr:first-child th,
85 | colgroup + tbody tr:first-child td,
86 | thead:first-child tr:first-child th,
87 | tbody:first-child tr:first-child th,
88 | tbody:first-child tr:first-child td {
89 | border-top: 0;
90 | }
91 | // For first th or td in the first row in the first thead or tbody
92 | thead:first-child tr:first-child th:first-child,
93 | tbody:first-child tr:first-child td:first-child {
94 | -webkit-border-top-left-radius: 4px;
95 | border-top-left-radius: 4px;
96 | -moz-border-radius-topleft: 4px;
97 | }
98 | thead:first-child tr:first-child th:last-child,
99 | tbody:first-child tr:first-child td:last-child {
100 | -webkit-border-top-right-radius: 4px;
101 | border-top-right-radius: 4px;
102 | -moz-border-radius-topright: 4px;
103 | }
104 | // For first th or td in the first row in the first thead or tbody
105 | thead:last-child tr:last-child th:first-child,
106 | tbody:last-child tr:last-child td:first-child,
107 | tfoot:last-child tr:last-child td:first-child {
108 | .border-radius(0 0 0 4px);
109 | -webkit-border-bottom-left-radius: 4px;
110 | border-bottom-left-radius: 4px;
111 | -moz-border-radius-bottomleft: 4px;
112 | }
113 | thead:last-child tr:last-child th:last-child,
114 | tbody:last-child tr:last-child td:last-child,
115 | tfoot:last-child tr:last-child td:last-child {
116 | -webkit-border-bottom-right-radius: 4px;
117 | border-bottom-right-radius: 4px;
118 | -moz-border-radius-bottomright: 4px;
119 | }
120 |
121 | // Special fixes to round the left border on the first td/th
122 | caption + thead tr:first-child th:first-child,
123 | caption + tbody tr:first-child td:first-child,
124 | colgroup + thead tr:first-child th:first-child,
125 | colgroup + tbody tr:first-child td:first-child {
126 | -webkit-border-top-left-radius: 4px;
127 | border-top-left-radius: 4px;
128 | -moz-border-radius-topleft: 4px;
129 | }
130 | caption + thead tr:first-child th:last-child,
131 | caption + tbody tr:first-child td:last-child,
132 | colgroup + thead tr:first-child th:last-child,
133 | colgroup + tbody tr:first-child td:last-child {
134 | -webkit-border-top-right-radius: 4px;
135 | border-top-right-radius: 4px;
136 | -moz-border-radius-topleft: 4px;
137 | }
138 |
139 | }
140 |
141 |
142 |
143 |
144 | // ZEBRA-STRIPING
145 | // --------------
146 |
147 | // Default zebra-stripe styles (alternating gray and transparent backgrounds)
148 | .table-striped {
149 | tbody {
150 | tr:nth-child(odd) td,
151 | tr:nth-child(odd) th {
152 | background-color: @tableBackgroundAccent;
153 | }
154 | }
155 | }
156 |
157 |
158 | // HOVER EFFECT
159 | // ------------
160 | // Placed here since it has to come after the potential zebra striping
161 | .table-hover {
162 | tbody {
163 | tr:hover td,
164 | tr:hover th {
165 | background-color: @tableBackgroundHover;
166 | }
167 | }
168 | }
169 |
170 |
171 | // TABLE CELL SIZING
172 | // -----------------
173 |
174 | // Reset default grid behavior
175 | table [class*=span],
176 | .row-fluid table [class*=span] {
177 | display: table-cell;
178 | float: none; // undo default grid column styles
179 | margin-left: 0; // undo default grid column styles
180 | }
181 |
182 | // Change the column widths to account for td/th padding
183 | .table {
184 | .span1 { .tableColumns(1); }
185 | .span2 { .tableColumns(2); }
186 | .span3 { .tableColumns(3); }
187 | .span4 { .tableColumns(4); }
188 | .span5 { .tableColumns(5); }
189 | .span6 { .tableColumns(6); }
190 | .span7 { .tableColumns(7); }
191 | .span8 { .tableColumns(8); }
192 | .span9 { .tableColumns(9); }
193 | .span10 { .tableColumns(10); }
194 | .span11 { .tableColumns(11); }
195 | .span12 { .tableColumns(12); }
196 | .span13 { .tableColumns(13); }
197 | .span14 { .tableColumns(14); }
198 | .span15 { .tableColumns(15); }
199 | .span16 { .tableColumns(16); }
200 | .span17 { .tableColumns(17); }
201 | .span18 { .tableColumns(18); }
202 | .span19 { .tableColumns(19); }
203 | .span20 { .tableColumns(20); }
204 | .span21 { .tableColumns(21); }
205 | .span22 { .tableColumns(22); }
206 | .span23 { .tableColumns(23); }
207 | .span24 { .tableColumns(24); }
208 | }
209 |
210 |
211 |
212 | // TABLE BACKGROUNDS
213 | // -----------------
214 | // Exact selectors below required to override .table-striped
215 |
216 | .table tbody tr {
217 | &.success td {
218 | background-color: @successBackground;
219 | }
220 | &.error td {
221 | background-color: @errorBackground;
222 | }
223 | &.warning td {
224 | background-color: @warningBackground;
225 | }
226 | &.info td {
227 | background-color: @infoBackground;
228 | }
229 | }
230 |
231 | // Hover states for .table-hover
232 | .table-hover tbody tr {
233 | &.success:hover td {
234 | background-color: darken(@successBackground, 5%);
235 | }
236 | &.error:hover td {
237 | background-color: darken(@errorBackground, 5%);
238 | }
239 | &.warning:hover td {
240 | background-color: darken(@warningBackground, 5%);
241 | }
242 | &.info:hover td {
243 | background-color: darken(@infoBackground, 5%);
244 | }
245 | }
246 |
--------------------------------------------------------------------------------
/app/assets/css/lib/thumbnails.less:
--------------------------------------------------------------------------------
1 | //
2 | // Thumbnails
3 | // --------------------------------------------------
4 |
5 |
6 | // Note: `.thumbnails` and `.thumbnails > li` are overriden in responsive files
7 |
8 | // Make wrapper ul behave like the grid
9 | .thumbnails {
10 | margin-left: -@gridGutterWidth;
11 | list-style: none;
12 | .clearfix();
13 | }
14 | // Fluid rows have no left margin
15 | .row-fluid .thumbnails {
16 | margin-left: 0;
17 | }
18 |
19 | // Float li to make thumbnails appear in a row
20 | .thumbnails > li {
21 | float: left; // Explicity set the float since we don't require .span* classes
22 | margin-bottom: @baseLineHeight;
23 | margin-left: @gridGutterWidth;
24 | }
25 |
26 | // The actual thumbnail (can be `a` or `div`)
27 | .thumbnail {
28 | display: block;
29 | padding: 4px;
30 | line-height: @baseLineHeight;
31 | border: 1px solid #ddd;
32 | .border-radius(4px);
33 | .box-shadow(0 1px 3px rgba(0,0,0,.055));
34 | .transition(all .2s ease-in-out);
35 | }
36 | // Add a hover state for linked versions only
37 | a.thumbnail:hover {
38 | border-color: @linkColor;
39 | .box-shadow(0 1px 4px rgba(0,105,214,.25));
40 | }
41 |
42 | // Images and captions
43 | .thumbnail > img {
44 | display: block;
45 | max-width: 100%;
46 | margin-left: auto;
47 | margin-right: auto;
48 | }
49 | .thumbnail .caption {
50 | padding: 9px;
51 | color: @gray;
52 | }
53 |
--------------------------------------------------------------------------------
/app/assets/css/lib/tooltip.less:
--------------------------------------------------------------------------------
1 | //
2 | // Tooltips
3 | // --------------------------------------------------
4 |
5 |
6 | // Base class
7 | .tooltip {
8 | position: absolute;
9 | z-index: @zindexTooltip;
10 | display: block;
11 | visibility: visible;
12 | padding: 5px;
13 | font-size: 11px;
14 | .opacity(0);
15 | &.in { .opacity(80); }
16 | &.top { margin-top: -3px; }
17 | &.right { margin-left: 3px; }
18 | &.bottom { margin-top: 3px; }
19 | &.left { margin-left: -3px; }
20 | }
21 |
22 | // Wrapper for the tooltip content
23 | .tooltip-inner {
24 | max-width: 200px;
25 | padding: 3px 8px;
26 | color: @tooltipColor;
27 | text-align: center;
28 | text-decoration: none;
29 | background-color: @tooltipBackground;
30 | .border-radius(4px);
31 | }
32 |
33 | // Arrows
34 | .tooltip-arrow {
35 | position: absolute;
36 | width: 0;
37 | height: 0;
38 | border-color: transparent;
39 | border-style: solid;
40 | }
41 | .tooltip {
42 | &.top .tooltip-arrow {
43 | bottom: 0;
44 | left: 50%;
45 | margin-left: -@tooltipArrowWidth;
46 | border-width: @tooltipArrowWidth @tooltipArrowWidth 0;
47 | border-top-color: @tooltipArrowColor;
48 | }
49 | &.right .tooltip-arrow {
50 | top: 50%;
51 | left: 0;
52 | margin-top: -@tooltipArrowWidth;
53 | border-width: @tooltipArrowWidth @tooltipArrowWidth @tooltipArrowWidth 0;
54 | border-right-color: @tooltipArrowColor;
55 | }
56 | &.left .tooltip-arrow {
57 | top: 50%;
58 | right: 0;
59 | margin-top: -@tooltipArrowWidth;
60 | border-width: @tooltipArrowWidth 0 @tooltipArrowWidth @tooltipArrowWidth;
61 | border-left-color: @tooltipArrowColor;
62 | }
63 | &.bottom .tooltip-arrow {
64 | top: 0;
65 | left: 50%;
66 | margin-left: -@tooltipArrowWidth;
67 | border-width: 0 @tooltipArrowWidth @tooltipArrowWidth;
68 | border-bottom-color: @tooltipArrowColor;
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/app/assets/css/lib/type.less:
--------------------------------------------------------------------------------
1 | //
2 | // Typography
3 | // --------------------------------------------------
4 |
5 |
6 | // Body text
7 | // -------------------------
8 |
9 | p {
10 | margin: 0 0 @baseLineHeight / 2;
11 | }
12 | .lead {
13 | margin-bottom: @baseLineHeight;
14 | font-size: @baseFontSize * 1.5;
15 | font-weight: 200;
16 | line-height: @baseLineHeight * 1.5;
17 | }
18 |
19 |
20 | // Emphasis & misc
21 | // -------------------------
22 |
23 | small {
24 | font-size: 85%; // Ex: 14px base font * 85% = about 12px
25 | }
26 | strong {
27 | font-weight: bold;
28 | }
29 | em {
30 | font-style: italic;
31 | }
32 | cite {
33 | font-style: normal;
34 | }
35 |
36 | // Utility classes
37 | .muted {
38 | color: @grayLight;
39 | }
40 | .text-warning {
41 | color: @warningText;
42 | }
43 | .text-error {
44 | color: @errorText;
45 | }
46 | .text-info {
47 | color: @infoText;
48 | }
49 | .text-success {
50 | color: @successText;
51 | }
52 |
53 |
54 | // Headings
55 | // -------------------------
56 |
57 | h1, h2, h3, h4, h5, h6 {
58 | margin: (@baseLineHeight / 2) 0;
59 | font-family: @headingsFontFamily;
60 | font-weight: @headingsFontWeight;
61 | line-height: 1;
62 | color: @headingsColor;
63 | text-rendering: optimizelegibility; // Fix the character spacing for headings
64 | small {
65 | font-weight: normal;
66 | line-height: 1;
67 | color: @grayLight;
68 | }
69 | }
70 | h1 { font-size: 36px; line-height: 40px; }
71 | h2 { font-size: 30px; line-height: 40px; }
72 | h3 { font-size: 24px; line-height: 40px; }
73 | h4 { font-size: 18px; line-height: 20px; }
74 | h5 { font-size: 14px; line-height: 20px; }
75 | h6 { font-size: 12px; line-height: 20px; }
76 |
77 | h1 small { font-size: 24px; }
78 | h2 small { font-size: 18px; }
79 | h3 small { font-size: 14px; }
80 | h4 small { font-size: 14px; }
81 |
82 |
83 | // Page header
84 | // -------------------------
85 |
86 | .page-header {
87 | padding-bottom: (@baseLineHeight / 2) - 1;
88 | margin: @baseLineHeight 0 (@baseLineHeight * 1.5);
89 | border-bottom: 1px solid @grayLighter;
90 | }
91 |
92 |
93 |
94 | // Lists
95 | // --------------------------------------------------
96 |
97 | // Unordered and Ordered lists
98 | ul, ol {
99 | padding: 0;
100 | margin: 0 0 @baseLineHeight / 2 25px;
101 | }
102 | ul ul,
103 | ul ol,
104 | ol ol,
105 | ol ul {
106 | margin-bottom: 0;
107 | }
108 | li {
109 | line-height: @baseLineHeight;
110 | }
111 | ul.unstyled,
112 | ol.unstyled {
113 | margin-left: 0;
114 | list-style: none;
115 | }
116 |
117 | // Description Lists
118 | dl {
119 | margin-bottom: @baseLineHeight;
120 | }
121 | dt,
122 | dd {
123 | line-height: @baseLineHeight;
124 | }
125 | dt {
126 | font-weight: bold;
127 | }
128 | dd {
129 | margin-left: @baseLineHeight / 2;
130 | }
131 | // Horizontal layout (like forms)
132 | .dl-horizontal {
133 | .clearfix(); // Ensure dl clears floats if empty dd elements present
134 | dt {
135 | float: left;
136 | width: @horizontalComponentOffset - 20;
137 | clear: left;
138 | text-align: right;
139 | .text-overflow();
140 | }
141 | dd {
142 | margin-left: @horizontalComponentOffset;
143 | }
144 | }
145 |
146 | // MISC
147 | // ----
148 |
149 | // Horizontal rules
150 | hr {
151 | margin: @baseLineHeight 0;
152 | border: 0;
153 | border-top: 1px solid @hrBorder;
154 | border-bottom: 1px solid @white;
155 | }
156 |
157 | // Abbreviations and acronyms
158 | abbr[title] {
159 | cursor: help;
160 | border-bottom: 1px dotted @grayLight;
161 | }
162 | abbr.initialism {
163 | font-size: 90%;
164 | text-transform: uppercase;
165 | }
166 |
167 | // Blockquotes
168 | blockquote {
169 | padding: 0 0 0 15px;
170 | margin: 0 0 @baseLineHeight;
171 | border-left: 5px solid @grayLighter;
172 | p {
173 | margin-bottom: 0;
174 | #font > .shorthand(16px,300,@baseLineHeight * 1.25);
175 | }
176 | small {
177 | display: block;
178 | line-height: @baseLineHeight;
179 | color: @grayLight;
180 | &:before {
181 | content: '\2014 \00A0';
182 | }
183 | }
184 |
185 | // Float right with text-align: right
186 | &.pull-right {
187 | float: right;
188 | padding-right: 15px;
189 | padding-left: 0;
190 | border-right: 5px solid @grayLighter;
191 | border-left: 0;
192 | p,
193 | small {
194 | text-align: right;
195 | }
196 | small {
197 | &:before {
198 | content: '';
199 | }
200 | &:after {
201 | content: '\00A0 \2014';
202 | }
203 | }
204 | }
205 | }
206 |
207 | // Quotes
208 | q:before,
209 | q:after,
210 | blockquote:before,
211 | blockquote:after {
212 | content: "";
213 | }
214 |
215 | // Addresses
216 | address {
217 | display: block;
218 | margin-bottom: @baseLineHeight;
219 | font-style: normal;
220 | line-height: @baseLineHeight;
221 | }
222 |
--------------------------------------------------------------------------------
/app/assets/css/lib/utilities.less:
--------------------------------------------------------------------------------
1 | //
2 | // Utility classes
3 | // --------------------------------------------------
4 |
5 |
6 | // Quick floats
7 | .pull-right {
8 | float: right;
9 | }
10 | .pull-left {
11 | float: left;
12 | }
13 |
14 | // Toggling content
15 | .hide {
16 | display: none;
17 | }
18 | .show {
19 | display: block;
20 | }
21 |
22 | // Visibility
23 | .invisible {
24 | visibility: hidden;
25 | }
26 |
27 | // For Affix plugin
28 | .affix {
29 | position: fixed;
30 | }
31 |
--------------------------------------------------------------------------------
/app/assets/css/lib/wells.less:
--------------------------------------------------------------------------------
1 | //
2 | // Wells
3 | // --------------------------------------------------
4 |
5 |
6 | // Base class
7 | .well {
8 | min-height: 20px;
9 | padding: 19px;
10 | margin-bottom: 20px;
11 | background-color: @wellBackground;
12 | border: 1px solid darken(@wellBackground, 7%);
13 | .border-radius(4px);
14 | .box-shadow(inset 0 1px 1px rgba(0,0,0,.05));
15 | blockquote {
16 | border-color: #ddd;
17 | border-color: rgba(0,0,0,.15);
18 | }
19 | }
20 |
21 | // Sizes
22 | .well-large {
23 | padding: 24px;
24 | .border-radius(6px);
25 | }
26 | .well-small {
27 | padding: 9px;
28 | .border-radius(3px);
29 | }
30 |
--------------------------------------------------------------------------------
/app/assets/css/main.css.less:
--------------------------------------------------------------------------------
1 |
2 | @import "./lib/bootstrap.less";
3 |
4 | body {
5 | padding-top: 60px;
6 | padding-bottom: 40px;
7 | background-image: url("/images/grey.png");
8 | background-repeat: repeat;
9 | }
10 |
11 | @import "./lib/responsive.less";
12 |
13 | .jumbo {
14 |
15 | background-color: rgba(0, 0, 0, 0.1);
16 | .border-radius(5px);
17 | margin-top: -60px;
18 |
19 | .container();
20 | padding-top: 60px;
21 | padding-bottom: 40px;
22 | margin-bottom: 30px;
23 |
24 | .border-radius(6px);
25 | h1 {
26 | margin-bottom: 0;
27 | font-size: 60px;
28 | line-height: 1;
29 | color: @heroUnitHeadingColor;
30 | letter-spacing: -1px;
31 | }
32 |
33 | }
34 |
--------------------------------------------------------------------------------
/app/assets/js/bootstrap.js:
--------------------------------------------------------------------------------
1 | //= require ./jquery-1.8.2.js
2 | //= require ./bootstrap/bootstrap-transition.js
3 | //= require ./bootstrap/bootstrap-alert.js
4 | //= require ./bootstrap/bootstrap-modal.js
5 | //= require ./bootstrap/bootstrap-dropdown.js
6 | //= require ./bootstrap/bootstrap-scrollspy.js
7 | //= require ./bootstrap/bootstrap-tab.js
8 | //= require ./bootstrap/bootstrap-tooltip.js
9 | //= require ./bootstrap/bootstrap-popover.js
10 | //= require ./bootstrap/bootstrap-button.js
11 | //= require ./bootstrap/bootstrap-collapse.js
12 | //= require ./bootstrap/bootstrap-carousel.js
13 | //= require ./bootstrap/bootstrap-typeahead.js
14 |
--------------------------------------------------------------------------------
/app/assets/js/bootstrap/bootstrap-affix.js:
--------------------------------------------------------------------------------
1 | /* ==========================================================
2 | * bootstrap-affix.js v2.1.1
3 | * http://twitter.github.com/bootstrap/javascript.html#affix
4 | * ==========================================================
5 | * Copyright 2012 Twitter, Inc.
6 | *
7 | * Licensed under the Apache License, Version 2.0 (the "License");
8 | * you may not use this file except in compliance with the License.
9 | * You may obtain a copy of the License at
10 | *
11 | * http://www.apache.org/licenses/LICENSE-2.0
12 | *
13 | * Unless required by applicable law or agreed to in writing, software
14 | * distributed under the License is distributed on an "AS IS" BASIS,
15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 | * See the License for the specific language governing permissions and
17 | * limitations under the License.
18 | * ========================================================== */
19 |
20 |
21 | !function ($) {
22 |
23 | "use strict"; // jshint ;_;
24 |
25 |
26 | /* AFFIX CLASS DEFINITION
27 | * ====================== */
28 |
29 | var Affix = function (element, options) {
30 | this.options = $.extend({}, $.fn.affix.defaults, options)
31 | this.$window = $(window).on('scroll.affix.data-api', $.proxy(this.checkPosition, this))
32 | this.$element = $(element)
33 | this.checkPosition()
34 | }
35 |
36 | Affix.prototype.checkPosition = function () {
37 | if (!this.$element.is(':visible')) return
38 |
39 | var scrollHeight = $(document).height()
40 | , scrollTop = this.$window.scrollTop()
41 | , position = this.$element.offset()
42 | , offset = this.options.offset
43 | , offsetBottom = offset.bottom
44 | , offsetTop = offset.top
45 | , reset = 'affix affix-top affix-bottom'
46 | , affix
47 |
48 | if (typeof offset != 'object') offsetBottom = offsetTop = offset
49 | if (typeof offsetTop == 'function') offsetTop = offset.top()
50 | if (typeof offsetBottom == 'function') offsetBottom = offset.bottom()
51 |
52 | affix = this.unpin != null && (scrollTop + this.unpin <= position.top) ?
53 | false : offsetBottom != null && (position.top + this.$element.height() >= scrollHeight - offsetBottom) ?
54 | 'bottom' : offsetTop != null && scrollTop <= offsetTop ?
55 | 'top' : false
56 |
57 | if (this.affixed === affix) return
58 |
59 | this.affixed = affix
60 | this.unpin = affix == 'bottom' ? position.top - scrollTop : null
61 |
62 | this.$element.removeClass(reset).addClass('affix' + (affix ? '-' + affix : ''))
63 | }
64 |
65 |
66 | /* AFFIX PLUGIN DEFINITION
67 | * ======================= */
68 |
69 | $.fn.affix = function (option) {
70 | return this.each(function () {
71 | var $this = $(this)
72 | , data = $this.data('affix')
73 | , options = typeof option == 'object' && option
74 | if (!data) $this.data('affix', (data = new Affix(this, options)))
75 | if (typeof option == 'string') data[option]()
76 | })
77 | }
78 |
79 | $.fn.affix.Constructor = Affix
80 |
81 | $.fn.affix.defaults = {
82 | offset: 0
83 | }
84 |
85 |
86 | /* AFFIX DATA-API
87 | * ============== */
88 |
89 | $(window).on('load', function () {
90 | $('[data-spy="affix"]').each(function () {
91 | var $spy = $(this)
92 | , data = $spy.data()
93 |
94 | data.offset = data.offset || {}
95 |
96 | data.offsetBottom && (data.offset.bottom = data.offsetBottom)
97 | data.offsetTop && (data.offset.top = data.offsetTop)
98 |
99 | $spy.affix(data)
100 | })
101 | })
102 |
103 |
104 | }(window.jQuery);
--------------------------------------------------------------------------------
/app/assets/js/bootstrap/bootstrap-alert.js:
--------------------------------------------------------------------------------
1 | /* ==========================================================
2 | * bootstrap-alert.js v2.1.1
3 | * http://twitter.github.com/bootstrap/javascript.html#alerts
4 | * ==========================================================
5 | * Copyright 2012 Twitter, Inc.
6 | *
7 | * Licensed under the Apache License, Version 2.0 (the "License");
8 | * you may not use this file except in compliance with the License.
9 | * You may obtain a copy of the License at
10 | *
11 | * http://www.apache.org/licenses/LICENSE-2.0
12 | *
13 | * Unless required by applicable law or agreed to in writing, software
14 | * distributed under the License is distributed on an "AS IS" BASIS,
15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 | * See the License for the specific language governing permissions and
17 | * limitations under the License.
18 | * ========================================================== */
19 |
20 |
21 | !function ($) {
22 |
23 | "use strict"; // jshint ;_;
24 |
25 |
26 | /* ALERT CLASS DEFINITION
27 | * ====================== */
28 |
29 | var dismiss = '[data-dismiss="alert"]'
30 | , Alert = function (el) {
31 | $(el).on('click', dismiss, this.close)
32 | }
33 |
34 | Alert.prototype.close = function (e) {
35 | var $this = $(this)
36 | , selector = $this.attr('data-target')
37 | , $parent
38 |
39 | if (!selector) {
40 | selector = $this.attr('href')
41 | selector = selector && selector.replace(/.*(?=#[^\s]*$)/, '') //strip for ie7
42 | }
43 |
44 | $parent = $(selector)
45 |
46 | e && e.preventDefault()
47 |
48 | $parent.length || ($parent = $this.hasClass('alert') ? $this : $this.parent())
49 |
50 | $parent.trigger(e = $.Event('close'))
51 |
52 | if (e.isDefaultPrevented()) return
53 |
54 | $parent.removeClass('in')
55 |
56 | function removeElement() {
57 | $parent
58 | .trigger('closed')
59 | .remove()
60 | }
61 |
62 | $.support.transition && $parent.hasClass('fade') ?
63 | $parent.on($.support.transition.end, removeElement) :
64 | removeElement()
65 | }
66 |
67 |
68 | /* ALERT PLUGIN DEFINITION
69 | * ======================= */
70 |
71 | $.fn.alert = function (option) {
72 | return this.each(function () {
73 | var $this = $(this)
74 | , data = $this.data('alert')
75 | if (!data) $this.data('alert', (data = new Alert(this)))
76 | if (typeof option == 'string') data[option].call($this)
77 | })
78 | }
79 |
80 | $.fn.alert.Constructor = Alert
81 |
82 |
83 | /* ALERT DATA-API
84 | * ============== */
85 |
86 | $(function () {
87 | $('body').on('click.alert.data-api', dismiss, Alert.prototype.close)
88 | })
89 |
90 | }(window.jQuery);
--------------------------------------------------------------------------------
/app/assets/js/bootstrap/bootstrap-button.js:
--------------------------------------------------------------------------------
1 | /* ============================================================
2 | * bootstrap-button.js v2.1.1
3 | * http://twitter.github.com/bootstrap/javascript.html#buttons
4 | * ============================================================
5 | * Copyright 2012 Twitter, Inc.
6 | *
7 | * Licensed under the Apache License, Version 2.0 (the "License");
8 | * you may not use this file except in compliance with the License.
9 | * You may obtain a copy of the License at
10 | *
11 | * http://www.apache.org/licenses/LICENSE-2.0
12 | *
13 | * Unless required by applicable law or agreed to in writing, software
14 | * distributed under the License is distributed on an "AS IS" BASIS,
15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 | * See the License for the specific language governing permissions and
17 | * limitations under the License.
18 | * ============================================================ */
19 |
20 |
21 | !function ($) {
22 |
23 | "use strict"; // jshint ;_;
24 |
25 |
26 | /* BUTTON PUBLIC CLASS DEFINITION
27 | * ============================== */
28 |
29 | var Button = function (element, options) {
30 | this.$element = $(element)
31 | this.options = $.extend({}, $.fn.button.defaults, options)
32 | }
33 |
34 | Button.prototype.setState = function (state) {
35 | var d = 'disabled'
36 | , $el = this.$element
37 | , data = $el.data()
38 | , val = $el.is('input') ? 'val' : 'html'
39 |
40 | state = state + 'Text'
41 | data.resetText || $el.data('resetText', $el[val]())
42 |
43 | $el[val](data[state] || this.options[state])
44 |
45 | // push to event loop to allow forms to submit
46 | setTimeout(function () {
47 | state == 'loadingText' ?
48 | $el.addClass(d).attr(d, d) :
49 | $el.removeClass(d).removeAttr(d)
50 | }, 0)
51 | }
52 |
53 | Button.prototype.toggle = function () {
54 | var $parent = this.$element.closest('[data-toggle="buttons-radio"]')
55 |
56 | $parent && $parent
57 | .find('.active')
58 | .removeClass('active')
59 |
60 | this.$element.toggleClass('active')
61 | }
62 |
63 |
64 | /* BUTTON PLUGIN DEFINITION
65 | * ======================== */
66 |
67 | $.fn.button = function (option) {
68 | return this.each(function () {
69 | var $this = $(this)
70 | , data = $this.data('button')
71 | , options = typeof option == 'object' && option
72 | if (!data) $this.data('button', (data = new Button(this, options)))
73 | if (option == 'toggle') data.toggle()
74 | else if (option) data.setState(option)
75 | })
76 | }
77 |
78 | $.fn.button.defaults = {
79 | loadingText: 'loading...'
80 | }
81 |
82 | $.fn.button.Constructor = Button
83 |
84 |
85 | /* BUTTON DATA-API
86 | * =============== */
87 |
88 | $(function () {
89 | $('body').on('click.button.data-api', '[data-toggle^=button]', function ( e ) {
90 | var $btn = $(e.target)
91 | if (!$btn.hasClass('btn')) $btn = $btn.closest('.btn')
92 | $btn.button('toggle')
93 | })
94 | })
95 |
96 | }(window.jQuery);
--------------------------------------------------------------------------------
/app/assets/js/bootstrap/bootstrap-carousel.js:
--------------------------------------------------------------------------------
1 | /* ==========================================================
2 | * bootstrap-carousel.js v2.1.1
3 | * http://twitter.github.com/bootstrap/javascript.html#carousel
4 | * ==========================================================
5 | * Copyright 2012 Twitter, Inc.
6 | *
7 | * Licensed under the Apache License, Version 2.0 (the "License");
8 | * you may not use this file except in compliance with the License.
9 | * You may obtain a copy of the License at
10 | *
11 | * http://www.apache.org/licenses/LICENSE-2.0
12 | *
13 | * Unless required by applicable law or agreed to in writing, software
14 | * distributed under the License is distributed on an "AS IS" BASIS,
15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 | * See the License for the specific language governing permissions and
17 | * limitations under the License.
18 | * ========================================================== */
19 |
20 |
21 | !function ($) {
22 |
23 | "use strict"; // jshint ;_;
24 |
25 |
26 | /* CAROUSEL CLASS DEFINITION
27 | * ========================= */
28 |
29 | var Carousel = function (element, options) {
30 | this.$element = $(element)
31 | this.options = options
32 | this.options.slide && this.slide(this.options.slide)
33 | this.options.pause == 'hover' && this.$element
34 | .on('mouseenter', $.proxy(this.pause, this))
35 | .on('mouseleave', $.proxy(this.cycle, this))
36 | }
37 |
38 | Carousel.prototype = {
39 |
40 | cycle: function (e) {
41 | if (!e) this.paused = false
42 | this.options.interval
43 | && !this.paused
44 | && (this.interval = setInterval($.proxy(this.next, this), this.options.interval))
45 | return this
46 | }
47 |
48 | , to: function (pos) {
49 | var $active = this.$element.find('.item.active')
50 | , children = $active.parent().children()
51 | , activePos = children.index($active)
52 | , that = this
53 |
54 | if (pos > (children.length - 1) || pos < 0) return
55 |
56 | if (this.sliding) {
57 | return this.$element.one('slid', function () {
58 | that.to(pos)
59 | })
60 | }
61 |
62 | if (activePos == pos) {
63 | return this.pause().cycle()
64 | }
65 |
66 | return this.slide(pos > activePos ? 'next' : 'prev', $(children[pos]))
67 | }
68 |
69 | , pause: function (e) {
70 | if (!e) this.paused = true
71 | if (this.$element.find('.next, .prev').length && $.support.transition.end) {
72 | this.$element.trigger($.support.transition.end)
73 | this.cycle()
74 | }
75 | clearInterval(this.interval)
76 | this.interval = null
77 | return this
78 | }
79 |
80 | , next: function () {
81 | if (this.sliding) return
82 | return this.slide('next')
83 | }
84 |
85 | , prev: function () {
86 | if (this.sliding) return
87 | return this.slide('prev')
88 | }
89 |
90 | , slide: function (type, next) {
91 | var $active = this.$element.find('.item.active')
92 | , $next = next || $active[type]()
93 | , isCycling = this.interval
94 | , direction = type == 'next' ? 'left' : 'right'
95 | , fallback = type == 'next' ? 'first' : 'last'
96 | , that = this
97 | , e = $.Event('slide', {
98 | relatedTarget: $next[0]
99 | })
100 |
101 | this.sliding = true
102 |
103 | isCycling && this.pause()
104 |
105 | $next = $next.length ? $next : this.$element.find('.item')[fallback]()
106 |
107 | if ($next.hasClass('active')) return
108 |
109 | if ($.support.transition && this.$element.hasClass('slide')) {
110 | this.$element.trigger(e)
111 | if (e.isDefaultPrevented()) return
112 | $next.addClass(type)
113 | $next[0].offsetWidth // force reflow
114 | $active.addClass(direction)
115 | $next.addClass(direction)
116 | this.$element.one($.support.transition.end, function () {
117 | $next.removeClass([type, direction].join(' ')).addClass('active')
118 | $active.removeClass(['active', direction].join(' '))
119 | that.sliding = false
120 | setTimeout(function () { that.$element.trigger('slid') }, 0)
121 | })
122 | } else {
123 | this.$element.trigger(e)
124 | if (e.isDefaultPrevented()) return
125 | $active.removeClass('active')
126 | $next.addClass('active')
127 | this.sliding = false
128 | this.$element.trigger('slid')
129 | }
130 |
131 | isCycling && this.cycle()
132 |
133 | return this
134 | }
135 |
136 | }
137 |
138 |
139 | /* CAROUSEL PLUGIN DEFINITION
140 | * ========================== */
141 |
142 | $.fn.carousel = function (option) {
143 | return this.each(function () {
144 | var $this = $(this)
145 | , data = $this.data('carousel')
146 | , options = $.extend({}, $.fn.carousel.defaults, typeof option == 'object' && option)
147 | , action = typeof option == 'string' ? option : options.slide
148 | if (!data) $this.data('carousel', (data = new Carousel(this, options)))
149 | if (typeof option == 'number') data.to(option)
150 | else if (action) data[action]()
151 | else if (options.interval) data.cycle()
152 | })
153 | }
154 |
155 | $.fn.carousel.defaults = {
156 | interval: 5000
157 | , pause: 'hover'
158 | }
159 |
160 | $.fn.carousel.Constructor = Carousel
161 |
162 |
163 | /* CAROUSEL DATA-API
164 | * ================= */
165 |
166 | $(function () {
167 | $('body').on('click.carousel.data-api', '[data-slide]', function ( e ) {
168 | var $this = $(this), href
169 | , $target = $($this.attr('data-target') || (href = $this.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '')) //strip for ie7
170 | , options = !$target.data('modal') && $.extend({}, $target.data(), $this.data())
171 | $target.carousel(options)
172 | e.preventDefault()
173 | })
174 | })
175 |
176 | }(window.jQuery);
--------------------------------------------------------------------------------
/app/assets/js/bootstrap/bootstrap-collapse.js:
--------------------------------------------------------------------------------
1 | /* =============================================================
2 | * bootstrap-collapse.js v2.1.1
3 | * http://twitter.github.com/bootstrap/javascript.html#collapse
4 | * =============================================================
5 | * Copyright 2012 Twitter, Inc.
6 | *
7 | * Licensed under the Apache License, Version 2.0 (the "License");
8 | * you may not use this file except in compliance with the License.
9 | * You may obtain a copy of the License at
10 | *
11 | * http://www.apache.org/licenses/LICENSE-2.0
12 | *
13 | * Unless required by applicable law or agreed to in writing, software
14 | * distributed under the License is distributed on an "AS IS" BASIS,
15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 | * See the License for the specific language governing permissions and
17 | * limitations under the License.
18 | * ============================================================ */
19 |
20 |
21 | !function ($) {
22 |
23 | "use strict"; // jshint ;_;
24 |
25 |
26 | /* COLLAPSE PUBLIC CLASS DEFINITION
27 | * ================================ */
28 |
29 | var Collapse = function (element, options) {
30 | this.$element = $(element)
31 | this.options = $.extend({}, $.fn.collapse.defaults, options)
32 |
33 | if (this.options.parent) {
34 | this.$parent = $(this.options.parent)
35 | }
36 |
37 | this.options.toggle && this.toggle()
38 | }
39 |
40 | Collapse.prototype = {
41 |
42 | constructor: Collapse
43 |
44 | , dimension: function () {
45 | var hasWidth = this.$element.hasClass('width')
46 | return hasWidth ? 'width' : 'height'
47 | }
48 |
49 | , show: function () {
50 | var dimension
51 | , scroll
52 | , actives
53 | , hasData
54 |
55 | if (this.transitioning) return
56 |
57 | dimension = this.dimension()
58 | scroll = $.camelCase(['scroll', dimension].join('-'))
59 | actives = this.$parent && this.$parent.find('> .accordion-group > .in')
60 |
61 | if (actives && actives.length) {
62 | hasData = actives.data('collapse')
63 | if (hasData && hasData.transitioning) return
64 | actives.collapse('hide')
65 | hasData || actives.data('collapse', null)
66 | }
67 |
68 | this.$element[dimension](0)
69 | this.transition('addClass', $.Event('show'), 'shown')
70 | $.support.transition && this.$element[dimension](this.$element[0][scroll])
71 | }
72 |
73 | , hide: function () {
74 | var dimension
75 | if (this.transitioning) return
76 | dimension = this.dimension()
77 | this.reset(this.$element[dimension]())
78 | this.transition('removeClass', $.Event('hide'), 'hidden')
79 | this.$element[dimension](0)
80 | }
81 |
82 | , reset: function (size) {
83 | var dimension = this.dimension()
84 |
85 | this.$element
86 | .removeClass('collapse')
87 | [dimension](size || 'auto')
88 | [0].offsetWidth
89 |
90 | this.$element[size !== null ? 'addClass' : 'removeClass']('collapse')
91 |
92 | return this
93 | }
94 |
95 | , transition: function (method, startEvent, completeEvent) {
96 | var that = this
97 | , complete = function () {
98 | if (startEvent.type == 'show') that.reset()
99 | that.transitioning = 0
100 | that.$element.trigger(completeEvent)
101 | }
102 |
103 | this.$element.trigger(startEvent)
104 |
105 | if (startEvent.isDefaultPrevented()) return
106 |
107 | this.transitioning = 1
108 |
109 | this.$element[method]('in')
110 |
111 | $.support.transition && this.$element.hasClass('collapse') ?
112 | this.$element.one($.support.transition.end, complete) :
113 | complete()
114 | }
115 |
116 | , toggle: function () {
117 | this[this.$element.hasClass('in') ? 'hide' : 'show']()
118 | }
119 |
120 | }
121 |
122 |
123 | /* COLLAPSIBLE PLUGIN DEFINITION
124 | * ============================== */
125 |
126 | $.fn.collapse = function (option) {
127 | return this.each(function () {
128 | var $this = $(this)
129 | , data = $this.data('collapse')
130 | , options = typeof option == 'object' && option
131 | if (!data) $this.data('collapse', (data = new Collapse(this, options)))
132 | if (typeof option == 'string') data[option]()
133 | })
134 | }
135 |
136 | $.fn.collapse.defaults = {
137 | toggle: true
138 | }
139 |
140 | $.fn.collapse.Constructor = Collapse
141 |
142 |
143 | /* COLLAPSIBLE DATA-API
144 | * ==================== */
145 |
146 | $(function () {
147 | $('body').on('click.collapse.data-api', '[data-toggle=collapse]', function (e) {
148 | var $this = $(this), href
149 | , target = $this.attr('data-target')
150 | || e.preventDefault()
151 | || (href = $this.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '') //strip for ie7
152 | , option = $(target).data('collapse') ? 'toggle' : $this.data()
153 | $this[$(target).hasClass('in') ? 'addClass' : 'removeClass']('collapsed')
154 | $(target).collapse(option)
155 | })
156 | })
157 |
158 | }(window.jQuery);
--------------------------------------------------------------------------------
/app/assets/js/bootstrap/bootstrap-dropdown.js:
--------------------------------------------------------------------------------
1 | /* ============================================================
2 | * bootstrap-dropdown.js v2.1.1
3 | * http://twitter.github.com/bootstrap/javascript.html#dropdowns
4 | * ============================================================
5 | * Copyright 2012 Twitter, Inc.
6 | *
7 | * Licensed under the Apache License, Version 2.0 (the "License");
8 | * you may not use this file except in compliance with the License.
9 | * You may obtain a copy of the License at
10 | *
11 | * http://www.apache.org/licenses/LICENSE-2.0
12 | *
13 | * Unless required by applicable law or agreed to in writing, software
14 | * distributed under the License is distributed on an "AS IS" BASIS,
15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 | * See the License for the specific language governing permissions and
17 | * limitations under the License.
18 | * ============================================================ */
19 |
20 |
21 | !function ($) {
22 |
23 | "use strict"; // jshint ;_;
24 |
25 |
26 | /* DROPDOWN CLASS DEFINITION
27 | * ========================= */
28 |
29 | var toggle = '[data-toggle=dropdown]'
30 | , Dropdown = function (element) {
31 | var $el = $(element).on('click.dropdown.data-api', this.toggle)
32 | $('html').on('click.dropdown.data-api', function () {
33 | $el.parent().removeClass('open')
34 | })
35 | }
36 |
37 | Dropdown.prototype = {
38 |
39 | constructor: Dropdown
40 |
41 | , toggle: function (e) {
42 | var $this = $(this)
43 | , $parent
44 | , isActive
45 |
46 | if ($this.is('.disabled, :disabled')) return
47 |
48 | $parent = getParent($this)
49 |
50 | isActive = $parent.hasClass('open')
51 |
52 | clearMenus()
53 |
54 | if (!isActive) {
55 | $parent.toggleClass('open')
56 | $this.focus()
57 | }
58 |
59 | return false
60 | }
61 |
62 | , keydown: function (e) {
63 | var $this
64 | , $items
65 | , $active
66 | , $parent
67 | , isActive
68 | , index
69 |
70 | if (!/(38|40|27)/.test(e.keyCode)) return
71 |
72 | $this = $(this)
73 |
74 | e.preventDefault()
75 | e.stopPropagation()
76 |
77 | if ($this.is('.disabled, :disabled')) return
78 |
79 | $parent = getParent($this)
80 |
81 | isActive = $parent.hasClass('open')
82 |
83 | if (!isActive || (isActive && e.keyCode == 27)) return $this.click()
84 |
85 | $items = $('[role=menu] li:not(.divider) a', $parent)
86 |
87 | if (!$items.length) return
88 |
89 | index = $items.index($items.filter(':focus'))
90 |
91 | if (e.keyCode == 38 && index > 0) index-- // up
92 | if (e.keyCode == 40 && index < $items.length - 1) index++ // down
93 | if (!~index) index = 0
94 |
95 | $items
96 | .eq(index)
97 | .focus()
98 | }
99 |
100 | }
101 |
102 | function clearMenus() {
103 | getParent($(toggle))
104 | .removeClass('open')
105 | }
106 |
107 | function getParent($this) {
108 | var selector = $this.attr('data-target')
109 | , $parent
110 |
111 | if (!selector) {
112 | selector = $this.attr('href')
113 | selector = selector && /#/.test(selector) && selector.replace(/.*(?=#[^\s]*$)/, '') //strip for ie7
114 | }
115 |
116 | $parent = $(selector)
117 | $parent.length || ($parent = $this.parent())
118 |
119 | return $parent
120 | }
121 |
122 |
123 | /* DROPDOWN PLUGIN DEFINITION
124 | * ========================== */
125 |
126 | $.fn.dropdown = function (option) {
127 | return this.each(function () {
128 | var $this = $(this)
129 | , data = $this.data('dropdown')
130 | if (!data) $this.data('dropdown', (data = new Dropdown(this)))
131 | if (typeof option == 'string') data[option].call($this)
132 | })
133 | }
134 |
135 | $.fn.dropdown.Constructor = Dropdown
136 |
137 |
138 | /* APPLY TO STANDARD DROPDOWN ELEMENTS
139 | * =================================== */
140 |
141 | $(function () {
142 | $('html')
143 | .on('click.dropdown.data-api touchstart.dropdown.data-api', clearMenus)
144 | $('body')
145 | .on('click.dropdown touchstart.dropdown.data-api', '.dropdown form', function (e) { e.stopPropagation() })
146 | .on('click.dropdown.data-api touchstart.dropdown.data-api' , toggle, Dropdown.prototype.toggle)
147 | .on('keydown.dropdown.data-api touchstart.dropdown.data-api', toggle + ', [role=menu]' , Dropdown.prototype.keydown)
148 | })
149 |
150 | }(window.jQuery);
--------------------------------------------------------------------------------
/app/assets/js/bootstrap/bootstrap-modal.js:
--------------------------------------------------------------------------------
1 | /* =========================================================
2 | * bootstrap-modal.js v2.1.1
3 | * http://twitter.github.com/bootstrap/javascript.html#modals
4 | * =========================================================
5 | * Copyright 2012 Twitter, Inc.
6 | *
7 | * Licensed under the Apache License, Version 2.0 (the "License");
8 | * you may not use this file except in compliance with the License.
9 | * You may obtain a copy of the License at
10 | *
11 | * http://www.apache.org/licenses/LICENSE-2.0
12 | *
13 | * Unless required by applicable law or agreed to in writing, software
14 | * distributed under the License is distributed on an "AS IS" BASIS,
15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 | * See the License for the specific language governing permissions and
17 | * limitations under the License.
18 | * ========================================================= */
19 |
20 |
21 | !function ($) {
22 |
23 | "use strict"; // jshint ;_;
24 |
25 |
26 | /* MODAL CLASS DEFINITION
27 | * ====================== */
28 |
29 | var Modal = function (element, options) {
30 | this.options = options
31 | this.$element = $(element)
32 | .delegate('[data-dismiss="modal"]', 'click.dismiss.modal', $.proxy(this.hide, this))
33 | this.options.remote && this.$element.find('.modal-body').load(this.options.remote)
34 | }
35 |
36 | Modal.prototype = {
37 |
38 | constructor: Modal
39 |
40 | , toggle: function () {
41 | return this[!this.isShown ? 'show' : 'hide']()
42 | }
43 |
44 | , show: function () {
45 | var that = this
46 | , e = $.Event('show')
47 |
48 | this.$element.trigger(e)
49 |
50 | if (this.isShown || e.isDefaultPrevented()) return
51 |
52 | $('body').addClass('modal-open')
53 |
54 | this.isShown = true
55 |
56 | this.escape()
57 |
58 | this.backdrop(function () {
59 | var transition = $.support.transition && that.$element.hasClass('fade')
60 |
61 | if (!that.$element.parent().length) {
62 | that.$element.appendTo(document.body) //don't move modals dom position
63 | }
64 |
65 | that.$element
66 | .show()
67 |
68 | if (transition) {
69 | that.$element[0].offsetWidth // force reflow
70 | }
71 |
72 | that.$element
73 | .addClass('in')
74 | .attr('aria-hidden', false)
75 | .focus()
76 |
77 | that.enforceFocus()
78 |
79 | transition ?
80 | that.$element.one($.support.transition.end, function () { that.$element.trigger('shown') }) :
81 | that.$element.trigger('shown')
82 |
83 | })
84 | }
85 |
86 | , hide: function (e) {
87 | e && e.preventDefault()
88 |
89 | var that = this
90 |
91 | e = $.Event('hide')
92 |
93 | this.$element.trigger(e)
94 |
95 | if (!this.isShown || e.isDefaultPrevented()) return
96 |
97 | this.isShown = false
98 |
99 | $('body').removeClass('modal-open')
100 |
101 | this.escape()
102 |
103 | $(document).off('focusin.modal')
104 |
105 | this.$element
106 | .removeClass('in')
107 | .attr('aria-hidden', true)
108 |
109 | $.support.transition && this.$element.hasClass('fade') ?
110 | this.hideWithTransition() :
111 | this.hideModal()
112 | }
113 |
114 | , enforceFocus: function () {
115 | var that = this
116 | $(document).on('focusin.modal', function (e) {
117 | if (that.$element[0] !== e.target && !that.$element.has(e.target).length) {
118 | that.$element.focus()
119 | }
120 | })
121 | }
122 |
123 | , escape: function () {
124 | var that = this
125 | if (this.isShown && this.options.keyboard) {
126 | this.$element.on('keyup.dismiss.modal', function ( e ) {
127 | e.which == 27 && that.hide()
128 | })
129 | } else if (!this.isShown) {
130 | this.$element.off('keyup.dismiss.modal')
131 | }
132 | }
133 |
134 | , hideWithTransition: function () {
135 | var that = this
136 | , timeout = setTimeout(function () {
137 | that.$element.off($.support.transition.end)
138 | that.hideModal()
139 | }, 500)
140 |
141 | this.$element.one($.support.transition.end, function () {
142 | clearTimeout(timeout)
143 | that.hideModal()
144 | })
145 | }
146 |
147 | , hideModal: function (that) {
148 | this.$element
149 | .hide()
150 | .trigger('hidden')
151 |
152 | this.backdrop()
153 | }
154 |
155 | , removeBackdrop: function () {
156 | this.$backdrop.remove()
157 | this.$backdrop = null
158 | }
159 |
160 | , backdrop: function (callback) {
161 | var that = this
162 | , animate = this.$element.hasClass('fade') ? 'fade' : ''
163 |
164 | if (this.isShown && this.options.backdrop) {
165 | var doAnimate = $.support.transition && animate
166 |
167 | this.$backdrop = $('
')
168 | .appendTo(document.body)
169 |
170 | if (this.options.backdrop != 'static') {
171 | this.$backdrop.click($.proxy(this.hide, this))
172 | }
173 |
174 | if (doAnimate) this.$backdrop[0].offsetWidth // force reflow
175 |
176 | this.$backdrop.addClass('in')
177 |
178 | doAnimate ?
179 | this.$backdrop.one($.support.transition.end, callback) :
180 | callback()
181 |
182 | } else if (!this.isShown && this.$backdrop) {
183 | this.$backdrop.removeClass('in')
184 |
185 | $.support.transition && this.$element.hasClass('fade')?
186 | this.$backdrop.one($.support.transition.end, $.proxy(this.removeBackdrop, this)) :
187 | this.removeBackdrop()
188 |
189 | } else if (callback) {
190 | callback()
191 | }
192 | }
193 | }
194 |
195 |
196 | /* MODAL PLUGIN DEFINITION
197 | * ======================= */
198 |
199 | $.fn.modal = function (option) {
200 | return this.each(function () {
201 | var $this = $(this)
202 | , data = $this.data('modal')
203 | , options = $.extend({}, $.fn.modal.defaults, $this.data(), typeof option == 'object' && option)
204 | if (!data) $this.data('modal', (data = new Modal(this, options)))
205 | if (typeof option == 'string') data[option]()
206 | else if (options.show) data.show()
207 | })
208 | }
209 |
210 | $.fn.modal.defaults = {
211 | backdrop: true
212 | , keyboard: true
213 | , show: true
214 | }
215 |
216 | $.fn.modal.Constructor = Modal
217 |
218 |
219 | /* MODAL DATA-API
220 | * ============== */
221 |
222 | $(function () {
223 | $('body').on('click.modal.data-api', '[data-toggle="modal"]', function ( e ) {
224 | var $this = $(this)
225 | , href = $this.attr('href')
226 | , $target = $($this.attr('data-target') || (href && href.replace(/.*(?=#[^\s]+$)/, ''))) //strip for ie7
227 | , option = $target.data('modal') ? 'toggle' : $.extend({ remote: !/#/.test(href) && href }, $target.data(), $this.data())
228 |
229 | e.preventDefault()
230 |
231 | $target
232 | .modal(option)
233 | .one('hide', function () {
234 | $this.focus()
235 | })
236 | })
237 | })
238 |
239 | }(window.jQuery);
--------------------------------------------------------------------------------
/app/assets/js/bootstrap/bootstrap-popover.js:
--------------------------------------------------------------------------------
1 | /* ===========================================================
2 | * bootstrap-popover.js v2.1.1
3 | * http://twitter.github.com/bootstrap/javascript.html#popovers
4 | * ===========================================================
5 | * Copyright 2012 Twitter, Inc.
6 | *
7 | * Licensed under the Apache License, Version 2.0 (the "License");
8 | * you may not use this file except in compliance with the License.
9 | * You may obtain a copy of the License at
10 | *
11 | * http://www.apache.org/licenses/LICENSE-2.0
12 | *
13 | * Unless required by applicable law or agreed to in writing, software
14 | * distributed under the License is distributed on an "AS IS" BASIS,
15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 | * See the License for the specific language governing permissions and
17 | * limitations under the License.
18 | * =========================================================== */
19 |
20 |
21 | !function ($) {
22 |
23 | "use strict"; // jshint ;_;
24 |
25 |
26 | /* POPOVER PUBLIC CLASS DEFINITION
27 | * =============================== */
28 |
29 | var Popover = function (element, options) {
30 | this.init('popover', element, options)
31 | }
32 |
33 |
34 | /* NOTE: POPOVER EXTENDS BOOTSTRAP-TOOLTIP.js
35 | ========================================== */
36 |
37 | Popover.prototype = $.extend({}, $.fn.tooltip.Constructor.prototype, {
38 |
39 | constructor: Popover
40 |
41 | , setContent: function () {
42 | var $tip = this.tip()
43 | , title = this.getTitle()
44 | , content = this.getContent()
45 |
46 | $tip.find('.popover-title')[this.options.html ? 'html' : 'text'](title)
47 | $tip.find('.popover-content > *')[this.options.html ? 'html' : 'text'](content)
48 |
49 | $tip.removeClass('fade top bottom left right in')
50 | }
51 |
52 | , hasContent: function () {
53 | return this.getTitle() || this.getContent()
54 | }
55 |
56 | , getContent: function () {
57 | var content
58 | , $e = this.$element
59 | , o = this.options
60 |
61 | content = $e.attr('data-content')
62 | || (typeof o.content == 'function' ? o.content.call($e[0]) : o.content)
63 |
64 | return content
65 | }
66 |
67 | , tip: function () {
68 | if (!this.$tip) {
69 | this.$tip = $(this.options.template)
70 | }
71 | return this.$tip
72 | }
73 |
74 | , destroy: function () {
75 | this.hide().$element.off('.' + this.type).removeData(this.type)
76 | }
77 |
78 | })
79 |
80 |
81 | /* POPOVER PLUGIN DEFINITION
82 | * ======================= */
83 |
84 | $.fn.popover = function (option) {
85 | return this.each(function () {
86 | var $this = $(this)
87 | , data = $this.data('popover')
88 | , options = typeof option == 'object' && option
89 | if (!data) $this.data('popover', (data = new Popover(this, options)))
90 | if (typeof option == 'string') data[option]()
91 | })
92 | }
93 |
94 | $.fn.popover.Constructor = Popover
95 |
96 | $.fn.popover.defaults = $.extend({} , $.fn.tooltip.defaults, {
97 | placement: 'right'
98 | , trigger: 'click'
99 | , content: ''
100 | , template: ''
101 | })
102 |
103 | }(window.jQuery);
--------------------------------------------------------------------------------
/app/assets/js/bootstrap/bootstrap-scrollspy.js:
--------------------------------------------------------------------------------
1 | /* =============================================================
2 | * bootstrap-scrollspy.js v2.1.1
3 | * http://twitter.github.com/bootstrap/javascript.html#scrollspy
4 | * =============================================================
5 | * Copyright 2012 Twitter, Inc.
6 | *
7 | * Licensed under the Apache License, Version 2.0 (the "License");
8 | * you may not use this file except in compliance with the License.
9 | * You may obtain a copy of the License at
10 | *
11 | * http://www.apache.org/licenses/LICENSE-2.0
12 | *
13 | * Unless required by applicable law or agreed to in writing, software
14 | * distributed under the License is distributed on an "AS IS" BASIS,
15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 | * See the License for the specific language governing permissions and
17 | * limitations under the License.
18 | * ============================================================== */
19 |
20 |
21 | !function ($) {
22 |
23 | "use strict"; // jshint ;_;
24 |
25 |
26 | /* SCROLLSPY CLASS DEFINITION
27 | * ========================== */
28 |
29 | function ScrollSpy(element, options) {
30 | var process = $.proxy(this.process, this)
31 | , $element = $(element).is('body') ? $(window) : $(element)
32 | , href
33 | this.options = $.extend({}, $.fn.scrollspy.defaults, options)
34 | this.$scrollElement = $element.on('scroll.scroll-spy.data-api', process)
35 | this.selector = (this.options.target
36 | || ((href = $(element).attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '')) //strip for ie7
37 | || '') + ' .nav li > a'
38 | this.$body = $('body')
39 | this.refresh()
40 | this.process()
41 | }
42 |
43 | ScrollSpy.prototype = {
44 |
45 | constructor: ScrollSpy
46 |
47 | , refresh: function () {
48 | var self = this
49 | , $targets
50 |
51 | this.offsets = $([])
52 | this.targets = $([])
53 |
54 | $targets = this.$body
55 | .find(this.selector)
56 | .map(function () {
57 | var $el = $(this)
58 | , href = $el.data('target') || $el.attr('href')
59 | , $href = /^#\w/.test(href) && $(href)
60 | return ( $href
61 | && $href.length
62 | && [[ $href.position().top, href ]] ) || null
63 | })
64 | .sort(function (a, b) { return a[0] - b[0] })
65 | .each(function () {
66 | self.offsets.push(this[0])
67 | self.targets.push(this[1])
68 | })
69 | }
70 |
71 | , process: function () {
72 | var scrollTop = this.$scrollElement.scrollTop() + this.options.offset
73 | , scrollHeight = this.$scrollElement[0].scrollHeight || this.$body[0].scrollHeight
74 | , maxScroll = scrollHeight - this.$scrollElement.height()
75 | , offsets = this.offsets
76 | , targets = this.targets
77 | , activeTarget = this.activeTarget
78 | , i
79 |
80 | if (scrollTop >= maxScroll) {
81 | return activeTarget != (i = targets.last()[0])
82 | && this.activate ( i )
83 | }
84 |
85 | for (i = offsets.length; i--;) {
86 | activeTarget != targets[i]
87 | && scrollTop >= offsets[i]
88 | && (!offsets[i + 1] || scrollTop <= offsets[i + 1])
89 | && this.activate( targets[i] )
90 | }
91 | }
92 |
93 | , activate: function (target) {
94 | var active
95 | , selector
96 |
97 | this.activeTarget = target
98 |
99 | $(this.selector)
100 | .parent('.active')
101 | .removeClass('active')
102 |
103 | selector = this.selector
104 | + '[data-target="' + target + '"],'
105 | + this.selector + '[href="' + target + '"]'
106 |
107 | active = $(selector)
108 | .parent('li')
109 | .addClass('active')
110 |
111 | if (active.parent('.dropdown-menu').length) {
112 | active = active.closest('li.dropdown').addClass('active')
113 | }
114 |
115 | active.trigger('activate')
116 | }
117 |
118 | }
119 |
120 |
121 | /* SCROLLSPY PLUGIN DEFINITION
122 | * =========================== */
123 |
124 | $.fn.scrollspy = function (option) {
125 | return this.each(function () {
126 | var $this = $(this)
127 | , data = $this.data('scrollspy')
128 | , options = typeof option == 'object' && option
129 | if (!data) $this.data('scrollspy', (data = new ScrollSpy(this, options)))
130 | if (typeof option == 'string') data[option]()
131 | })
132 | }
133 |
134 | $.fn.scrollspy.Constructor = ScrollSpy
135 |
136 | $.fn.scrollspy.defaults = {
137 | offset: 10
138 | }
139 |
140 |
141 | /* SCROLLSPY DATA-API
142 | * ================== */
143 |
144 | $(window).on('load', function () {
145 | $('[data-spy="scroll"]').each(function () {
146 | var $spy = $(this)
147 | $spy.scrollspy($spy.data())
148 | })
149 | })
150 |
151 | }(window.jQuery);
--------------------------------------------------------------------------------
/app/assets/js/bootstrap/bootstrap-tab.js:
--------------------------------------------------------------------------------
1 | /* ========================================================
2 | * bootstrap-tab.js v2.1.1
3 | * http://twitter.github.com/bootstrap/javascript.html#tabs
4 | * ========================================================
5 | * Copyright 2012 Twitter, Inc.
6 | *
7 | * Licensed under the Apache License, Version 2.0 (the "License");
8 | * you may not use this file except in compliance with the License.
9 | * You may obtain a copy of the License at
10 | *
11 | * http://www.apache.org/licenses/LICENSE-2.0
12 | *
13 | * Unless required by applicable law or agreed to in writing, software
14 | * distributed under the License is distributed on an "AS IS" BASIS,
15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 | * See the License for the specific language governing permissions and
17 | * limitations under the License.
18 | * ======================================================== */
19 |
20 |
21 | !function ($) {
22 |
23 | "use strict"; // jshint ;_;
24 |
25 |
26 | /* TAB CLASS DEFINITION
27 | * ==================== */
28 |
29 | var Tab = function (element) {
30 | this.element = $(element)
31 | }
32 |
33 | Tab.prototype = {
34 |
35 | constructor: Tab
36 |
37 | , show: function () {
38 | var $this = this.element
39 | , $ul = $this.closest('ul:not(.dropdown-menu)')
40 | , selector = $this.attr('data-target')
41 | , previous
42 | , $target
43 | , e
44 |
45 | if (!selector) {
46 | selector = $this.attr('href')
47 | selector = selector && selector.replace(/.*(?=#[^\s]*$)/, '') //strip for ie7
48 | }
49 |
50 | if ( $this.parent('li').hasClass('active') ) return
51 |
52 | previous = $ul.find('.active a').last()[0]
53 |
54 | e = $.Event('show', {
55 | relatedTarget: previous
56 | })
57 |
58 | $this.trigger(e)
59 |
60 | if (e.isDefaultPrevented()) return
61 |
62 | $target = $(selector)
63 |
64 | this.activate($this.parent('li'), $ul)
65 | this.activate($target, $target.parent(), function () {
66 | $this.trigger({
67 | type: 'shown'
68 | , relatedTarget: previous
69 | })
70 | })
71 | }
72 |
73 | , activate: function ( element, container, callback) {
74 | var $active = container.find('> .active')
75 | , transition = callback
76 | && $.support.transition
77 | && $active.hasClass('fade')
78 |
79 | function next() {
80 | $active
81 | .removeClass('active')
82 | .find('> .dropdown-menu > .active')
83 | .removeClass('active')
84 |
85 | element.addClass('active')
86 |
87 | if (transition) {
88 | element[0].offsetWidth // reflow for transition
89 | element.addClass('in')
90 | } else {
91 | element.removeClass('fade')
92 | }
93 |
94 | if ( element.parent('.dropdown-menu') ) {
95 | element.closest('li.dropdown').addClass('active')
96 | }
97 |
98 | callback && callback()
99 | }
100 |
101 | transition ?
102 | $active.one($.support.transition.end, next) :
103 | next()
104 |
105 | $active.removeClass('in')
106 | }
107 | }
108 |
109 |
110 | /* TAB PLUGIN DEFINITION
111 | * ===================== */
112 |
113 | $.fn.tab = function ( option ) {
114 | return this.each(function () {
115 | var $this = $(this)
116 | , data = $this.data('tab')
117 | if (!data) $this.data('tab', (data = new Tab(this)))
118 | if (typeof option == 'string') data[option]()
119 | })
120 | }
121 |
122 | $.fn.tab.Constructor = Tab
123 |
124 |
125 | /* TAB DATA-API
126 | * ============ */
127 |
128 | $(function () {
129 | $('body').on('click.tab.data-api', '[data-toggle="tab"], [data-toggle="pill"]', function (e) {
130 | e.preventDefault()
131 | $(this).tab('show')
132 | })
133 | })
134 |
135 | }(window.jQuery);
--------------------------------------------------------------------------------
/app/assets/js/bootstrap/bootstrap-tooltip.js:
--------------------------------------------------------------------------------
1 | /* ===========================================================
2 | * bootstrap-tooltip.js v2.1.1
3 | * http://twitter.github.com/bootstrap/javascript.html#tooltips
4 | * Inspired by the original jQuery.tipsy by Jason Frame
5 | * ===========================================================
6 | * Copyright 2012 Twitter, Inc.
7 | *
8 | * Licensed under the Apache License, Version 2.0 (the "License");
9 | * you may not use this file except in compliance with the License.
10 | * You may obtain a copy of the License at
11 | *
12 | * http://www.apache.org/licenses/LICENSE-2.0
13 | *
14 | * Unless required by applicable law or agreed to in writing, software
15 | * distributed under the License is distributed on an "AS IS" BASIS,
16 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 | * See the License for the specific language governing permissions and
18 | * limitations under the License.
19 | * ========================================================== */
20 |
21 |
22 | !function ($) {
23 |
24 | "use strict"; // jshint ;_;
25 |
26 |
27 | /* TOOLTIP PUBLIC CLASS DEFINITION
28 | * =============================== */
29 |
30 | var Tooltip = function (element, options) {
31 | this.init('tooltip', element, options)
32 | }
33 |
34 | Tooltip.prototype = {
35 |
36 | constructor: Tooltip
37 |
38 | , init: function (type, element, options) {
39 | var eventIn
40 | , eventOut
41 |
42 | this.type = type
43 | this.$element = $(element)
44 | this.options = this.getOptions(options)
45 | this.enabled = true
46 |
47 | if (this.options.trigger == 'click') {
48 | this.$element.on('click.' + this.type, this.options.selector, $.proxy(this.toggle, this))
49 | } else if (this.options.trigger != 'manual') {
50 | eventIn = this.options.trigger == 'hover' ? 'mouseenter' : 'focus'
51 | eventOut = this.options.trigger == 'hover' ? 'mouseleave' : 'blur'
52 | this.$element.on(eventIn + '.' + this.type, this.options.selector, $.proxy(this.enter, this))
53 | this.$element.on(eventOut + '.' + this.type, this.options.selector, $.proxy(this.leave, this))
54 | }
55 |
56 | this.options.selector ?
57 | (this._options = $.extend({}, this.options, { trigger: 'manual', selector: '' })) :
58 | this.fixTitle()
59 | }
60 |
61 | , getOptions: function (options) {
62 | options = $.extend({}, $.fn[this.type].defaults, options, this.$element.data())
63 |
64 | if (options.delay && typeof options.delay == 'number') {
65 | options.delay = {
66 | show: options.delay
67 | , hide: options.delay
68 | }
69 | }
70 |
71 | return options
72 | }
73 |
74 | , enter: function (e) {
75 | var self = $(e.currentTarget)[this.type](this._options).data(this.type)
76 |
77 | if (!self.options.delay || !self.options.delay.show) return self.show()
78 |
79 | clearTimeout(this.timeout)
80 | self.hoverState = 'in'
81 | this.timeout = setTimeout(function() {
82 | if (self.hoverState == 'in') self.show()
83 | }, self.options.delay.show)
84 | }
85 |
86 | , leave: function (e) {
87 | var self = $(e.currentTarget)[this.type](this._options).data(this.type)
88 |
89 | if (this.timeout) clearTimeout(this.timeout)
90 | if (!self.options.delay || !self.options.delay.hide) return self.hide()
91 |
92 | self.hoverState = 'out'
93 | this.timeout = setTimeout(function() {
94 | if (self.hoverState == 'out') self.hide()
95 | }, self.options.delay.hide)
96 | }
97 |
98 | , show: function () {
99 | var $tip
100 | , inside
101 | , pos
102 | , actualWidth
103 | , actualHeight
104 | , placement
105 | , tp
106 |
107 | if (this.hasContent() && this.enabled) {
108 | $tip = this.tip()
109 | this.setContent()
110 |
111 | if (this.options.animation) {
112 | $tip.addClass('fade')
113 | }
114 |
115 | placement = typeof this.options.placement == 'function' ?
116 | this.options.placement.call(this, $tip[0], this.$element[0]) :
117 | this.options.placement
118 |
119 | inside = /in/.test(placement)
120 |
121 | $tip
122 | .remove()
123 | .css({ top: 0, left: 0, display: 'block' })
124 | .appendTo(inside ? this.$element : document.body)
125 |
126 | pos = this.getPosition(inside)
127 |
128 | actualWidth = $tip[0].offsetWidth
129 | actualHeight = $tip[0].offsetHeight
130 |
131 | switch (inside ? placement.split(' ')[1] : placement) {
132 | case 'bottom':
133 | tp = {top: pos.top + pos.height, left: pos.left + pos.width / 2 - actualWidth / 2}
134 | break
135 | case 'top':
136 | tp = {top: pos.top - actualHeight, left: pos.left + pos.width / 2 - actualWidth / 2}
137 | break
138 | case 'left':
139 | tp = {top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left - actualWidth}
140 | break
141 | case 'right':
142 | tp = {top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left + pos.width}
143 | break
144 | }
145 |
146 | $tip
147 | .css(tp)
148 | .addClass(placement)
149 | .addClass('in')
150 | }
151 | }
152 |
153 | , setContent: function () {
154 | var $tip = this.tip()
155 | , title = this.getTitle()
156 |
157 | $tip.find('.tooltip-inner')[this.options.html ? 'html' : 'text'](title)
158 | $tip.removeClass('fade in top bottom left right')
159 | }
160 |
161 | , hide: function () {
162 | var that = this
163 | , $tip = this.tip()
164 |
165 | $tip.removeClass('in')
166 |
167 | function removeWithAnimation() {
168 | var timeout = setTimeout(function () {
169 | $tip.off($.support.transition.end).remove()
170 | }, 500)
171 |
172 | $tip.one($.support.transition.end, function () {
173 | clearTimeout(timeout)
174 | $tip.remove()
175 | })
176 | }
177 |
178 | $.support.transition && this.$tip.hasClass('fade') ?
179 | removeWithAnimation() :
180 | $tip.remove()
181 |
182 | return this
183 | }
184 |
185 | , fixTitle: function () {
186 | var $e = this.$element
187 | if ($e.attr('title') || typeof($e.attr('data-original-title')) != 'string') {
188 | $e.attr('data-original-title', $e.attr('title') || '').removeAttr('title')
189 | }
190 | }
191 |
192 | , hasContent: function () {
193 | return this.getTitle()
194 | }
195 |
196 | , getPosition: function (inside) {
197 | return $.extend({}, (inside ? {top: 0, left: 0} : this.$element.offset()), {
198 | width: this.$element[0].offsetWidth
199 | , height: this.$element[0].offsetHeight
200 | })
201 | }
202 |
203 | , getTitle: function () {
204 | var title
205 | , $e = this.$element
206 | , o = this.options
207 |
208 | title = $e.attr('data-original-title')
209 | || (typeof o.title == 'function' ? o.title.call($e[0]) : o.title)
210 |
211 | return title
212 | }
213 |
214 | , tip: function () {
215 | return this.$tip = this.$tip || $(this.options.template)
216 | }
217 |
218 | , validate: function () {
219 | if (!this.$element[0].parentNode) {
220 | this.hide()
221 | this.$element = null
222 | this.options = null
223 | }
224 | }
225 |
226 | , enable: function () {
227 | this.enabled = true
228 | }
229 |
230 | , disable: function () {
231 | this.enabled = false
232 | }
233 |
234 | , toggleEnabled: function () {
235 | this.enabled = !this.enabled
236 | }
237 |
238 | , toggle: function () {
239 | this[this.tip().hasClass('in') ? 'hide' : 'show']()
240 | }
241 |
242 | , destroy: function () {
243 | this.hide().$element.off('.' + this.type).removeData(this.type)
244 | }
245 |
246 | }
247 |
248 |
249 | /* TOOLTIP PLUGIN DEFINITION
250 | * ========================= */
251 |
252 | $.fn.tooltip = function ( option ) {
253 | return this.each(function () {
254 | var $this = $(this)
255 | , data = $this.data('tooltip')
256 | , options = typeof option == 'object' && option
257 | if (!data) $this.data('tooltip', (data = new Tooltip(this, options)))
258 | if (typeof option == 'string') data[option]()
259 | })
260 | }
261 |
262 | $.fn.tooltip.Constructor = Tooltip
263 |
264 | $.fn.tooltip.defaults = {
265 | animation: true
266 | , placement: 'top'
267 | , selector: false
268 | , template: ''
269 | , trigger: 'hover'
270 | , title: ''
271 | , delay: 0
272 | , html: true
273 | }
274 |
275 | }(window.jQuery);
276 |
--------------------------------------------------------------------------------
/app/assets/js/bootstrap/bootstrap-transition.js:
--------------------------------------------------------------------------------
1 | /* ===================================================
2 | * bootstrap-transition.js v2.1.1
3 | * http://twitter.github.com/bootstrap/javascript.html#transitions
4 | * ===================================================
5 | * Copyright 2012 Twitter, Inc.
6 | *
7 | * Licensed under the Apache License, Version 2.0 (the "License");
8 | * you may not use this file except in compliance with the License.
9 | * You may obtain a copy of the License at
10 | *
11 | * http://www.apache.org/licenses/LICENSE-2.0
12 | *
13 | * Unless required by applicable law or agreed to in writing, software
14 | * distributed under the License is distributed on an "AS IS" BASIS,
15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 | * See the License for the specific language governing permissions and
17 | * limitations under the License.
18 | * ========================================================== */
19 |
20 |
21 | !function ($) {
22 |
23 | $(function () {
24 |
25 | "use strict"; // jshint ;_;
26 |
27 |
28 | /* CSS TRANSITION SUPPORT (http://www.modernizr.com/)
29 | * ======================================================= */
30 |
31 | $.support.transition = (function () {
32 |
33 | var transitionEnd = (function () {
34 |
35 | var el = document.createElement('bootstrap')
36 | , transEndEventNames = {
37 | 'WebkitTransition' : 'webkitTransitionEnd'
38 | , 'MozTransition' : 'transitionend'
39 | , 'OTransition' : 'oTransitionEnd otransitionend'
40 | , 'transition' : 'transitionend'
41 | }
42 | , name
43 |
44 | for (name in transEndEventNames){
45 | if (el.style[name] !== undefined) {
46 | return transEndEventNames[name]
47 | }
48 | }
49 |
50 | }())
51 |
52 | return transitionEnd && {
53 | end: transitionEnd
54 | }
55 |
56 | })()
57 |
58 | })
59 |
60 | }(window.jQuery);
--------------------------------------------------------------------------------
/app/assets/js/bootstrap/bootstrap-typeahead.js:
--------------------------------------------------------------------------------
1 | /* =============================================================
2 | * bootstrap-typeahead.js v2.1.1
3 | * http://twitter.github.com/bootstrap/javascript.html#typeahead
4 | * =============================================================
5 | * Copyright 2012 Twitter, Inc.
6 | *
7 | * Licensed under the Apache License, Version 2.0 (the "License");
8 | * you may not use this file except in compliance with the License.
9 | * You may obtain a copy of the License at
10 | *
11 | * http://www.apache.org/licenses/LICENSE-2.0
12 | *
13 | * Unless required by applicable law or agreed to in writing, software
14 | * distributed under the License is distributed on an "AS IS" BASIS,
15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 | * See the License for the specific language governing permissions and
17 | * limitations under the License.
18 | * ============================================================ */
19 |
20 |
21 | !function($){
22 |
23 | "use strict"; // jshint ;_;
24 |
25 |
26 | /* TYPEAHEAD PUBLIC CLASS DEFINITION
27 | * ================================= */
28 |
29 | var Typeahead = function (element, options) {
30 | this.$element = $(element)
31 | this.options = $.extend({}, $.fn.typeahead.defaults, options)
32 | this.matcher = this.options.matcher || this.matcher
33 | this.sorter = this.options.sorter || this.sorter
34 | this.highlighter = this.options.highlighter || this.highlighter
35 | this.updater = this.options.updater || this.updater
36 | this.$menu = $(this.options.menu).appendTo('body')
37 | this.source = this.options.source
38 | this.shown = false
39 | this.listen()
40 | }
41 |
42 | Typeahead.prototype = {
43 |
44 | constructor: Typeahead
45 |
46 | , select: function () {
47 | var val = this.$menu.find('.active').attr('data-value')
48 | this.$element
49 | .val(this.updater(val))
50 | .change()
51 | return this.hide()
52 | }
53 |
54 | , updater: function (item) {
55 | return item
56 | }
57 |
58 | , show: function () {
59 | var pos = $.extend({}, this.$element.offset(), {
60 | height: this.$element[0].offsetHeight
61 | })
62 |
63 | this.$menu.css({
64 | top: pos.top + pos.height
65 | , left: pos.left
66 | })
67 |
68 | this.$menu.show()
69 | this.shown = true
70 | return this
71 | }
72 |
73 | , hide: function () {
74 | this.$menu.hide()
75 | this.shown = false
76 | return this
77 | }
78 |
79 | , lookup: function (event) {
80 | var items
81 |
82 | this.query = this.$element.val()
83 |
84 | if (!this.query || this.query.length < this.options.minLength) {
85 | return this.shown ? this.hide() : this
86 | }
87 |
88 | items = $.isFunction(this.source) ? this.source(this.query, $.proxy(this.process, this)) : this.source
89 |
90 | return items ? this.process(items) : this
91 | }
92 |
93 | , process: function (items) {
94 | var that = this
95 |
96 | items = $.grep(items, function (item) {
97 | return that.matcher(item)
98 | })
99 |
100 | items = this.sorter(items)
101 |
102 | if (!items.length) {
103 | return this.shown ? this.hide() : this
104 | }
105 |
106 | return this.render(items.slice(0, this.options.items)).show()
107 | }
108 |
109 | , matcher: function (item) {
110 | return ~item.toLowerCase().indexOf(this.query.toLowerCase())
111 | }
112 |
113 | , sorter: function (items) {
114 | var beginswith = []
115 | , caseSensitive = []
116 | , caseInsensitive = []
117 | , item
118 |
119 | while (item = items.shift()) {
120 | if (!item.toLowerCase().indexOf(this.query.toLowerCase())) beginswith.push(item)
121 | else if (~item.indexOf(this.query)) caseSensitive.push(item)
122 | else caseInsensitive.push(item)
123 | }
124 |
125 | return beginswith.concat(caseSensitive, caseInsensitive)
126 | }
127 |
128 | , highlighter: function (item) {
129 | var query = this.query.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g, '\\$&')
130 | return item.replace(new RegExp('(' + query + ')', 'ig'), function ($1, match) {
131 | return '' + match + ' '
132 | })
133 | }
134 |
135 | , render: function (items) {
136 | var that = this
137 |
138 | items = $(items).map(function (i, item) {
139 | i = $(that.options.item).attr('data-value', item)
140 | i.find('a').html(that.highlighter(item))
141 | return i[0]
142 | })
143 |
144 | items.first().addClass('active')
145 | this.$menu.html(items)
146 | return this
147 | }
148 |
149 | , next: function (event) {
150 | var active = this.$menu.find('.active').removeClass('active')
151 | , next = active.next()
152 |
153 | if (!next.length) {
154 | next = $(this.$menu.find('li')[0])
155 | }
156 |
157 | next.addClass('active')
158 | }
159 |
160 | , prev: function (event) {
161 | var active = this.$menu.find('.active').removeClass('active')
162 | , prev = active.prev()
163 |
164 | if (!prev.length) {
165 | prev = this.$menu.find('li').last()
166 | }
167 |
168 | prev.addClass('active')
169 | }
170 |
171 | , listen: function () {
172 | this.$element
173 | .on('blur', $.proxy(this.blur, this))
174 | .on('keypress', $.proxy(this.keypress, this))
175 | .on('keyup', $.proxy(this.keyup, this))
176 |
177 | if ($.browser.chrome || $.browser.webkit || $.browser.msie) {
178 | this.$element.on('keydown', $.proxy(this.keydown, this))
179 | }
180 |
181 | this.$menu
182 | .on('click', $.proxy(this.click, this))
183 | .on('mouseenter', 'li', $.proxy(this.mouseenter, this))
184 | }
185 |
186 | , move: function (e) {
187 | if (!this.shown) return
188 |
189 | switch(e.keyCode) {
190 | case 9: // tab
191 | case 13: // enter
192 | case 27: // escape
193 | e.preventDefault()
194 | break
195 |
196 | case 38: // up arrow
197 | e.preventDefault()
198 | this.prev()
199 | break
200 |
201 | case 40: // down arrow
202 | e.preventDefault()
203 | this.next()
204 | break
205 | }
206 |
207 | e.stopPropagation()
208 | }
209 |
210 | , keydown: function (e) {
211 | this.suppressKeyPressRepeat = !~$.inArray(e.keyCode, [40,38,9,13,27])
212 | this.move(e)
213 | }
214 |
215 | , keypress: function (e) {
216 | if (this.suppressKeyPressRepeat) return
217 | this.move(e)
218 | }
219 |
220 | , keyup: function (e) {
221 | switch(e.keyCode) {
222 | case 40: // down arrow
223 | case 38: // up arrow
224 | break
225 |
226 | case 9: // tab
227 | case 13: // enter
228 | if (!this.shown) return
229 | this.select()
230 | break
231 |
232 | case 27: // escape
233 | if (!this.shown) return
234 | this.hide()
235 | break
236 |
237 | default:
238 | this.lookup()
239 | }
240 |
241 | e.stopPropagation()
242 | e.preventDefault()
243 | }
244 |
245 | , blur: function (e) {
246 | var that = this
247 | setTimeout(function () { that.hide() }, 150)
248 | }
249 |
250 | , click: function (e) {
251 | e.stopPropagation()
252 | e.preventDefault()
253 | this.select()
254 | }
255 |
256 | , mouseenter: function (e) {
257 | this.$menu.find('.active').removeClass('active')
258 | $(e.currentTarget).addClass('active')
259 | }
260 |
261 | }
262 |
263 |
264 | /* TYPEAHEAD PLUGIN DEFINITION
265 | * =========================== */
266 |
267 | $.fn.typeahead = function (option) {
268 | return this.each(function () {
269 | var $this = $(this)
270 | , data = $this.data('typeahead')
271 | , options = typeof option == 'object' && option
272 | if (!data) $this.data('typeahead', (data = new Typeahead(this, options)))
273 | if (typeof option == 'string') data[option]()
274 | })
275 | }
276 |
277 | $.fn.typeahead.defaults = {
278 | source: []
279 | , items: 8
280 | , menu: ''
281 | , item: ' '
282 | , minLength: 1
283 | }
284 |
285 | $.fn.typeahead.Constructor = Typeahead
286 |
287 |
288 | /* TYPEAHEAD DATA-API
289 | * ================== */
290 |
291 | $(function () {
292 | $('body').on('focus.typeahead.data-api', '[data-provide="typeahead"]', function (e) {
293 | var $this = $(this)
294 | if ($this.data('typeahead')) return
295 | e.preventDefault()
296 | $this.typeahead($this.data())
297 | })
298 | })
299 |
300 | }(window.jQuery);
301 |
--------------------------------------------------------------------------------
/app/assets/js/home.js:
--------------------------------------------------------------------------------
1 |
2 | $(document).ready(function() {
3 | $("#go").submit(function() {
4 | var val = $("#topic").val()
5 | if(val.length > 0) {
6 | window.location = "/topics/" + val;
7 | }
8 | return false;
9 | });
10 | });
11 |
--------------------------------------------------------------------------------
/app/assets/js/html5-shiv.js:
--------------------------------------------------------------------------------
1 | /*! HTML5 Shiv vpre3.6 | @afarkas @jdalton @jon_neal @rem | MIT/GPL2 Licensed
2 | Uncompressed source: https://github.com/aFarkas/html5shiv */
3 | (function(a,b){function h(a,b){var c=a.createElement("p"),d=a.getElementsByTagName("head")[0]||a.documentElement;return c.innerHTML="x",d.insertBefore(c.lastChild,d.firstChild)}function i(){var a=l.elements;return typeof a=="string"?a.split(" "):a}function j(a){var b={},c=a.createElement,f=a.createDocumentFragment,g=f();a.createElement=function(a){if(!l.shivMethods)return c(a);var f;return b[a]?f=b[a].cloneNode():e.test(a)?f=(b[a]=c(a)).cloneNode():f=c(a),f.canHaveChildren&&!d.test(a)?g.appendChild(f):f},a.createDocumentFragment=Function("h,f","return function(){var n=f.cloneNode(),c=n.createElement;h.shivMethods&&("+i().join().replace(/\w+/g,function(a){return c(a),g.createElement(a),'c("'+a+'")'})+");return n}")(l,g)}function k(a){var b;return a.documentShived?a:(l.shivCSS&&!f&&(b=!!h(a,"article,aside,details,figcaption,figure,footer,header,hgroup,nav,section{display:block}audio{display:none}canvas,video{display:inline-block;*display:inline;*zoom:1}[hidden]{display:none}audio[controls]{display:inline-block;*display:inline;*zoom:1}mark{background:#FF0;color:#000}")),g||(b=!j(a)),b&&(a.documentShived=b),a)}var c=a.html5||{},d=/^<|^(?:button|form|map|select|textarea|object|iframe|option|optgroup)$/i,e=/^<|^(?:a|b|button|code|div|fieldset|form|h1|h2|h3|h4|h5|h6|i|iframe|img|input|label|li|link|ol|option|p|param|q|script|select|span|strong|style|table|tbody|td|textarea|tfoot|th|thead|tr|ul)$/i,f,g;(function(){var c=b.createElement("a");c.innerHTML=" ",f="hidden"in c,f&&typeof injectElementWithStyles=="function"&&injectElementWithStyles("#modernizr{}",function(b){b.hidden=!0,f=(a.getComputedStyle?getComputedStyle(b,null):b.currentStyle).display=="none"}),g=c.childNodes.length==1||function(){try{b.createElement("a")}catch(a){return!0}var c=b.createDocumentFragment();return typeof c.cloneNode=="undefined"||typeof c.createDocumentFragment=="undefined"||typeof c.createElement=="undefined"}()})();var l={elements:c.elements||"abbr article aside audio bdi canvas data datalist details figcaption figure footer header hgroup mark meter nav output progress section summary time video",shivCSS:c.shivCSS!==!1,shivMethods:c.shivMethods!==!1,type:"default",shivDocument:k};a.html5=l,k(b)})(this,document)
--------------------------------------------------------------------------------
/app/assets/js/topics.js:
--------------------------------------------------------------------------------
1 | //= require ./jsonlint.js
2 |
3 | function setupTopic(topic) {
4 | var url = window.location.protocol + '//' + window.location.hostname;
5 | if (window.location.port) {
6 | url += ':' + window.location.port;
7 | }
8 | var socket = io.connect(url)
9 |
10 | var textarea = $("#payload");
11 | var textarea_error = $("#payload-error");
12 | var textarea_field = $("#payload-field");
13 | var update = $("#update");
14 | var edit = $("#edit");
15 | var cancel = $("#cancel");
16 | var validate = $("#validate");
17 |
18 | update.hide();
19 | cancel.hide();
20 |
21 | var last_data = null;
22 |
23 | update_content = function(data) {
24 | data = { payload: data }
25 | last_data = data;
26 | if(textarea.attr("readonly")) {
27 | if(typeof data.payload === "string") {
28 | textarea.val(data.payload);
29 | validate.removeAttr("checked");
30 | } else {
31 | textarea.val(JSON.stringify(data.payload, null, 4));
32 | validate.attr("checked", true);
33 | last_data.json = true;
34 | }
35 | }
36 | };
37 |
38 | socket.on("connect", function(data) {
39 | socket.emit('subscribe', topic);
40 | });
41 |
42 | socket.on("/topics/" + topic, function(data) {
43 | update_content(data);
44 | });
45 |
46 | update.click(function() {
47 | var val = textarea.val();
48 | var settings = {
49 | url: "/topics/"+ topic,
50 | type: "PUT"
51 | }
52 | if(validate.attr("checked")) {
53 | try {
54 | val = JSON.parse(val);
55 | if(typeof val == 'string') {
56 | throw "invalid json";
57 | }
58 | settings.data = JSON.stringify(val);
59 | settings.contentType = "application/json";
60 | } catch(e) {
61 | textarea.addClass("error");
62 | textarea_field.addClass("error");
63 | textarea_error.show();
64 | return false;
65 | }
66 | } else {
67 | settings.data = { payload: val };
68 | }
69 |
70 | $.ajax(settings);
71 | textarea.removeClass("error");
72 | textarea_field.removeClass("error");
73 | textarea_error.hide();
74 |
75 | textarea.attr("readonly", true);
76 | validate.attr("disabled", true);
77 | edit.toggle();
78 | update.toggle();
79 | cancel.toggle();
80 | return false;
81 | });
82 |
83 | edit.click(function() {
84 | textarea.removeAttr("readonly");
85 | validate.removeAttr("disabled");
86 | update.toggle();
87 | cancel.toggle();
88 | edit.toggle();
89 | return false;
90 | });
91 |
92 | cancel.click(function() {
93 | if(last_data.json) {
94 | textarea.val(JSON.stringify(last_data.payload));
95 | } else {
96 | textarea.val(last_data.payload);
97 | }
98 | validate.attr("checked", last_data.json);
99 | textarea.attr("readonly", true);
100 | validate.attr("disabled", true);
101 | update.toggle();
102 | cancel.toggle();
103 | edit.toggle();
104 |
105 | textarea.removeClass("error");
106 | textarea_field.removeClass("error");
107 | textarea_error.hide();
108 | return false;
109 | });
110 | }
111 |
--------------------------------------------------------------------------------
/app/controllers/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mcollina/qest/810f720b61c47fa74cdf7bb2e3ea616c4e548ffd/app/controllers/.gitkeep
--------------------------------------------------------------------------------
/app/controllers/home.coffee:
--------------------------------------------------------------------------------
1 |
2 | module.exports = (app) ->
3 | app.get '/', (req, res) ->
4 | req.session.topics ||= []
5 | res.render 'home.hbs', topics: req.session.topics
6 |
--------------------------------------------------------------------------------
/app/controllers/http_api.coffee:
--------------------------------------------------------------------------------
1 |
2 | module.exports = (app) ->
3 | io = app.io
4 | Data = app.models.Data
5 |
6 | app.get (/^\/topics\/(.+)$/), (req, res) ->
7 | topic = req.params[0]
8 |
9 | topics = req.session.topics || []
10 | index = topics.indexOf(topic)
11 |
12 | if index >= 0
13 | topics = [].concat(topics.splice(0, index), topics.splice(index + 1, req.session.topics.length))
14 |
15 | topics.push(topic)
16 | topics.pull() if topics.length > 5
17 |
18 | req.session.topics = topics
19 |
20 | Data.find topic, (err, data) ->
21 |
22 | type = req.accepts(['txt', 'json', 'html'])
23 |
24 | if type == "html"
25 | res.render 'topic.hbs', topic: topic
26 | else if err?
27 | res.send 404
28 | else if type == 'json'
29 | res.contentType('json')
30 | try
31 | res.json data.value
32 | catch e
33 | res.json "" + data.value
34 | else if type == 'txt'
35 | res.send data.value
36 | else
37 | res.send 406
38 |
39 | app.put /^\/topics\/(.+)$/, (req, res) ->
40 | topic = req.params[0]
41 | if req.is("json")
42 | payload = req.body
43 | else
44 | payload = req.body.payload
45 | Data.findOrCreate topic, payload
46 | res.send 204
47 |
--------------------------------------------------------------------------------
/app/controllers/mqtt_api.coffee:
--------------------------------------------------------------------------------
1 |
2 | module.exports = (app) ->
3 | Data = app.models.Data
4 |
5 | (client) ->
6 |
7 | listeners = {}
8 |
9 | unsubscribeAll = ->
10 | for topic, listener of listeners
11 | Data.unsubscribe(topic, listener)
12 |
13 | client.on 'connect', (packet) ->
14 | client.id = packet.client
15 | client.connack(returnCode: 0)
16 |
17 | client.on 'subscribe', (packet) ->
18 | granted = []
19 | subscriptions = []
20 |
21 | for subscription in packet.subscriptions
22 | # '#' is 'match anything to the end of the string' */
23 | # + is 'match anything but a / until you hit a /' */
24 | subscriptions.push(subscription.topic.replace("#", "*"))
25 | granted.push 0
26 |
27 | client.suback(messageId: packet.messageId, granted: granted)
28 |
29 | # subscribe for updates
30 | for subscription in subscriptions
31 | (->
32 | listener = (data) ->
33 | try
34 | if typeof data.value == "string"
35 | value = data.value
36 | else
37 | value = data.jsonValue
38 | client.publish(topic: data.key, payload: value)
39 | catch error
40 | console.log error
41 | client.close()
42 | listeners[subscription] = listener
43 | Data.subscribe(subscription, listener)
44 |
45 | Data.find new RegExp(subscription), (err, data) ->
46 | throw err if err? # the persistance layer is not working properly
47 | listener(data)
48 | )()
49 |
50 | client.on 'publish', (packet) ->
51 | payload = packet.payload
52 | try
53 | payload = JSON.parse(payload)
54 | catch error
55 | # nothing to do
56 | Data.findOrCreate packet.topic, payload
57 |
58 | client.on 'pingreq', (packet) ->
59 | client.pingresp()
60 |
61 | client.on 'disconnect', ->
62 | client.stream.end()
63 |
64 | client.on 'error', (error) ->
65 | console.log error
66 | client.stream.end()
67 |
68 | client.on 'close', (err) ->
69 | unsubscribeAll()
70 |
71 | client.on 'unsubscribe', (packet) ->
72 | client.unsuback(messageId: packet.messageId)
73 |
--------------------------------------------------------------------------------
/app/controllers/websocket_api.coffee:
--------------------------------------------------------------------------------
1 |
2 | module.exports = (app) ->
3 | Data = app.models.Data
4 |
5 | app.io.sockets.on 'connection', (socket) ->
6 |
7 | subscriptions = {}
8 |
9 | socket.on 'subscribe', (topic) ->
10 |
11 | subscription = (currentData) ->
12 | socket.emit("/topics/#{topic}", currentData.value)
13 |
14 | subscriptions[topic] = subscription
15 |
16 | Data.subscribe topic, subscription
17 |
18 | Data.find topic, (err, data) ->
19 | subscription(data) if data?.value?
20 |
21 | socket.on 'disconnect', ->
22 | for topic, listener of subscriptions
23 | Data.unsubscribe(topic, listener)
24 |
--------------------------------------------------------------------------------
/app/helpers/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mcollina/qest/810f720b61c47fa74cdf7bb2e3ea616c4e548ffd/app/helpers/.gitkeep
--------------------------------------------------------------------------------
/app/helpers/asset_pipeline.coffee:
--------------------------------------------------------------------------------
1 | hbs = require 'hbs'
2 | path = require 'path'
3 | Mincer = require('mincer')
4 |
5 | module.exports = (app) ->
6 | environment = new Mincer.Environment()
7 | environment.appendPath('app/assets/js')
8 | environment.appendPath('app/assets/css')
9 |
10 | app.use("/assets", Mincer.createServer(environment))
11 |
12 | # dummy helper that injects extension
13 | rewrite_extension = (source, ext) ->
14 | source_ext = path.extname(source)
15 | if (source_ext == ext)
16 | source
17 | else
18 | (source + ext)
19 |
20 | # returns a list of asset paths
21 | find_asset_paths = (logicalPath, ext) ->
22 | asset = environment.findAsset(logicalPath)
23 | paths = []
24 |
25 | if (!asset)
26 | return null
27 |
28 | if ('production' != process.env.NODE_ENV && asset.isCompiled)
29 | asset.toArray().forEach (dep) ->
30 | paths.push('/assets/' + rewrite_extension(dep.logicalPath, ext) + '?body=1')
31 | else
32 | paths.push('/assets/' + rewrite_extension(asset.digestPath, ext))
33 |
34 | return paths
35 |
36 | hbs.registerHelper 'js', (logicalPath) ->
37 | paths = find_asset_paths(logicalPath, ".js")
38 |
39 | if (!paths)
40 | # this will help us notify that given logicalPath is not found
41 | # without "breaking" view renderer
42 | return new hbs.SafeString('')
45 |
46 | result = paths.map (path) ->
47 | ''
48 | new hbs.SafeString(result.join("\n"))
49 |
50 | hbs.registerHelper 'css', (logicalPath) ->
51 | paths = find_asset_paths(logicalPath, ".css")
52 |
53 | if (!paths)
54 | # this will help us notify that given logicalPath is not found
55 | # without "breaking" view renderer
56 | return new hbs.SafeString('')
59 |
60 | result = paths.map (path) ->
61 | ' '
62 | new hbs.SafeString(result.join("\n"))
63 |
--------------------------------------------------------------------------------
/app/helpers/json.coffee:
--------------------------------------------------------------------------------
1 |
2 |
3 | hbs = require 'hbs'
4 |
5 | module.exports = (app) ->
6 | hbs.registerHelper 'json', (context) ->
7 | new hbs.SafeString(JSON.stringify(context))
8 |
--------------------------------------------------------------------------------
/app/helpers/markdown.coffee:
--------------------------------------------------------------------------------
1 |
2 | hbs = require 'hbs'
3 |
4 | module.exports = (app) ->
5 | hbs.registerHelper 'markdown', (options) ->
6 | input = options.fn(@)
7 | result = require( "markdown" ).markdown.toHTML(input)
8 | return result
9 |
--------------------------------------------------------------------------------
/app/helpers/notest.coffee:
--------------------------------------------------------------------------------
1 |
2 | hbs = require 'hbs'
3 |
4 | module.exports = (app) ->
5 | hbs.registerHelper 'notest', (options) ->
6 | if process.env.NODE_ENV != "test"
7 | input = options.fn(@)
8 | return input
9 | else
10 | return ""
11 |
--------------------------------------------------------------------------------
/app/models/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mcollina/qest/810f720b61c47fa74cdf7bb2e3ea616c4e548ffd/app/models/.gitkeep
--------------------------------------------------------------------------------
/app/models/data.coffee:
--------------------------------------------------------------------------------
1 |
2 | EventEmitter = require('events').EventEmitter
3 |
4 | globalEventEmitter = new EventEmitter()
5 | globalEventEmitter.setMaxListeners(0)
6 | events = {}
7 |
8 | KEYS_SET_NAME = 'topics'
9 |
10 | module.exports = (app) ->
11 | buildKey = (key) ->
12 | "topic:#{key}"
13 |
14 | class Data
15 |
16 | constructor: (@key, @value) ->
17 | @value ||= null
18 |
19 | Object.defineProperty @prototype, 'key',
20 | enumerable: true
21 | configurable: false
22 | get: -> @_key
23 | set: (key) ->
24 | @redisKey = buildKey(key)
25 | @_key = key
26 |
27 | Object.defineProperty @prototype, 'jsonValue',
28 | configurable: false
29 | enumerable: true
30 | get: ->
31 | JSON.stringify(@value)
32 |
33 | set: (val) ->
34 | @value = JSON.parse(val)
35 |
36 | save: (callback) ->
37 | app.redis.client.set @redisKey, @jsonValue, (err) =>
38 | app.ascoltatore.publish @key, @value, =>
39 | callback(err, @) if callback?
40 |
41 | app.redis.client.sadd KEYS_SET_NAME, @key
42 |
43 | Data.find = (pattern, callback) ->
44 |
45 | foundRecord = (key) ->
46 | app.redis.client.get buildKey(key), (err, value) ->
47 | if err
48 | callback(err) if callback?
49 | return
50 |
51 | unless value?
52 | callback("Record not found") if callback?
53 | return
54 |
55 | callback(null, Data.fromRedis(key, value)) if callback?
56 |
57 | if pattern.constructor != RegExp
58 | foundRecord(pattern)
59 | else
60 | app.redis.client.smembers KEYS_SET_NAME, (err, topics) ->
61 | for topic in topics
62 | foundRecord(topic) if pattern.test(topic)
63 |
64 | Data
65 |
66 | Data.findOrCreate = ->
67 | args = Array.prototype.slice.call arguments
68 |
69 | key = args.shift() # first arg shifted out
70 | arg = args.shift() # second arg popped out
71 |
72 | if typeof arg == 'function'
73 | # if the second arg is a function,
74 | # then there is no third arg
75 | callback = arg
76 | else
77 | # if the second arg is not a function
78 | # then it's the value, and the third is
79 | # the callback
80 | value = arg
81 | callback = args.shift()
82 |
83 | # FIXME this is not atomic, is it a problem?
84 | app.redis.client.get buildKey(key), (err, oldValue) ->
85 | data = Data.fromRedis(key, oldValue)
86 | data.value = value if value?
87 | data.save(callback)
88 |
89 | Data
90 |
91 | Data.fromRedis = (topic, value) ->
92 | data = new Data(topic)
93 | data.jsonValue = value
94 | data
95 |
96 | Data.subscribe = (topic, callback) ->
97 | callback._subscriber = (actualTopic, value) ->
98 | callback(new Data(actualTopic, value))
99 | app.ascoltatore.subscribe topic, callback._subscriber
100 | @
101 |
102 | Data.unsubscribe = (topic, callback) ->
103 | app.ascoltatore.unsubscribe topic, callback._subscriber
104 | @
105 |
106 | Data
107 |
--------------------------------------------------------------------------------
/app/views/home.hbs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
QEST
8 | The stargate of devices
9 |
10 |
19 |
20 |
21 |
22 |
27 |
28 |
Your latest topics
29 | {{#if topics.length }}
30 |
31 | {{#each topics}}
32 | {{this}}
33 | {{/each}}
34 |
35 | {{else}}
36 |
37 | No topics found, let‘s open a new one!
38 |
39 | {{/if}}
40 |
41 |
42 |
43 |
44 |
45 |
46 |
Building
47 | {{#markdown}}
48 | QEST is a stargate between the universe of devices which speak [MQTT](http://mqtt.org), and the universe of apps which
49 | speak [HTTP](http://en.wikipedia.org/wiki/HTTP) and [REST](http://en.wikipedia.org/wiki/REST).
50 | In this way you don't have to deal any custom protocol, you just GET and PUT the topic URI, like these:
51 |
52 | $ curl -X PUT -d '{ "hello": 555 }' \
53 | -H "Content-Type: application/json" \
54 | http://mqtt.matteocollina.com/topics/prova
55 | $ curl http://mqtt.matteocollina.com/topics/prova
56 | { "hello": 555 }
57 |
58 | Let's build cool things with MQTT, REST and Arduino!
59 | {{/markdown}}
60 |
61 |
62 |
63 |
64 |
Dreaming
65 | {{#markdown}}
66 | Here we are dreaming a Web of Things, where you can reach (and interact) with each of your "real" devices using the web,
67 | as it's the Way everybody interacts with a computer these days.
68 | However it's somewhat hard to build these kind of apps, so researchers have written custom protocols for communicating
69 | with the devices.
70 |
71 | The state-of-the-art protocol for __devices__ is [MQTT](http://mqtt.org), which is standard, free of royalties, and widespread:
72 | there are libraries for all the major platforms.
73 |
74 | The state-of-the-art protocol for __apps__ are [REST](http://en.wikipedia.org/wiki/REST) and [HTTP](http://en.wikipedia.org/wiki/HTTP),
75 | so why can't we bridge them? So QEST was born.
76 | {{/markdown}}
77 |
78 |
79 |
80 |
81 |
88 |
89 |
90 |
Examples:
91 | {{#markdown}}
92 | * [NetworkButton](https://github.com/mcollina/qest/wiki/Network-Button-Example)
93 | * [NetworkButtonJSON](https://gist.github.com/mcollina/5337389), same as
94 | before, but exchanging JSONs.
95 | {{/markdown}}
96 |
97 |
98 | {{ js "home.js" }}
99 |
--------------------------------------------------------------------------------
/app/views/layout.hbs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | QEST - The Internet of Things broker that loves devices and developers
5 | {{css "main.css"}}
6 |
9 | {{js "bootstrap.js"}}
10 |
11 |
12 |
13 |
33 |
34 |
35 |
36 |
37 | {{{body}}}
38 |
39 |
40 |
43 |
44 |
45 |
46 | {{#notest}}
47 |
48 |
68 |
69 | {{/notest}}
70 |
71 |
72 |
73 |
--------------------------------------------------------------------------------
/app/views/network_button.hbs:
--------------------------------------------------------------------------------
1 |
2 |
3 | {{#markdown}}
4 | NetworkButton Example
5 | -----
6 |
7 | NetworkButton is a forest of network enabled leds that switch on and off on simultaneosly.
8 | It's also an example with MQTT-REST.
9 |
10 | Let's build your own!
11 | {{/markdown}}
12 |
13 | {{#notest}}
14 |
15 | {{/notest}}
16 |
17 |
18 |
--------------------------------------------------------------------------------
/app/views/topic.hbs:
--------------------------------------------------------------------------------
1 |
4 |
30 |
31 |
32 | {{ js "topics.js" }}
33 |
36 |
--------------------------------------------------------------------------------
/benchmarks/bench_http_mqtt_multiple_pub.coffee:
--------------------------------------------------------------------------------
1 | #! /usr/bin/env coffee
2 |
3 | request = require("request")
4 | Pool = require("./mqtt_client_pool").Pool
5 |
6 | host = "localhost"
7 | mqtt_port = 1883
8 | http_port = 3000
9 |
10 | Benchmark = require 'benchmark'
11 |
12 | pool = new Pool(host, mqtt_port)
13 |
14 | suite = new Benchmark.Suite
15 |
16 | payload = 0
17 |
18 | setup_bench = (suite, number) ->
19 | suite.add("#{number} publishes", (d) ->
20 | payload += 1
21 | publish_count = 0
22 | subscribed_count = 0
23 | topic = "bench/#{number}"
24 |
25 | pool.get (client) ->
26 |
27 | # when we receive an update
28 | client.on 'publish', (packet) ->
29 |
30 | if String(packet.payload) == String(number)
31 | # we unsubscribe from the topic
32 | client.unsubscribe(topic: topic)
33 | client.removeAllListeners('publish')
34 | client.removeAllListeners('suback')
35 |
36 | # we resolve the benchmark as we have received all the updates
37 | d.resolve()
38 |
39 | client.on 'unsuback', ->
40 | # and we put the client back to the pool
41 | pool.release(client)
42 | client.removeAllListeners('unsuback')
43 |
44 | # subscribe to the topic
45 | client.subscribe(topic: topic)
46 |
47 | client.on 'suback', ->
48 |
49 | setup_request = (num) ->
50 | process.nextTick ->
51 | request.put url: "http://#{host}:#{http_port}/topics/#{topic}", json: { payload: num}
52 |
53 | setup_request(num) for num in [0..number]
54 |
55 | , defer: true)
56 | suite
57 |
58 | # setting up the benches
59 | # setup_bench(suite, 1)
60 | # setup_bench(suite, 10)
61 | # setup_bench(suite, 100)
62 | # setup_bench(suite, 1000)
63 | setup_bench(suite, 10000)
64 |
65 | suite.on('cycle', (event) ->
66 | console.log(event.target.name)
67 | console.log(event.target.stats.mean)
68 | console.log("total clients: #{pool.created()}")
69 | ).on('complete', ->
70 | process.exit(0)
71 | )
72 |
73 |
74 | suite.run(minSamples: 10, delay: 10, async: false, initCount: 1, maxTime: 60)
75 |
76 | # clients = []
77 | # total = 0
78 | # preload_connections = 15100
79 | # load_cycle = 100
80 | # launched = false
81 | #
82 | # create = ->
83 | # for num in [0...load_cycle]
84 | # pool.get (client) ->
85 | # total += 1
86 | # clients.push(client)
87 | # if total % load_cycle == 0
88 | # if total < preload_connections
89 | # setTimeout(create, 500)
90 | # else if not launched
91 | # launched = true
92 | # console.log "connection pool populated"
93 | # pool.release(client) for client in clients
94 | # suite.run(minSamples: 10, delay: 10, async: false, initCount: 1, maxTime: 60)
95 | #
96 | # console.log "populating connection pool"
97 | # create()
98 |
--------------------------------------------------------------------------------
/benchmarks/bench_http_mqtt_multiple_sub.coffee:
--------------------------------------------------------------------------------
1 | #! /usr/bin/env coffee
2 |
3 | request = require("request")
4 | Pool = require("./mqtt_client_pool").Pool
5 |
6 | host = "localhost"
7 | mqtt_port = 1883
8 | http_port = 3000
9 |
10 | Benchmark = require 'benchmark'
11 |
12 | pool = new Pool(host, mqtt_port)
13 |
14 | suite = new Benchmark.Suite
15 |
16 | payload = 0
17 |
18 | setup_listeners = (suite, number) ->
19 | suite.add("#{number} Client", (d) ->
20 | payload += 1
21 | publish_count = 0
22 | subscribed_count = 0
23 | topic = "bench/#{number}"
24 | for num in [0...number]
25 | pool.get (client) ->
26 |
27 | # when we receive an update
28 | client.on 'publish', (packet) ->
29 |
30 | if packet.payload = String(payload)
31 | # we unsubscribe from the topic
32 | client.unsubscribe(topic: topic)
33 | client.removeAllListeners('publish')
34 | client.removeAllListeners('suback')
35 |
36 | # we resolve the benchmark if we have received all the updates
37 | publish_count += 1
38 | if publish_count == number
39 | #console.log "completed run"
40 | d.resolve()
41 |
42 | client.on 'unsuback', ->
43 | # and we put the client back to the pool
44 | pool.release(client)
45 | client.removeAllListeners('unsuback')
46 |
47 | # subscribe to the topic
48 | client.subscribe(topic: topic)
49 |
50 | # when we receive a subscription ack
51 | client.on 'suback', (packet) ->
52 | subscribed_count += 1
53 |
54 | # if we completed the subscriptions
55 | if subscribed_count == number
56 | request.put url: "http://#{host}:#{http_port}/topics/#{topic}", json: { payload: payload }
57 |
58 | , defer: true)
59 | suite
60 |
61 | # setting up the benches
62 | # setup_listeners(suite, 1)
63 | # setup_listeners(suite, 10)
64 | setup_listeners(suite, 100)
65 | # setup_listeners(suite, 1000)
66 | # setup_listeners(suite, 10000)
67 |
68 | suite.on('cycle', (event) ->
69 | console.log(event.target.name)
70 | console.log(event.target.stats.mean)
71 | console.log("total clients: #{pool.created()}")
72 | ).on('complete', ->
73 | process.exit(0)
74 | )
75 |
76 |
77 | suite.run(minSamples: 10, delay: 10, async: false, initCount: 1, maxTime: 60)
78 |
79 | # clients = []
80 | # total = 0
81 | # preload_connections = 15100
82 | # load_cycle = 100
83 | # launched = false
84 | #
85 | # create = ->
86 | # for num in [0...load_cycle]
87 | # pool.get (client) ->
88 | # total += 1
89 | # clients.push(client)
90 | # if total % load_cycle == 0
91 | # if total < preload_connections
92 | # setTimeout(create, 500)
93 | # else if not launched
94 | # launched = true
95 | # console.log "connection pool populated"
96 | # pool.release(client) for client in clients
97 | # suite.run(minSamples: 10, delay: 10, async: false, initCount: 1, maxTime: 60)
98 | #
99 | # console.log "populating connection pool"
100 | # create()
101 |
--------------------------------------------------------------------------------
/benchmarks/bench_mqtt.coffee:
--------------------------------------------------------------------------------
1 | #! /usr/bin/env coffee
2 |
3 | Pool = require("./mqtt_client_pool").Pool
4 | mqtt_port = 1883
5 |
6 | Benchmark = require 'benchmark'
7 |
8 | pool = new Pool("localhost", mqtt_port)
9 |
10 | suite = new Benchmark.Suite
11 |
12 | payload = 0
13 |
14 | setup_listeners = (suite, number) ->
15 | suite.add("#{number} Client", (d) ->
16 | payload = 1
17 | publish_count = 0
18 | subscribed_count = 0
19 | topic = "bench/#{number}"
20 | for num in [0...number]
21 | pool.get (client) ->
22 |
23 | # subscribe to the topic
24 | client.subscribe(topic: topic)
25 |
26 | # when we receive an update
27 | client.on 'publish', (packet) ->
28 |
29 | if packet.payload = String(payload)
30 | # we unsubscribe from the topic
31 | client.unsubscribe(topic: topic)
32 | client.removeAllListeners('publish')
33 | client.removeAllListeners('suback')
34 |
35 | # we resolve the benchmark if we have received all the updates
36 | publish_count += 1
37 | if publish_count == number
38 | #console.log "completed run"
39 | d.resolve()
40 |
41 | client.on 'unsuback', ->
42 | # and we put the client back to the pool
43 | client.removeAllListeners('unsuback')
44 | pool.release(client)
45 |
46 | # when we receive a subscription ack
47 | client.on 'suback', (packet) ->
48 | subscribed_count += 1
49 |
50 | # console.log "suback #{subscribed_count}"
51 |
52 | # if we completed the subscriptions
53 | if subscribed_count == number
54 | for count in [0...10]
55 | pool.do (pub_client) ->
56 | # we publish a new value on the queue
57 | # we do it ten times, so if one it's rejected
58 | # it's not a problem
59 | pub_client.publish topic: topic, payload: String(payload)
60 |
61 | , defer: true)
62 | suite
63 |
64 | # setting up the benches
65 | setup_listeners(suite, 1)
66 | setup_listeners(suite, 10)
67 | setup_listeners(suite, 100)
68 | setup_listeners(suite, 1000)
69 | # setup_listeners(suite, 10000)
70 |
71 | suite.on('cycle', (event) ->
72 | console.log(event.target.name)
73 | console.log(event.target.stats.mean)
74 | console.log("total clients: #{pool.created()}")
75 | ).on('complete', ->
76 | process.exit(0)
77 | )
78 |
79 | suite.run(minSamples: 100, maxSamples: 1000, delay: 10, async: false, initCount: 1000, maxTime: 60)
80 |
81 | # clients = []
82 | # total = 0
83 | # preload_connections = 20001
84 | # load_cycle = 100
85 | # launched = false
86 | #
87 | # create = ->
88 | # for num in [0...load_cycle]
89 | # pool.get (client) ->
90 | # total += 1
91 | # clients.push(client)
92 | # if total % load_cycle == 0
93 | # if total < preload_connections
94 | # setTimeout(create, 500)
95 | # else if not launched
96 | # launched = true
97 | # console.log "connection pool populated"
98 | # pool.release(client) for client in clients
99 | # suite.run(minSamples: 10, delay: 10, async: false, initCount: 1, maxTime: 10)
100 | #
101 | # console.log "populating connection pool"
102 | # create()
103 |
--------------------------------------------------------------------------------
/benchmarks/haproxy.cfg:
--------------------------------------------------------------------------------
1 | global
2 | ulimit-n 999999
3 | maxconn 65000
4 | maxpipes 65000
5 | tune.maxaccept 500
6 | spread-checks 5
7 |
8 | defaults
9 | retries 5
10 |
11 | option redispatch
12 | option tcp-smart-connect
13 | option tcpka
14 |
15 | timeout client 5m
16 | timeout queue 5m
17 | timeout server 5m
18 | timeout connect 5m
19 |
20 | listen qest-http :3000
21 | mode http
22 | maxconn 65000
23 | balance roundrobin
24 |
25 | server qest-http1 localhost:8001 check
26 | server qest-http2 localhost:8002 check
27 | server qest-http3 localhost:8003 check
28 | server qest-http4 localhost:8004 check
29 | server qest-http5 localhost:8005 check
30 |
31 |
32 | listen qest-mqtt :1883
33 | mode tcp
34 | maxconn 65000
35 | balance roundrobin
36 |
37 | server qest-mqtt1 localhost:9001 check
38 | server qest-mqtt2 localhost:9002 check
39 | server qest-mqtt4 localhost:9003 check
40 | server qest-mqtt3 localhost:9004 check
41 | server qest-mqtt5 localhost:9005 check
42 |
--------------------------------------------------------------------------------
/benchmarks/mqtt_client_pool.coffee:
--------------------------------------------------------------------------------
1 | mqtt = require('mqttjs')
2 |
3 | class Pool
4 |
5 | constructor: (@host, @port) ->
6 | @clients_counter = 0
7 | @clients = []
8 | @total_errors = 0
9 |
10 | print_total_errors = =>
11 | console.log "Current errors: #{@total_errors}"
12 | setTimeout(print_total_errors, 2000)
13 | # print_total_errors()
14 |
15 | created: () -> @clients_counter
16 |
17 | do: (callback) ->
18 | @get (client) =>
19 | callback(client)
20 | @release(client)
21 |
22 | release: (client) -> @clients.push(client)
23 |
24 | get: (setupCallback = ->) ->
25 | client = @clients.pop()
26 |
27 | if client?
28 | setupCallback(client)
29 | return @
30 |
31 | errors = 0
32 |
33 | @clients_counter += 1
34 | client_id = @clients_counter
35 |
36 | create = =>
37 | created = false
38 | mqtt.createClient @port, @host, (err, client) =>
39 |
40 | if err?
41 | @total_errors += 1 if errors == 0
42 | setTimeout(=>
43 | errors += 1
44 | if errors == 10
45 | console.log "Impossible to connect to the server"
46 | process.exit(1)
47 | # console.log "Connecting error n #{errors}"
48 | # console.log "Reconnecting"
49 | create()
50 | , 100)
51 | return
52 |
53 | # console.log "created #{@clients_counter} with errors #{errors}" if errors > 0
54 |
55 | # console.log "total clients: #{@clients_counter}"
56 |
57 | client.connect client: "mqtt_bench_#{client_id}_#{errors}", keepalive: 500
58 |
59 | client.on 'connack', (packet) =>
60 | if packet.returnCode == 0
61 | @total_errors -= 1 if errors > 0
62 | setupCallback(client)
63 | else
64 | console.log('connack error %d', packet.returnCode)
65 |
66 | client.on 'pingreq', (packet) ->
67 | client.pingresp()
68 |
69 | client.on 'close', ->
70 | client.stream.removeAllListeners()
71 | client.stream.destroy()
72 |
73 | create()
74 | @
75 |
76 | destroyAll: ->
77 | for client in @clients
78 | client.disconnect()
79 |
80 | @clients = []
81 |
82 | module.exports.Pool = Pool
83 |
--------------------------------------------------------------------------------
/features/homepage.feature:
--------------------------------------------------------------------------------
1 | Feature: Home page
2 | As a prospect of QEST
3 | I want to get the home page
4 |
5 | Scenario: QEST name
6 | When I visit "/"
7 | Then I should see "QEST"
8 |
9 | Scenario: QEST title
10 | When I visit "/"
11 | Then I should see the title "QEST - The Internet of Things broker that loves devices and developers"
12 |
13 |
--------------------------------------------------------------------------------
/features/http_pub_sub.feature:
--------------------------------------------------------------------------------
1 | Feature: HTTP pub/sub
2 | As a web developer
3 | In order to communicate with my "things"
4 | I want to subscribe and publish to topics
5 |
6 | Scenario: GETting and PUTting
7 | When client "B" publishes "hello world" to "foobar" via HTTP
8 | Then client "A" should have received "hello world" from "foobar" via HTTP
9 |
10 | Scenario: GETting and PUTting JSON
11 | When client "B" publishes "[ 42, 43 ]" to "foobar" via HTTP_JSON
12 | Then client "A" should have received "[42,43]" from "foobar" via HTTP_JSON
13 |
14 | Scenario: GETting and PUTting plain text
15 | When client "B" publishes "hello world" to "foobar" via HTTP_TXT
16 | Then client "A" should have received "hello world" from "foobar" via HTTP_TXT
17 |
18 | Scenario: PUTting JSON and reading from MQTT
19 | Given client "A" subscribe to "foobar" via MQTT
20 | When client "B" publishes "[ 42, 43 ]" to "foobar" via HTTP_JSON
21 | Then client "A" should have received "[42,43]" from "foobar" via MQTT
22 |
--------------------------------------------------------------------------------
/features/mqtt_pub_sub.feature:
--------------------------------------------------------------------------------
1 | Feature: MQTT pub/sub
2 | As a MQTT developer
3 | In order to communicate with my "things"
4 | I want to subscribe and publish to topics
5 |
6 | Scenario: Subscribe and publish 2 clients
7 | Given client "A" subscribe to "foobar" via MQTT
8 | When client "B" publishes "hello world" to "foobar" via MQTT
9 | Then client "A" should have received "hello world" from "foobar" via MQTT
10 |
11 | Scenario: Subscribe and publish 1 client
12 | Given client "A" subscribe to "foobar" via MQTT
13 | When client "A" publishes "hello world" to "foobar" via MQTT
14 | Then client "A" should have received "hello world" from "foobar" via MQTT
15 |
16 | Scenario: Always retains the last message
17 | Given client "B" publishes "aaa" to "foobar" via MQTT
18 | When client "A" subscribe to "foobar" via MQTT
19 | Then client "A" should have received "aaa" from "foobar" via MQTT
20 |
21 | Scenario: Subscribe and publish with pattern
22 | Given client "A" subscribe to "foo/#" via MQTT
23 | When client "B" publishes "hello world" to "foo/bar" via MQTT
24 | Then client "A" should have received "hello world" from "foo/bar" via MQTT
25 |
26 | Scenario: Subscribe and publish 3 clients
27 | Given client "A" subscribe to "foobar" via MQTT
28 | And client "B" subscribe to "foobar" via MQTT
29 | When client "C" publishes "hello world" to "foobar" via MQTT
30 | Then client "A" should have received "hello world" from "foobar" via MQTT
31 | Then client "B" should have received "hello world" from "foobar" via MQTT
32 |
--------------------------------------------------------------------------------
/features/steps/clients_steps.coffee:
--------------------------------------------------------------------------------
1 | expect = require('chai').expect
2 |
3 | module.exports = ->
4 | @Given /^client "([^"]*)" subscribe to "([^"]*)" via ([^ ]*)$/, (client, topic, protocol, callback) ->
5 | @getClient protocol, client, (client) ->
6 | client.subscribe(topic)
7 | callback()
8 |
9 | @When /^client "([^"]*)" publishes "([^"]*)" to "([^"]*)" via ([^ ]*)$/, (client, message, topic, protocol, callback) ->
10 | @getClient protocol, client, (client) ->
11 | client.publish topic, message, callback
12 |
13 | @Then /^client "([^"]*)" should have received "([^"]*)" from "([^"]*)" via ([^ ]*)$/, (client, message, topic, protocol, callback) ->
14 | @getClient protocol, client, (client) ->
15 | client.getLastMessageFromTopic topic, (lastMessage) ->
16 | expect(lastMessage).to.equal(message)
17 | callback()
18 |
--------------------------------------------------------------------------------
/features/steps/http_steps.coffee:
--------------------------------------------------------------------------------
1 | expect = require('chai').expect
2 |
3 | module.exports = () ->
4 |
5 | @When /^I visit "([^"]*)"$/, (url, callback) ->
6 | @browser.visit url, callback
7 |
8 | @Then /^I should see "([^"]*)"$/, (text, callback) ->
9 | expect(@browser.text("body")).to.include(text)
10 | callback()
11 |
12 | @Then /^I should see "([^"]*)" in the textarea$/, (text, callback) ->
13 | doneWaiting = =>
14 | expect(@browser.field("textarea").value).to.include(text)
15 | callback()
16 |
17 | if @browser.field("textarea").value.indexOf(text) != -1
18 | callback()
19 | else
20 | setTimeout(doneWaiting, 50)
21 |
22 |
23 | @Then /^I should see the title "([^"]*)"$/, (text, callback) ->
24 | expect(@browser.text("title")).to.equal(text)
25 | callback()
26 |
--------------------------------------------------------------------------------
/features/steps/web_client_steps.coffee:
--------------------------------------------------------------------------------
1 | expect = require('chai').expect
2 |
3 | module.exports = () ->
4 |
5 | this.Given /^I open the topic "([^"]*)"$/, (topic, callback) ->
6 | @browser.visit "/", =>
7 | @browser.fill "topic", topic, =>
8 | @browser.pressButton "GO!", callback
9 |
10 |
11 | this.When /^I change the payload to "([^"]*)"$/, (payload, callback) ->
12 | @browser.pressButton "Edit", =>
13 | @browser.fill "payload", payload, =>
14 | @browser.pressButton "Update", callback
15 |
--------------------------------------------------------------------------------
/features/steps/world_overrider.coffee:
--------------------------------------------------------------------------------
1 |
2 | module.exports = ->
3 | @World = require("../support/world").World
4 |
--------------------------------------------------------------------------------
/features/support/clients/http.coffee:
--------------------------------------------------------------------------------
1 | request = require('request')
2 |
3 | class HttpClient
4 |
5 | constructor: (@port, @host) ->
6 |
7 | subscribe: (topic) ->
8 | throw new Error("Not implemented yet")
9 |
10 | publish: (topic, message, callback) ->
11 | request.put(uri: @url(topic), form: { payload: message }, callback)
12 |
13 | getLastMessageFromTopic: (topic, callback) ->
14 | request.get uri: @url(topic), headers: @headers , (err, response, body) ->
15 | callback(body)
16 |
17 | headers: () ->
18 | {}
19 |
20 | disconnect: () ->
21 |
22 | url: (topic) ->
23 | "http://#{@host}:#{@port}/topics/#{topic}"
24 |
25 | HttpClient.build = (opts, callback) ->
26 | callback new HttpClient(opts.port, "127.0.0.1")
27 |
28 | module.exports.HttpClient = HttpClient
29 |
--------------------------------------------------------------------------------
/features/support/clients/http_json.coffee:
--------------------------------------------------------------------------------
1 | request = require('request')
2 |
3 | class HttpJsonClient
4 |
5 | constructor: (@port, @host) ->
6 |
7 | subscribe: (topic) ->
8 | throw new Error("Not implemented yet")
9 |
10 | publish: (topic, message, callback) ->
11 | message = JSON.parse(message)
12 | request.put(
13 | uri: @url(topic),
14 | headers: { "Content-Type": "application/json" },
15 | body: JSON.stringify(message),
16 | callback)
17 |
18 | getLastMessageFromTopic: (topic, callback) ->
19 | request.get uri: @url(topic), headers: { "Accept": "application/json" } , (err, response, body) ->
20 | callback(body)
21 |
22 | disconnect: () ->
23 |
24 | url: (topic) ->
25 | "http://#{@host}:#{@port}/topics/#{topic}"
26 |
27 | HttpJsonClient.build = (opts, callback) ->
28 | callback new HttpJsonClient(opts.port, "127.0.0.1")
29 |
30 | module.exports.HttpJsonClient = HttpJsonClient
31 |
--------------------------------------------------------------------------------
/features/support/clients/http_txt.coffee:
--------------------------------------------------------------------------------
1 |
2 | { HttpClient } = require './http'
3 |
4 | class HttpTxtClient extends HttpClient
5 | headers: ->
6 | { "Accept": "text/plain" }
7 |
8 | HttpTxtClient.build = (opts, callback) ->
9 | callback new HttpTxtClient(opts.port, "127.0.0.1")
10 |
11 | module.exports.HttpTxtClient = HttpTxtClient
12 |
--------------------------------------------------------------------------------
/features/support/clients/mqtt.coffee:
--------------------------------------------------------------------------------
1 | mqtt = require('mqttjs')
2 |
3 | class MqttClient
4 |
5 | constructor: (@client) ->
6 | @last_packets = {}
7 |
8 | @client.on 'publish', (packet) =>
9 | @last_packets[packet.topic] = packet.payload
10 |
11 | subscribe: (topic) ->
12 | @client.subscribe(topic: topic)
13 |
14 | publish: (topic, message, callback) ->
15 | @client.publish(topic: topic, payload: message)
16 | callback()
17 |
18 | disconnect: ->
19 | @client.disconnect()
20 |
21 | getLastMessageFromTopic: (topic, callback) ->
22 | last_packet = @last_packets[topic]
23 | if last_packet?
24 | callback(last_packet)
25 | return
26 |
27 | listenToPublish = (packet) =>
28 | if packet.topic == topic
29 | callback(packet.payload)
30 | @client.removeListener(topic, listenToPublish)
31 |
32 | @client.on('publish', listenToPublish)
33 |
34 | counter = 0
35 |
36 | MqttClient.build = (opts, callback) ->
37 | mqtt.createClient opts.mqtt, "127.0.0.1", (err, client) =>
38 | throw new Error(err) if err?
39 | client.connect(client: "cucumber #{counter++}!", keepalive: 3000)
40 |
41 | client.on 'connack', (packet) ->
42 | if packet.returnCode == 0
43 | callback(new MqttClient(client))
44 | else
45 | console.log('connack error %d', packet.returnCode)
46 | throw new Error("connack error #{packet.returnCode}")
47 |
48 | module.exports.MqttClient = MqttClient
49 |
--------------------------------------------------------------------------------
/features/support/clients_helpers.coffee:
--------------------------------------------------------------------------------
1 |
2 | module.exports = ->
3 | @After (done) ->
4 | for name, client of @clients
5 | client.disconnect()
6 | done()
7 |
8 |
--------------------------------------------------------------------------------
/features/support/reset_db_hook.coffee:
--------------------------------------------------------------------------------
1 |
2 | module.exports = () ->
3 | @Before (done) ->
4 | @app.models.Data.reset?()
5 | @app.redis.client.flushdb =>
6 | done()
7 |
--------------------------------------------------------------------------------
/features/support/world.coffee:
--------------------------------------------------------------------------------
1 | env = require("../../qest.coffee")
2 | zombie = require('zombie')
3 |
4 | opts =
5 | port: 9777
6 | mqtt: 9778
7 | redisHost: "127.0.0.1"
8 | redisPort: 6379
9 | redisDB: 16
10 |
11 | app = env.start opts
12 | browser = new zombie.Browser(site: "http://localhost:#{opts.port}", headers: { "Accept": "text/html" })
13 |
14 | { MqttClient } = require("./clients/mqtt")
15 | { HttpClient } = require("./clients/http")
16 | { HttpJsonClient } = require("./clients/http_json")
17 | { HttpTxtClient } = require("./clients/http_txt")
18 |
19 | protocols =
20 | HTTP: HttpClient
21 | HTTP_JSON: HttpJsonClient
22 | HTTP_TXT: HttpTxtClient
23 | MQTT: MqttClient
24 |
25 | exports.World = (callback) ->
26 | @browser = browser
27 | @opts = opts
28 | @app = app
29 |
30 | @clients = {}
31 | @getClient = (protocol, name, callback) =>
32 | if @clients[name]?
33 | callback(@clients[name])
34 | else
35 | protocols[protocol].build @opts, (client) =>
36 | @clients[name] = client
37 | callback(client)
38 |
39 | callback()
40 |
--------------------------------------------------------------------------------
/features/web_interface.feature:
--------------------------------------------------------------------------------
1 | Feature: Web Interface
2 | As QEST user
3 | I want to go to one of my topics
4 |
5 | Scenario: go to topic page
6 | When I open the topic "mytopic"
7 | Then I should see "mytopic"
8 |
9 | Scenario: see the published value of a topic
10 | Given client "A" publishes "hello world" to "mytopic" via HTTP
11 | When I open the topic "mytopic"
12 | Then I should see "hello world" in the textarea
13 |
14 | Scenario: receives the updates from a topic
15 | Given I open the topic "mytopic"
16 | When client "A" publishes "hello world" to "mytopic" via HTTP
17 | Then I should see "hello world" in the textarea
18 |
19 | Scenario: send the update of a topic from the web to the devices
20 | Given client "A" subscribe to "mytopic" via MQTT
21 | And I open the topic "mytopic"
22 | When I change the payload to "hello world"
23 | Then client "A" should have received "hello world" from "mytopic" via MQTT
24 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "qest",
3 | "version": "0.0.1",
4 | "description": "A REST bridge to MQTT",
5 | "keywords": [
6 | "MQTT",
7 | "WebSockets",
8 | "REST"
9 | ],
10 | "author": "hello@matteocollina.com",
11 | "licenses": [
12 | "MIT"
13 | ],
14 | "dependencies": {
15 | "coffee-script": "~1.3.3",
16 | "optimist": "= 0.3.4",
17 | "express": "~3.0.1",
18 | "hbs": "= 1.0.4",
19 | "socket.io": "= 0.9.10",
20 | "mqttjs": "= 0.1.7",
21 | "redis": "~0.8.1",
22 | "hiredis": "= 0.1.14",
23 | "connect-redis": "1.4.0",
24 | "markdown": "0.3.1",
25 | "mincer": "0.3.0",
26 | "less": "~1.3.0",
27 | "ascoltatori": "0.0.4",
28 | "async": "~0.1.22"
29 | },
30 | "devDependencies": {
31 | "cucumber": "= 0.2.21",
32 | "zombie": "= 1.4.0",
33 | "mocha": "= 1.2.2",
34 | "sinon": "= 1.3.4",
35 | "chai": "= 1.1.0",
36 | "benchmark": "git://github.com/bestiejs/benchmark.js.git#master",
37 | "request": "= 2.10.0"
38 | },
39 | "scripts": {
40 | "test": "./node_modules/.bin/cake spec && ./node_modules/.bin/cake features"
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mcollina/qest/810f720b61c47fa74cdf7bb2e3ea616c4e548ffd/public/favicon.ico
--------------------------------------------------------------------------------
/public/images/grey.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mcollina/qest/810f720b61c47fa74cdf7bb2e3ea616c4e548ffd/public/images/grey.png
--------------------------------------------------------------------------------
/public/images/schema.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mcollina/qest/810f720b61c47fa74cdf7bb2e3ea616c4e548ffd/public/images/schema.png
--------------------------------------------------------------------------------
/public/mu-942f19e0-e626e170-21f6d708-caa97289.txt:
--------------------------------------------------------------------------------
1 | 42
2 |
--------------------------------------------------------------------------------
/public/stylesheets/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mcollina/qest/810f720b61c47fa74cdf7bb2e3ea616c4e548ffd/public/stylesheets/.gitkeep
--------------------------------------------------------------------------------
/qest.coffee:
--------------------------------------------------------------------------------
1 | #! /usr/bin/env coffee
2 |
3 | # Module dependencies.
4 |
5 | optimist = require 'optimist'
6 | express = require 'express'
7 | path = require 'path'
8 | fs = require 'fs'
9 | hbs = require 'hbs'
10 | redis = require 'redis'
11 | mqtt = require "mqttjs"
12 | EventEmitter = require('events').EventEmitter
13 | RedisStore = require('connect-redis')(express)
14 | ascoltatori = require('ascoltatori')
15 |
16 | # Create Server
17 |
18 | module.exports.app = app = express()
19 | http = require('http').createServer(app)
20 |
21 | # Configuration
22 |
23 | app.redis = {}
24 |
25 | module.exports.configure = configure = ->
26 | app.configure 'development', ->
27 | app.use(express.errorHandler({ dumpExceptions: true, showStack: true }))
28 |
29 | app.configure 'production', ->
30 | app.use(express.errorHandler())
31 |
32 | app.configure ->
33 | app.set('views', __dirname + '/app/views')
34 | app.set('view engine', 'hbs')
35 | app.use(express.bodyParser())
36 | app.use(express.methodOverride())
37 | app.use(express.cookieParser())
38 | app.use(express.session(secret: "wyRLuS5A79wLn3ItlGVF61Gt",
39 | store: new RedisStore(client: app.redis.client), maxAge: 1000 * 60 * 60 * 24 * 14)) # two weeks
40 |
41 | app.use(app.router)
42 | app.use(express.static(__dirname + '/public'))
43 |
44 | # setup websockets
45 | io = app.io = require('socket.io').listen(http)
46 |
47 | io.configure 'production', ->
48 | io.enable('browser client minification'); # send minified client
49 | io.enable('browser client etag'); # apply etag caching logic based on version number
50 | io.enable('browser client gzip'); # gzip the file
51 | io.set('log level', 0)
52 |
53 | io.configure 'test', ->
54 | io.set('log level', 0)
55 |
56 | load("models")
57 | load("controllers")
58 | load("helpers")
59 |
60 | load = (key) ->
61 | app[key] = {}
62 | loadPath = __dirname + "/app/#{key}/"
63 | for component in fs.readdirSync(loadPath)
64 | if component.match /(js|coffee)$/
65 | component = path.basename(component, path.extname(component))
66 | loadedModule = require(loadPath + component)(app)
67 | component = loadedModule.name if loadedModule?.name? and loadedModule.name != ""
68 | app[key][component] = loadedModule
69 |
70 | # Start the module if it's needed
71 |
72 | optionParser = optimist.
73 | default('port', 3000).
74 | default('mqtt', 1883).
75 | default('redis-port', 6379).
76 | default('redis-host', '127.0.0.1').
77 | default('redis-db', 0).
78 | usage("Usage: $0 [-p WEB-PORT] [-m MQTT-PORT] [-rp REDIS-PORT] [-rh REDIS-HOST]").
79 | alias('port', 'p').
80 | alias('mqtt', 'm').
81 | alias('redis-port', 'rp').
82 | alias('redis-host', 'rh').
83 | alias('redis-db', 'rd').
84 | describe('port', 'The port the web server will listen to').
85 | describe('mqtt', 'The port the mqtt server will listen to').
86 | describe('redis-port', 'The port of the redis server').
87 | describe('redis-host', 'The host of the redis server').
88 | boolean("help").
89 | describe("help", "This help")
90 |
91 | argv = optionParser.argv
92 |
93 | module.exports.setupAscoltatore = setupAscoltatore = (opts = {}) ->
94 | app.ascoltatore = new ascoltatori.RedisAscoltatore
95 | redis: redis
96 | port: opts.port
97 | host: opts.host
98 | db: opts.db
99 |
100 | module.exports.setup = setup = (opts = {}) ->
101 | args = [opts.port, opts.host]
102 | app.redis.client = redis.createClient(args...)
103 | app.redis.client.select(opts.db || 0)
104 |
105 | setupAscoltatore(opts)
106 |
107 | start = module.exports.start = (opts={}, cb=->) ->
108 |
109 | opts.port ||= argv.port
110 | opts.mqtt ||= argv.mqtt
111 | opts.redisPort ||= argv['redis-port']
112 | opts.redisHost ||= argv['redis-host']
113 | opts.redisDB ||= argv['redis-db']
114 |
115 | if argv.help
116 | optionParser.showHelp()
117 | return 1
118 |
119 | setup(port: opts.redisPort, host: opts.redisHost, db: opts.redisDB)
120 | configure()
121 |
122 | countDone = 0
123 | done = ->
124 | cb() if countDone++ == 2
125 |
126 | http.listen opts.port, ->
127 | console.log("mqtt-rest web server listening on port %d in %s mode", opts.port, app.settings.env)
128 | done()
129 |
130 | mqtt.createServer(app.controllers.mqtt_api).listen opts.mqtt, ->
131 | console.log("mqtt-rest mqtt server listening on port %d in %s mode", opts.mqtt, app.settings.env)
132 | done()
133 |
134 | app
135 |
136 | if require.main.filename == __filename
137 | start()
138 |
--------------------------------------------------------------------------------
/qest.js:
--------------------------------------------------------------------------------
1 | #! /usr/bin/env node
2 |
3 | require('coffee-script')
4 | app = require("./qest.coffee")
5 | app.start()
6 |
--------------------------------------------------------------------------------
/test/mocha.opts:
--------------------------------------------------------------------------------
1 | --require chai
2 | --require sinon
3 | --reporter dot
4 | --ui bdd
5 | --growl
6 | --colors
7 | --globals s
8 |
--------------------------------------------------------------------------------
/test/models/data_spec.coffee:
--------------------------------------------------------------------------------
1 |
2 | helper = require("../spec_helper")
3 |
4 | expect = require('chai').expect
5 | async = require("async")
6 |
7 | describe "Data", ->
8 |
9 | models = null
10 |
11 | before ->
12 | helper.globalSetup()
13 | models = helper.app.models
14 |
15 | after ->
16 | helper.globalTearDown()
17 |
18 | beforeEach (done) ->
19 | helper.setup(done)
20 |
21 | afterEach (done) ->
22 | helper.tearDown(done)
23 |
24 | it "should have a findOrCreate method", ->
25 | expect(models.Data.findOrCreate).to.exist
26 |
27 | it "should findOrCreate a new instance with a key", (done) ->
28 | models.Data.findOrCreate "key", (err, data) =>
29 | expect(data).to.eql(new models.Data("key"))
30 | done()
31 |
32 | it "should findOrCreate a new instance with a key and a value", (done) ->
33 | models.Data.findOrCreate "aaa", "bbbb", (err, data) =>
34 | expect(data).to.eql(new models.Data("aaa", "bbbb"))
35 | done()
36 |
37 | it "should findOrCreate an old instance overriding the value", (done) ->
38 | models.Data.findOrCreate "aaa", "bbbb", =>
39 | models.Data.findOrCreate "aaa", "ccc", =>
40 | models.Data.find "aaa", (err, data) =>
41 | expect(data).to.eql(new models.Data("aaa", "ccc"))
42 | done()
43 |
44 | it "should publish an update when calling findOrCreate", (done) ->
45 | models.Data.subscribe "aaa", (data) =>
46 | done()
47 | models.Data.findOrCreate "aaa", "bbbb"
48 |
49 | it "should allow subscribing in the create step", (done) ->
50 | models.Data.findOrCreate "aaa", (err, data) =>
51 | models.Data.subscribe "aaa", (curr) ->
52 | done() if curr.value == "ccc"
53 |
54 | data.value = "ccc"
55 | data.save()
56 |
57 | it "should allow unsubscribing in the create step", (done) ->
58 | models.Data.findOrCreate "aaa", (err, data) =>
59 | func = -> throw "This should never be called"
60 |
61 | models.Data.subscribe "aaa", func
62 | models.Data.unsubscribe "aaa", func
63 |
64 | models.Data.subscribe "aaa", (curr) ->
65 | done()
66 |
67 | data.save()
68 |
69 | it "should provide a find method that returns an error if there is no obj", (done) ->
70 | models.Data.find "obj", (err, data) =>
71 | expect(err).to.eql("Record not found")
72 | done()
73 |
74 | it "should provide a find method that uses a regexp for matching", (done) ->
75 | results = []
76 |
77 | async.parallel([
78 | async.apply(models.Data.findOrCreate, "hello bob", "aaa"),
79 | async.apply(models.Data.findOrCreate, "hello mark", "aaa"),
80 | ], ->
81 | models.Data.find /hello .*/, (err, data) ->
82 | results.push(data.key) unless err?
83 | if results.length == 2
84 | expect(results).to.contain("hello bob")
85 | expect(results).to.contain("hello mark")
86 | done()
87 | )
88 |
89 | it "should provide a subscribe method that works for new topics", (done) ->
90 | results = []
91 | models.Data.subscribe "hello/*", (data) ->
92 | results.push(data.key)
93 | if results.length == 2
94 | expect(results).to.contain("hello/bob")
95 | expect(results).to.contain("hello/mark")
96 | done()
97 |
98 | async.parallel([
99 | async.apply(models.Data.findOrCreate, "hello/bob", "aaa"),
100 | async.apply(models.Data.findOrCreate, "hello/mark", "aaa"),
101 | ])
102 |
103 | describe "instance", ->
104 |
105 | it "should get the key", ->
106 | subject = new models.Data("key", "value")
107 | expect(subject.key).to.eql("key")
108 |
109 | it "should get the key (dis)", ->
110 | subject = new models.Data("aaa")
111 | expect(subject.key).to.eql("aaa")
112 |
113 | it "should get the value", ->
114 | subject = new models.Data("key", "value")
115 | expect(subject.value).to.eql("value")
116 |
117 | it "should get the value (dis)", ->
118 | subject = new models.Data("key", "aaa")
119 | expect(subject.value).to.eql("aaa")
120 |
121 | it "should get the redisKey", ->
122 | subject = new models.Data("key", "value")
123 | expect(subject.redisKey).to.eql("topic:key")
124 |
125 | it "should get the redisKey (dis)", ->
126 | subject = new models.Data("aaa/42", "value")
127 | expect(subject.redisKey).to.eql("topic:aaa/42")
128 |
129 | it "should accept an object as value in the constructor", ->
130 | obj = { hello: 42 }
131 | subject = new models.Data("key", obj)
132 | expect(subject.value).to.eql(obj)
133 |
134 | it "should export its value as JSON", ->
135 | obj = { hello: 42 }
136 | subject = new models.Data("key", obj)
137 | expect(subject.jsonValue).to.eql(JSON.stringify(obj))
138 |
139 | it "should export its value as JSON when setting the value", ->
140 | obj = { hello: 42 }
141 | subject = new models.Data("key")
142 | subject.value = obj
143 | expect(subject.jsonValue).to.eql(JSON.stringify(obj))
144 |
145 | it "should set the value", ->
146 | subject = new models.Data("key")
147 | subject.value = "bbb"
148 | expect(subject.value).to.eql("bbb")
149 |
150 | it "should set the value (dis)", ->
151 | subject = new models.Data("key")
152 | subject.value = "ccc"
153 | expect(subject.value).to.eql("ccc")
154 |
155 | it "should set the json value", ->
156 | subject = new models.Data("key")
157 | subject.jsonValue = JSON.stringify("ccc")
158 | expect(subject.value).to.eql("ccc")
159 |
160 | it "should have a save method", ->
161 | subject = new models.Data("key")
162 | expect(subject.save).to.exist
163 |
164 | it "should save an array", (done) ->
165 | subject = new models.Data("key")
166 | subject.value = [1, 2]
167 | subject.save =>
168 | done()
169 |
170 | it "should support subscribing for change", (done) ->
171 | subject = new models.Data("key")
172 | subject.save =>
173 | models.Data.subscribe subject.key, (data) =>
174 | expect(data.value).to.equal("aaaa")
175 | done()
176 |
177 | subject.value = "aaaa"
178 | subject.save()
179 |
180 | it "should register for change before creation", (done) ->
181 | subject = new models.Data("key")
182 | models.Data.subscribe subject.key, (data) =>
183 | expect(data.value).to.equal("aaaa")
184 | done()
185 |
186 | subject.value = "aaaa"
187 | subject.save()
188 |
189 | it "should save and findOrCreate", (done) ->
190 | subject = new models.Data("key")
191 | subject.save =>
192 | models.Data.findOrCreate subject.key, (err, data) =>
193 | expect(data).to.eql(subject)
194 | done()
195 |
196 | it "should save and find", (done) ->
197 | subject = new models.Data("key")
198 | subject.save =>
199 | models.Data.find subject.key, (err, data) =>
200 | expect(data).to.eql(subject)
201 | done()
202 |
203 | it "should not persist the value before save", (done) ->
204 | subject = new models.Data("key")
205 | subject.save =>
206 | subject.value = "ccc"
207 | models.Data.find subject.key, (err, data) ->
208 | expect(data.value).to.not.eql("ccc")
209 | done()
210 |
--------------------------------------------------------------------------------
/test/spec_helper.coffee:
--------------------------------------------------------------------------------
1 |
2 | env = require("../qest.coffee")
3 | async = require("async")
4 |
5 | config =
6 | host: "127.0.0.1"
7 | port: 6379
8 | db: 16
9 |
10 | module.exports.globalSetup = ->
11 | return if @app?
12 | @app = env.app
13 | env.setup(config)
14 | env.configure()
15 |
16 | module.exports.globalTearDown = ->
17 | @app.redis.client.end()
18 |
19 | module.exports.setup = (done) ->
20 | env.setupAscoltatore(config)
21 | async.parallel([
22 | (cb) => @app.ascoltatore.once("ready", cb),
23 | (cb) => @app.redis.client.flushdb(cb)
24 | ], done)
25 |
26 | module.exports.tearDown = (done) ->
27 | @app.ascoltatore.close(done)
28 |
29 |
--------------------------------------------------------------------------------