vines 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. data/README +2 -2
  2. data/Rakefile +63 -8
  3. data/bin/vines +0 -1
  4. data/conf/config.rb +16 -7
  5. data/lib/vines.rb +21 -16
  6. data/lib/vines/command/init.rb +5 -3
  7. data/lib/vines/config.rb +34 -0
  8. data/lib/vines/contact.rb +14 -0
  9. data/lib/vines/stanza.rb +26 -0
  10. data/lib/vines/stanza/iq.rb +1 -1
  11. data/lib/vines/stanza/iq/disco_info.rb +3 -0
  12. data/lib/vines/stanza/iq/private_storage.rb +83 -0
  13. data/lib/vines/stanza/iq/roster.rb +26 -30
  14. data/lib/vines/stanza/presence.rb +0 -12
  15. data/lib/vines/stanza/presence/subscribe.rb +3 -20
  16. data/lib/vines/stanza/presence/subscribed.rb +9 -10
  17. data/lib/vines/stanza/presence/unsubscribe.rb +8 -15
  18. data/lib/vines/stanza/presence/unsubscribed.rb +8 -8
  19. data/lib/vines/storage.rb +28 -0
  20. data/lib/vines/storage/couchdb.rb +29 -0
  21. data/lib/vines/storage/local.rb +22 -0
  22. data/lib/vines/storage/redis.rb +26 -0
  23. data/lib/vines/storage/sql.rb +48 -5
  24. data/lib/vines/stream/client.rb +6 -8
  25. data/lib/vines/stream/http.rb +23 -21
  26. data/lib/vines/stream/http/auth.rb +1 -1
  27. data/lib/vines/stream/http/bind.rb +1 -1
  28. data/lib/vines/stream/http/bind_restart.rb +4 -3
  29. data/lib/vines/stream/http/ready.rb +1 -1
  30. data/lib/vines/stream/http/request.rb +94 -5
  31. data/lib/vines/stream/http/session.rb +8 -6
  32. data/lib/vines/version.rb +1 -1
  33. data/test/config_test.rb +12 -0
  34. data/test/contact_test.rb +40 -0
  35. data/test/rake_test_loader.rb +11 -3
  36. data/test/stanza/iq/private_storage_test.rb +177 -0
  37. data/test/stanza/iq/roster_test.rb +1 -1
  38. data/test/stanza/iq_test.rb +63 -0
  39. data/test/storage/couchdb_test.rb +7 -1
  40. data/test/storage/local_test.rb +8 -2
  41. data/test/storage/redis_test.rb +16 -7
  42. data/test/storage/sql_test.rb +8 -1
  43. data/test/storage/storage_tests.rb +50 -0
  44. data/test/stream/http/auth_test.rb +3 -0
  45. data/test/stream/http/ready_test.rb +3 -0
  46. data/test/stream/http/request_test.rb +86 -0
  47. data/test/stream/parser_test.rb +2 -0
  48. data/web/404.html +43 -0
  49. data/web/apple-touch-icon.png +0 -0
  50. data/web/chat/coffeescripts/chat.coffee +385 -0
  51. data/web/chat/coffeescripts/init.coffee +15 -0
  52. data/web/chat/coffeescripts/logout.coffee +5 -0
  53. data/web/chat/index.html +17 -0
  54. data/web/chat/javascripts/app.js +1 -0
  55. data/web/chat/javascripts/chat.js +436 -0
  56. data/web/chat/javascripts/init.js +21 -0
  57. data/web/chat/javascripts/logout.js +11 -0
  58. data/web/chat/stylesheets/chat.css +290 -0
  59. data/web/favicon.png +0 -0
  60. data/web/lib/coffeescripts/contact.coffee +32 -0
  61. data/web/lib/coffeescripts/layout.coffee +30 -0
  62. data/web/lib/coffeescripts/login.coffee +52 -0
  63. data/web/lib/coffeescripts/navbar.coffee +84 -0
  64. data/web/lib/coffeescripts/router.coffee +40 -0
  65. data/web/lib/coffeescripts/session.coffee +211 -0
  66. data/web/lib/images/default-user.png +0 -0
  67. data/web/lib/images/logo-large.png +0 -0
  68. data/web/lib/images/logo-small.png +0 -0
  69. data/web/lib/javascripts/base.js +9 -0
  70. data/web/lib/javascripts/contact.js +94 -0
  71. data/web/lib/javascripts/icons.js +101 -0
  72. data/web/lib/javascripts/jquery.cookie.js +91 -0
  73. data/web/lib/javascripts/jquery.js +18 -0
  74. data/web/lib/javascripts/layout.js +48 -0
  75. data/web/lib/javascripts/login.js +61 -0
  76. data/web/lib/javascripts/navbar.js +69 -0
  77. data/web/lib/javascripts/raphael.js +8 -0
  78. data/web/lib/javascripts/router.js +105 -0
  79. data/web/lib/javascripts/session.js +322 -0
  80. data/web/lib/javascripts/strophe.js +1 -0
  81. data/web/lib/stylesheets/base.css +223 -0
  82. data/web/lib/stylesheets/login.css +63 -0
  83. metadata +51 -9
@@ -0,0 +1,21 @@
1
+ $(function() {
2
+ var buttons, icon, label, nav, pages, session;
3
+ session = new Session();
4
+ nav = new NavBar(session);
5
+ nav.draw();
6
+ buttons = {
7
+ Messages: ICONS.chat,
8
+ Logout: ICONS.power
9
+ };
10
+ for (label in buttons) {
11
+ icon = buttons[label];
12
+ nav.addButton(label, icon);
13
+ }
14
+ pages = {
15
+ '/messages': new ChatPage(session),
16
+ '/logout': new LogoutPage(session),
17
+ 'default': new LoginPage(session, '/messages/')
18
+ };
19
+ new Router(pages).draw();
20
+ return nav.select($('#nav-link-messages').parent());
21
+ });
@@ -0,0 +1,11 @@
1
+ var LogoutPage;
2
+ LogoutPage = (function() {
3
+ function LogoutPage(session) {
4
+ this.session = session;
5
+ }
6
+ LogoutPage.prototype.draw = function() {
7
+ window.location.hash = '';
8
+ return window.location.reload();
9
+ };
10
+ return LogoutPage;
11
+ })();
@@ -0,0 +1,290 @@
1
+ #chat-page #container {
2
+ height: 100%;
3
+ }
4
+ #chat-page #alpha,
5
+ #chat-page #beta,
6
+ #chat-page #charlie {
7
+ height: 100%;
8
+ position: absolute;
9
+ }
10
+ #chat-page #alpha {
11
+ background: #f8f8f8;
12
+ -webkit-box-shadow: 0 0 40px rgba(0, 0, 0, 0.1) inset;
13
+ box-shadow: 0 0 40px rgba(0, 0, 0, 0.1) inset;
14
+ width: 260px;
15
+ }
16
+ #chat-page #alpha h2,
17
+ #chat-page #beta h2,
18
+ #chat-page #charlie h2 {
19
+ border-bottom: 1px solid #ddd;
20
+ font-size: 10pt;
21
+ padding-left: 10px;
22
+ position: relative;
23
+ text-shadow: 0 -1px 1px #fff;
24
+ }
25
+ #chat-page #beta {
26
+ -webkit-box-shadow: 0 0 5px rgba(0, 0, 0, 0.5), 0 0 40px rgba(0, 0, 0, 0.1) inset;
27
+ box-shadow: 0 0 5px rgba(0, 0, 0, 0.5), 0 0 40px rgba(0, 0, 0, 0.1) inset;
28
+ width: 460px;
29
+ left: 260px;
30
+ z-index: 1;
31
+ }
32
+ #chat-page #charlie {
33
+ background: #f8f8f8;
34
+ -webkit-box-shadow: 0 0 40px rgba(0, 0, 0, 0.1) inset;
35
+ box-shadow: 0 0 40px rgba(0, 0, 0, 0.1) inset;
36
+ width: 260px;
37
+ left: 720px;
38
+ }
39
+ #chat-page #chat-title {
40
+ background: #f8f8f8;
41
+ }
42
+ #chat-page #messages {
43
+ background: #fff;
44
+ height: 100%;
45
+ list-style: none;
46
+ overflow-y: auto;
47
+ text-shadow: 0 1px 1px #ddd;
48
+ width: 100%;
49
+ }
50
+ #chat-page #messages::-webkit-scrollbar,
51
+ #chat-page #roster::-webkit-scrollbar,
52
+ #chat-page #notifications::-webkit-scrollbar {
53
+ width: 6px;
54
+ }
55
+ #chat-page #messages::-webkit-scrollbar-thumb,
56
+ #chat-page #roster::-webkit-scrollbar-thumb,
57
+ #chat-page #notifications::-webkit-scrollbar-thumb {
58
+ border-radius: 10px;
59
+ }
60
+ #chat-page #messages::-webkit-scrollbar-thumb:vertical,
61
+ #chat-page #roster::-webkit-scrollbar-thumb:vertical,
62
+ #chat-page #notifications::-webkit-scrollbar-thumb:vertical {
63
+ background: rgba(0, 0, 0, .2);
64
+ }
65
+ #chat-page #messages li {
66
+ border-bottom: 1px solid #f0f0f0;
67
+ min-height: 40px;
68
+ padding: 10px;
69
+ position: relative;
70
+ }
71
+ #chat-page #messages li:hover > span .time {
72
+ opacity: 0.3;
73
+ }
74
+ #chat-page #messages li img {
75
+ border: 3px solid #fff;
76
+ -webkit-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2), 0 0 40px rgba(0, 0, 0, 0.06) inset;
77
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2), 0 0 40px rgba(0, 0, 0, 0.06) inset;
78
+ position: absolute;
79
+ top: 7px;
80
+ right: 7px;
81
+ height: 40px;
82
+ width: 40px;
83
+ }
84
+ #chat-page #messages li p {
85
+ line-height: 1.5;
86
+ width: 90%;
87
+ }
88
+ #chat-page #messages li footer {
89
+ font-size: 9pt;
90
+ padding-right: 50px;
91
+ text-align: right;
92
+ }
93
+ #chat-page #messages li footer span {
94
+ color: #d8d8d8;
95
+ margin-right: 0.5em;
96
+ text-shadow: none;
97
+ }
98
+ #chat-page #messages li footer .author::before {
99
+ content: '\2014 ';
100
+ }
101
+ #chat-page #message-form {
102
+ background: #f8f8f8;
103
+ border-top: 1px solid #dfdfdf;
104
+ height: 50px;
105
+ position: absolute;
106
+ bottom: 0;
107
+ width: 100%;
108
+ }
109
+ #chat-page input[type="text"]:focus,
110
+ #chat-page input[type="email"]:focus {
111
+ -webkit-box-shadow: inset 1px 1px 2px rgba(0, 0, 0, 0.6);
112
+ box-shadow: inset 1px 1px 2px rgba(0, 0, 0, 0.6);
113
+ }
114
+ #chat-page #message {
115
+ display: block;
116
+ position: relative;
117
+ left: 10px;
118
+ top: 10px;
119
+ width: 428px;
120
+ }
121
+ #chat-page #roster,
122
+ #chat-page #notifications {
123
+ height: 100%;
124
+ list-style: none;
125
+ overflow-y: auto;
126
+ text-shadow: 0 1px 1px #fff;
127
+ width: 260px;
128
+ }
129
+ #chat-page #roster li,
130
+ #chat-page #notifications li {
131
+ cursor: pointer;
132
+ border-bottom: 1px solid #ddd;
133
+ font-weight: bold;
134
+ min-height: 42px;
135
+ padding: 0 10px;
136
+ position: relative;
137
+ -moz-transition: background 0.3s;
138
+ -o-transition: background 0.3s;
139
+ -webkit-transition: background 0.3s;
140
+ transition: background 0.3s;
141
+ }
142
+ #chat-page #notifications li {
143
+ font-weight: normal;
144
+ padding: 10px 0 0 0;
145
+ }
146
+ #chat-page #roster li:hover,
147
+ #chat-page #notifications li:hover {
148
+ background: rgba(255, 255, 255, 1.0);
149
+ }
150
+ #chat-page #roster li.offline > * {
151
+ opacity: 0.4;
152
+ }
153
+ #chat-page #roster li.selected > * {
154
+ opacity: 1.0;
155
+ }
156
+ #chat-page #roster li.selected {
157
+ background: #4693FF;
158
+ background: -moz-linear-gradient(#4693FF, #015de6);
159
+ background: -o-linear-gradient(#4693FF, #015de6);
160
+ background: -webkit-gradient(linear, left top, left bottom, from(#4693FF), to(#015de6));
161
+ border-bottom: 1px solid #fff;
162
+ color: #fff;
163
+ text-shadow: 0 -1px 1px #1b3a65;
164
+ }
165
+ #chat-page #roster li.selected .status-msg {
166
+ color: rgba(255, 255, 255, 0.85);
167
+ }
168
+ #chat-page #roster .status-msg {
169
+ display: block;
170
+ font-size: 11px;
171
+ font-weight: normal;
172
+ line-height: 11px;
173
+ }
174
+ #chat-page #roster .unread {
175
+ background: #4693FF;
176
+ background: -moz-linear-gradient(#4693FF, #015de6);
177
+ background: -o-linear-gradient(#4693FF, #015de6);
178
+ background: -webkit-gradient(linear, left top, left bottom, from(#4693FF), to(#015de6));
179
+ border-radius: 30px;
180
+ color: #fff;
181
+ display: inline-block;
182
+ font-size: 11px;
183
+ font-weight: normal;
184
+ line-height: 15px;
185
+ padding: 0px 6px;
186
+ position: absolute;
187
+ right: 50px;
188
+ top: 14px;
189
+ text-shadow: none;
190
+ }
191
+ #chat-page #roster .vcard-img {
192
+ background: #fff;
193
+ border: 1px solid #fff;
194
+ -webkit-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2), 0 0 40px rgba(0, 0, 0, 0.06) inset;
195
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2), 0 0 40px rgba(0, 0, 0, 0.06) inset;
196
+ height: 32px;
197
+ width: 32px;
198
+ position: absolute;
199
+ top: 4px;
200
+ right: 10px;
201
+ }
202
+ #chat-page #roster-controls,
203
+ #chat-page #notification-controls {
204
+ background: rgba(255, 255, 255, 0.05);
205
+ border-top: 1px solid #ddd;
206
+ height: 50px;
207
+ position: absolute;
208
+ bottom: 0;
209
+ width: 260px;
210
+ }
211
+ #chat-page #notification-controls {
212
+ text-align: right;
213
+ }
214
+ #chat-page #roster-controls > div,
215
+ #chat-page #notification-controls > div {
216
+ cursor: pointer;
217
+ display: inline-block;
218
+ height: 27px;
219
+ margin: 0 10px;
220
+ position: relative;
221
+ top: 10px;
222
+ width: 27px;
223
+ }
224
+ #chat-page #roster-controls > div > svg,
225
+ #chat-page #notification-controls > div > svg {
226
+ height: 27px;
227
+ width: 27px;
228
+ }
229
+ #chat-page .contact-form {
230
+ background: inherit;
231
+ border-bottom: 1px solid #ddd;
232
+ border-top: 1px solid #fff;
233
+ -webkit-box-shadow: 0px -3px 5px rgba(0, 0, 0, 0.1);
234
+ box-shadow: 0px -3px 5px rgba(0, 0, 0, 0.1);
235
+ padding-top: 10px;
236
+ position: absolute;
237
+ bottom: 50px;
238
+ left: 0;
239
+ width: 260px;
240
+ }
241
+ #chat-page .contact-form h2,
242
+ #chat-page .notify-form h2 {
243
+ border: none !important;
244
+ line-height: 1;
245
+ margin-bottom: 10px;
246
+ }
247
+ #chat-page .contact-form p,
248
+ #chat-page .notify-form p {
249
+ line-height: 1.5;
250
+ margin: 0 10px 10px 10px;
251
+ text-shadow: 0 1px 1px #fff;
252
+ }
253
+ #chat-page .notify-form p {
254
+ margin-top: -5px;
255
+ }
256
+ #chat-page .contact-form .buttons,
257
+ #chat-page .notify-form .buttons {
258
+ padding-right: 10px;
259
+ text-align: right;
260
+ }
261
+ #chat-page .contact-form input[type="text"],
262
+ #chat-page .contact-form input[type="email"] {
263
+ margin-bottom: 10px;
264
+ width: 228px;
265
+ position: relative;
266
+ left: 10px;
267
+ }
268
+ #chat-page #edit-contact-jid {
269
+ color: #444;
270
+ margin-top: -5px;
271
+ }
272
+ #chat-page #search-roster {
273
+ cursor: pointer;
274
+ display: inline-block;
275
+ line-height: 1;
276
+ position: absolute;
277
+ right: 10px;
278
+ top: 6px;
279
+ }
280
+ #chat-page #search-roster svg {
281
+ height: 16px;
282
+ width: 16px;
283
+ }
284
+ #chat-page #search-roster-form {
285
+ border-bottom: 1px solid #ddd;
286
+ padding: 5px 10px;
287
+ }
288
+ #chat-page #search-roster-text {
289
+ width: 100%;
290
+ }
Binary file
@@ -0,0 +1,32 @@
1
+ class Contact
2
+ constructor: (node) ->
3
+ node = $(node)
4
+ @jid = node.attr 'jid'
5
+ @name = node.attr 'name'
6
+ @ask = node.attr 'ask'
7
+ @subscription = node.attr 'subscription'
8
+ @groups = $('group', node).map(-> $(this).text()).get()
9
+ @presence = []
10
+
11
+ online: ->
12
+ @presence.length > 0
13
+
14
+ offline: ->
15
+ @presence.length == 0
16
+
17
+ available: ->
18
+ this.online() && (p for p in @presence when !p.away).length > 0
19
+
20
+ away: -> !this.available()
21
+
22
+ status: ->
23
+ available = (p.status for p in @presence when p.status && !p.away)[0] || 'Available'
24
+ away = (p.status for p in @presence when p.status && p.away)[0] || 'Away'
25
+ if this.offline() then 'Offline'
26
+ else if this.away() then away
27
+ else available
28
+
29
+ update: (presence) ->
30
+ @presence = (p for p in @presence when p.from != presence.from)
31
+ @presence.push presence unless presence.type
32
+ @presence = [] if presence.type == 'unsubscribed'
@@ -0,0 +1,30 @@
1
+ class Layout
2
+ constructor: (@fn) ->
3
+ this.resize()
4
+ this.listen()
5
+ setTimeout (=> this.resize()), 250
6
+
7
+ resize: ->
8
+ this.fill '.x-fill', 'outerWidth', 'width'
9
+ this.fill '.y-fill', 'outerHeight', 'height'
10
+ this.fn()
11
+
12
+ fill: (selector, get, set) ->
13
+ $(selector).each (ix, node) =>
14
+ node = $(node)
15
+ getter = node[get]
16
+ parent = getter.call node.parent()
17
+ fixed = this.fixed node, selector, (n) -> getter.call n
18
+ node[set].call node, parent - fixed
19
+
20
+ fixed: (node, selector, fn) ->
21
+ node.siblings().not(selector).not('.float').filter(':visible')
22
+ .map(-> fn $ this).get()
23
+ .reduce ((sum, num) -> sum + num), 0
24
+
25
+ listen: ->
26
+ id = null
27
+ $(window).resize =>
28
+ clearTimeout id
29
+ id = setTimeout (=> this.resize()), 10
30
+ this.resize()
@@ -0,0 +1,52 @@
1
+ class LoginPage
2
+ constructor: (@session, @startPage) ->
3
+
4
+ start: ->
5
+ $('#error').hide()
6
+ callback = (success) =>
7
+ unless success
8
+ @session.disconnect()
9
+ $('#error').show()
10
+ $('#password').val('').focus()
11
+ return
12
+
13
+ localStorage['jid'] = $('#jid').val()
14
+ $('#current-user-name').text @session.bareJid()
15
+ $('#current-user-avatar').attr 'src', @session.avatar(@session.jid())
16
+ $('#current-user-avatar').attr 'alt', @session.bareJid()
17
+ $('#container').fadeOut 200, =>
18
+ $('#navbar').show()
19
+ window.location.hash = @startPage
20
+
21
+ @session.connect $('#jid').val(), $('#password').val(), callback
22
+ false
23
+
24
+ draw: ->
25
+ @session.disconnect()
26
+ jid = localStorage['jid'] || ''
27
+ $('#navbar').hide()
28
+ $('body').attr 'id', 'login-page'
29
+ $('#container').hide().empty()
30
+ $("""
31
+ <form id="login-form">
32
+ <h1>vines&gt;</h1>
33
+ <fieldset id="login-form-controls">
34
+ <input id="jid" name="jid" type="email" maxlength="1024" value="#{jid}" placeholder="Your user name"/>
35
+ <input id="password" name="password" type="password" maxlength="1024" placeholder="Your password"/>
36
+ <input id="start" type="submit" value="Sign in"/>
37
+ </fieldset>
38
+ <p id="error" style="display:none;">User name and password not found.</p>
39
+ </form>
40
+ """).appendTo '#container'
41
+ $('#login-form').submit => this.start()
42
+ $('#container').fadeIn 1000
43
+ $('#jid').keydown -> $('#error').fadeOut()
44
+ $('#password').keydown -> $('#error').fadeOut()
45
+ this.resize()
46
+
47
+ resize: ->
48
+ win = $ window
49
+ form = $ '#login-form'
50
+ sizer = -> form.css 'top', win.height() / 2 - form.height() / 2
51
+ win.resize sizer
52
+ sizer()
@@ -0,0 +1,84 @@
1
+ class NavBar
2
+ constructor: (@session) ->
3
+ @session.onCard (card) =>
4
+ if card.jid == @session.bareJid()
5
+ $('#current-user-avatar').attr 'src', @session.avatar card.jid
6
+
7
+ draw: ->
8
+ $("""
9
+ <header id="navbar" class="x-fill">
10
+ <h1 id="logo">vines&gt;</h1>
11
+ <div id="current-user">
12
+ <img id="current-user-avatar" alt="#{@session.bareJid()}" src="#{@session.avatar(@session.jid())}"/>
13
+ <div id="current-user-info">
14
+ <h1 id="current-user-name">#{@session.bareJid()}</h1>
15
+ <form id="current-user-presence-form">
16
+ <span class="select">
17
+ <span class="text">Available</span>
18
+ <select id="current-user-presence">
19
+ <optgroup label="Available">
20
+ <option>Available</option>
21
+ <option>Surfing the web</option>
22
+ <option>Reading email</option>
23
+ </optgroup>
24
+ <optgroup label="Away">
25
+ <option value="xa">Away</option>
26
+ <option value="xa">Out to lunch</option>
27
+ <option value="xa">On the phone</option>
28
+ <option value="xa">In a meeting</option>
29
+ </optgroup>
30
+ </select>
31
+ </span>
32
+ </form>
33
+ </div>
34
+ </div>
35
+ <nav id="app-nav" class="x-fill">
36
+ <ul id="nav-links"></ul>
37
+ </nav>
38
+ </header>
39
+ """).appendTo 'body'
40
+ $('<div id="container" class="x-fill y-fill"></div>').appendTo 'body'
41
+
42
+ $('#current-user-presence').change (event) =>
43
+ selected = $ 'option:selected', event.currentTarget
44
+ $('#current-user-presence-form .text').text selected.text()
45
+ @session.sendPresence selected.val() == 'xa', selected.text()
46
+
47
+ addButton: (label, icon) ->
48
+ id = "nav-link-#{label.toLowerCase()}"
49
+ node = $("""
50
+ <li>
51
+ <a id="#{id}" href="#/#{label.toLowerCase()}">
52
+ <span>#{label}</span>
53
+ </a>
54
+ </li>
55
+ """).appendTo '#nav-links'
56
+ this.button(id, icon)
57
+ node.click (event) => this.select(event.currentTarget)
58
+
59
+ select: (button) ->
60
+ button = $(button)
61
+ $('#nav-links li').removeClass('selected')
62
+ $('#nav-links li a').removeClass('selected')
63
+ button.addClass('selected')
64
+ $('a', button).addClass('selected')
65
+ dark = $('#nav-links svg path')
66
+ dark.attr 'opacity', '0.6'
67
+ dark.css 'opacity', '0.6'
68
+ light = $('svg path', button)
69
+ light.attr 'opacity', '1.0'
70
+ light.css 'opacity', '1.0'
71
+
72
+ button: (id, path) ->
73
+ paper = Raphael(id)
74
+ icon = paper.path(path).attr
75
+ fill: '#fff'
76
+ stroke: '#000'
77
+ 'stroke-width': 0.3
78
+ opacity: 0.6
79
+
80
+ node = $('#' + id)
81
+ node.hover(
82
+ -> icon.animate(opacity: 1.0, 200),
83
+ -> icon.animate(opacity: 0.6, 200) unless node.hasClass('selected'))
84
+ node.get 0