evrobone 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +3 -0
- data/.rubocop.yml +5 -0
- data/.travis.yml +9 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +53 -0
- data/Rakefile +7 -0
- data/app/assets/javascripts/evrobone/app-class.js.coffee +80 -0
- data/app/assets/javascripts/evrobone/app-mixins/custom-element-binding.js.coffee +33 -0
- data/app/assets/javascripts/evrobone/app-mixins/views-management.js.coffee +102 -0
- data/app/assets/javascripts/evrobone/app-mixins/window-navigation.js.coffee +38 -0
- data/app/assets/javascripts/evrobone/app-mixins/window-refresh.js.coffee +17 -0
- data/app/assets/javascripts/evrobone/helpers.js.coffee.erb +60 -0
- data/app/assets/javascripts/evrobone/initializers/.keep +0 -0
- data/app/assets/javascripts/evrobone/jquery-additions.js.coffee +89 -0
- data/app/assets/javascripts/evrobone/lib/jquery.ui.effect-stacked-drop.js.coffee +50 -0
- data/app/assets/javascripts/evrobone/lib/livereload-plugin-rails.js.coffee +75 -0
- data/app/assets/javascripts/evrobone/view.js.coffee +63 -0
- data/app/assets/javascripts/evrobone/views/.keep +0 -0
- data/bin/console +14 -0
- data/bin/setup +7 -0
- data/coffeelint.json +129 -0
- data/evrobone.gemspec +25 -0
- data/lib/evrobone.rb +9 -0
- data/lib/evrobone/version.rb +3 -0
- metadata +127 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 2baf24c64a08d5f463fc312f832fbda17bd01b15
|
4
|
+
data.tar.gz: 9c5702ca5a0ce30b22eef32385f88762589ba5e5
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: a5ebdfba039cb37c56a6c5ea3f5f8e7d6b967209045ee1ebd4c2cad5d16ffb7fe2496da01177f922b87246fab18b84a2744f6b7c24b0b463d37b19bdab7a9ae1
|
7
|
+
data.tar.gz: 435af2792b86a3c13f8a9e2704f653226e20fb7778355093a3bb0c9c54775fd546926d9c8c0c596e4e31df64d64517cdd98b7966f9558f98484537219f231785
|
data/.gitignore
ADDED
data/.rubocop.yml
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2015 Dmitry Karpunin (aka KODer) koderfunk@gmail.com
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
# Evrobone
|
2
|
+
|
3
|
+
Light-weight client-side framework based on Backbone.js for Ruby on Rails Front-end
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
gem 'evrobone'
|
11
|
+
```
|
12
|
+
|
13
|
+
And then execute:
|
14
|
+
|
15
|
+
$ bundle
|
16
|
+
|
17
|
+
Or install it yourself as:
|
18
|
+
|
19
|
+
$ gem install evrobone
|
20
|
+
|
21
|
+
## Usage
|
22
|
+
|
23
|
+
Coming soon...
|
24
|
+
|
25
|
+
### `Evrobone.AppClass` и организация приложения
|
26
|
+
|
27
|
+
### `Evrobone.AppMixins.ViewsManagement` и `Evrobone.View`
|
28
|
+
|
29
|
+
### `Evrobone.AppMixins.CustomElementBinding` и `/initializers`
|
30
|
+
|
31
|
+
### `Evrobone.AppMixins.WindowNavigation` и совместимость с Turbolinks
|
32
|
+
|
33
|
+
### `Evrobone.AppMixins.WindowRefresh`
|
34
|
+
|
35
|
+
### LiveReload plugin for Ruby on Rails
|
36
|
+
|
37
|
+
Для более удобного использования LiveReload в Ruby on Rails проекте, можно подключить плагин, добавив в `config/initializers/assets.rb`:
|
38
|
+
```ruby
|
39
|
+
Rails.application.config.assets.precompile += %w( evrobone/lib/livereload-plugin-rails.js ) if Rails.env.development?
|
40
|
+
```
|
41
|
+
и подключить скрипт в лэйауте:
|
42
|
+
```haml
|
43
|
+
- if Rails.env.development?
|
44
|
+
= javascript_include_tag 'evrobone/lib/livereload-plugin-rails'
|
45
|
+
```
|
46
|
+
|
47
|
+
## Contributing
|
48
|
+
|
49
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/KODerFunk/evrobone.
|
50
|
+
|
51
|
+
## License
|
52
|
+
|
53
|
+
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
@@ -0,0 +1,80 @@
|
|
1
|
+
#= require ./helpers
|
2
|
+
#= require ./jquery-additions
|
3
|
+
|
4
|
+
@Evrobone ||= {}
|
5
|
+
|
6
|
+
Evrobone.AppMixins ||= {}
|
7
|
+
|
8
|
+
class Evrobone.AppClass
|
9
|
+
@App: null
|
10
|
+
|
11
|
+
name: null
|
12
|
+
performanceReport: true
|
13
|
+
performanceMicro: false
|
14
|
+
|
15
|
+
touchDevice: ('ontouchstart' of document.documentElement and not TEST_MODE)
|
16
|
+
# or !!(window.DocumentTouch and document instanceof DocumentTouch)
|
17
|
+
$window: null
|
18
|
+
|
19
|
+
@mixinNames: null
|
20
|
+
|
21
|
+
# Reset mixinNames
|
22
|
+
@mixinable: ->
|
23
|
+
@mixinNames = if @mixinNames then _.clone(@mixinNames) else []
|
24
|
+
|
25
|
+
# Mixins support
|
26
|
+
@include: (mixin, name = null) ->
|
27
|
+
@mixinNames ||= []
|
28
|
+
if name?
|
29
|
+
@mixinNames = _.without(@mixinNames, name)
|
30
|
+
@mixinNames.push(name)
|
31
|
+
unless mixin?
|
32
|
+
cout 'error', "AppMixin #{name or '_no_name_'} is undefined"
|
33
|
+
_.extend @::, mixin
|
34
|
+
|
35
|
+
@include Backbone.Events
|
36
|
+
|
37
|
+
constructor: (name = null) ->
|
38
|
+
if @constructor.App
|
39
|
+
throw new Error('Can\'t create new AppClass instance because the single instance has already been created')
|
40
|
+
else
|
41
|
+
cout 'info', 'Evrobone.AppClass.constructor', name, @
|
42
|
+
@constructor.App = @
|
43
|
+
@name = name
|
44
|
+
for mixinName in @constructor.mixinNames
|
45
|
+
@[mixinName + 'Initialize']?()
|
46
|
+
return
|
47
|
+
|
48
|
+
start: (initial = true) ->
|
49
|
+
@$window = $(window) if initial
|
50
|
+
$('body').addClass('touch-device') if @touchDevice
|
51
|
+
@performanceReport = DEBUG_MODE if @performanceReport
|
52
|
+
@performanceStart() if @performanceReport
|
53
|
+
@trigger 'start', initial
|
54
|
+
return
|
55
|
+
|
56
|
+
stop: ->
|
57
|
+
@trigger 'stop'
|
58
|
+
return
|
59
|
+
|
60
|
+
performanceStartAt: null
|
61
|
+
|
62
|
+
performanceStart: ->
|
63
|
+
if _.isFunction(performance?.now)
|
64
|
+
@performanceStartAt = performance.now()
|
65
|
+
else
|
66
|
+
cout 'warn', 'performance.now() isnt available'
|
67
|
+
@performanceReport = null
|
68
|
+
|
69
|
+
performancePoint: (message, count = null) ->
|
70
|
+
pointAt = performance.now()
|
71
|
+
message = @_plurMessage(message, count) if count?
|
72
|
+
time = if @performanceMicro
|
73
|
+
"#{Math.round((pointAt - @performanceStartAt) * 1000)}\u00B5s"
|
74
|
+
else
|
75
|
+
"#{Math.round(pointAt - @performanceStartAt)}ms"
|
76
|
+
cout 'info', "#{message} in #{time}"
|
77
|
+
@performanceStartAt = pointAt
|
78
|
+
|
79
|
+
_plurMessage: (message, count) ->
|
80
|
+
message.replace('%count%', count).replace('%s%', if count is 1 then '' else 's')
|
@@ -0,0 +1,33 @@
|
|
1
|
+
Evrobone.AppMixins.CustomElementBinding =
|
2
|
+
|
3
|
+
customElementBindingInitialize: ->
|
4
|
+
@on 'start', @customElementBindingStart, @
|
5
|
+
return
|
6
|
+
|
7
|
+
customElementBindingStart: (initial) ->
|
8
|
+
@bindCustomElements null, initial
|
9
|
+
@performancePoint('Processed %count% custom element binder%s%', @customElementBinders.length) if @performanceReport
|
10
|
+
return
|
11
|
+
|
12
|
+
customElementBinders: []
|
13
|
+
|
14
|
+
registerCustomElementBinder: (binder) ->
|
15
|
+
@customElementBinders.push binder
|
16
|
+
|
17
|
+
bindCustomElements: ($root = $('body'), initial = false) ->
|
18
|
+
for binder in @customElementBinders
|
19
|
+
binder arguments...
|
20
|
+
return
|
21
|
+
|
22
|
+
registerEasyBinder: (name, handle) ->
|
23
|
+
if _.isArray(name)
|
24
|
+
[name, selector] = name
|
25
|
+
else
|
26
|
+
selector = ".js-#{name}"
|
27
|
+
@registerCustomElementBinder ($root, initial) ->
|
28
|
+
$elements = $root.find("#{selector}:not(.bound-#{name})")
|
29
|
+
$elements.addClass "bound-#{name}"
|
30
|
+
if $elements.length
|
31
|
+
handle $elements, initial
|
32
|
+
return
|
33
|
+
return
|
@@ -0,0 +1,102 @@
|
|
1
|
+
#= require ../view
|
2
|
+
|
3
|
+
Evrobone.AppMixins.ViewsManagement =
|
4
|
+
View: Evrobone.View
|
5
|
+
|
6
|
+
ViewMixins: {}
|
7
|
+
ProtoViews: {}
|
8
|
+
Views: {}
|
9
|
+
|
10
|
+
warnOnMultibind: true
|
11
|
+
preventMultibind: false
|
12
|
+
groupBindingLog: true
|
13
|
+
|
14
|
+
viewInstances: null
|
15
|
+
|
16
|
+
viewsManagementInitialize: ->
|
17
|
+
@viewInstances = []
|
18
|
+
@on 'start', @viewsManagementStart, @
|
19
|
+
@on 'stop', @viewsManagementStop, @
|
20
|
+
return
|
21
|
+
|
22
|
+
viewsManagementStart: (initial) ->
|
23
|
+
boundViews = @bindViews()
|
24
|
+
@performancePoint('Bound %count% view%s%', boundViews.length) if @performanceReport
|
25
|
+
return
|
26
|
+
|
27
|
+
viewsManagementStop: (initial) ->
|
28
|
+
@unbindViews @viewInstances
|
29
|
+
return
|
30
|
+
|
31
|
+
bindViews: ($root = $('html'), checkRoot = true) ->
|
32
|
+
if @groupBindingLog
|
33
|
+
console?.groupCollapsed? 'bindViews on', $root
|
34
|
+
boundViews = []
|
35
|
+
sortedViews = _.sortBy(_.pairs(@Views), (p) -> -p[1].priority or 0)
|
36
|
+
for [viewName, viewClass] in sortedViews when viewClass::el
|
37
|
+
if checkRoot
|
38
|
+
@_bindViews boundViews, $root.filter(viewClass::el), viewClass, viewName
|
39
|
+
@_bindViews boundViews, $root.find(viewClass::el), viewClass, viewName
|
40
|
+
if @groupBindingLog
|
41
|
+
console?.groupEnd?()
|
42
|
+
boundViews
|
43
|
+
|
44
|
+
_bindViews: (boundViews, $elements, viewClass, viewName) ->
|
45
|
+
$elements.each (index, el) =>
|
46
|
+
if view = @bindView(el, viewClass, viewName)
|
47
|
+
boundViews.push view
|
48
|
+
return
|
49
|
+
|
50
|
+
bindView: (element, viewClass, viewName) ->
|
51
|
+
if @canBind(element, viewClass)
|
52
|
+
options = _.defaults( el: element, $(element).data() )
|
53
|
+
view = new viewClass(options)
|
54
|
+
if viewName
|
55
|
+
cout 'info', "Bound view #{viewName}:", view
|
56
|
+
@viewInstances.push view
|
57
|
+
view
|
58
|
+
|
59
|
+
canBind: (element, viewClass) ->
|
60
|
+
if @warnOnMultibind or @preventMultibind
|
61
|
+
views = @getViewsOnElement(element)
|
62
|
+
l = views.length
|
63
|
+
if l > 0
|
64
|
+
if @warnOnMultibind
|
65
|
+
cout 'warn', @_plurMessage('Element already has bound %count% view%s%', l), element, viewClass, views
|
66
|
+
not @preventMultibind
|
67
|
+
else
|
68
|
+
true
|
69
|
+
else
|
70
|
+
true
|
71
|
+
|
72
|
+
unbindViews: (views, context = null) ->
|
73
|
+
return unless views?.length > 0
|
74
|
+
for view in views
|
75
|
+
view.undelegateEvents()
|
76
|
+
view.leave context
|
77
|
+
@viewInstances = _.without(@viewInstances, views...)
|
78
|
+
cout 'info', @_plurMessage('Unbound %count% view%s%:', views.length), views
|
79
|
+
return
|
80
|
+
|
81
|
+
getViewsOnElement: (element) ->
|
82
|
+
element = if element instanceof jQuery then element[0] else element
|
83
|
+
_.where @viewInstances, el: element
|
84
|
+
|
85
|
+
getViewsInContainer: ($container, checkRoot = true) ->
|
86
|
+
_.filter @viewInstances, (view) ->
|
87
|
+
view.$el.closest($container).length > 0 and (checkRoot or view.el isnt $container[0])
|
88
|
+
|
89
|
+
getFirstView: (viewClass) ->
|
90
|
+
for view in @viewInstances
|
91
|
+
if view.constructor is viewClass
|
92
|
+
return view
|
93
|
+
null
|
94
|
+
|
95
|
+
getFirstChildView: (viewClass) ->
|
96
|
+
for view in @viewInstances
|
97
|
+
if view instanceof viewClass
|
98
|
+
return view
|
99
|
+
null
|
100
|
+
|
101
|
+
getAllViews: (viewClass) ->
|
102
|
+
_.filter(@viewInstances, (view) -> view.constructor is viewClass)
|
@@ -0,0 +1,38 @@
|
|
1
|
+
Evrobone.AppMixins.WindowNavigation =
|
2
|
+
|
3
|
+
changeLocation: (url, push = false) ->
|
4
|
+
#cout '!> changeLocation', url
|
5
|
+
historyMethod = window.history[if push then 'pushState' else 'replaceState']
|
6
|
+
if historyMethod
|
7
|
+
historyMethod.call window.history, { turbolinks: Turbolinks?, url: url }, '', url
|
8
|
+
else
|
9
|
+
window.location.hash = url
|
10
|
+
return
|
11
|
+
|
12
|
+
visit: (location) ->
|
13
|
+
#cout '!> visit', location
|
14
|
+
if Turbolinks?
|
15
|
+
Turbolinks.visit location
|
16
|
+
else
|
17
|
+
window.location = location
|
18
|
+
return
|
19
|
+
|
20
|
+
reloadPage: ->
|
21
|
+
#cout '!> reloadPage'
|
22
|
+
@visit window.location
|
23
|
+
|
24
|
+
refreshPage: ->
|
25
|
+
if Turbolinks?
|
26
|
+
$document = $(document)
|
27
|
+
scrollTop = 0
|
28
|
+
$document.once 'page:before-unload.refreshPage', =>
|
29
|
+
scrollTop = @$window.scrollTop()
|
30
|
+
return
|
31
|
+
$document.on 'page:load.refreshPage page:restore.refreshPage', =>
|
32
|
+
@$window.scrollTop scrollTop
|
33
|
+
return
|
34
|
+
Turbolinks.visit window.location
|
35
|
+
else
|
36
|
+
# TODO: сделать возврат scrollTop пробросом через sessionStorage
|
37
|
+
window.location = window.location
|
38
|
+
return
|
@@ -0,0 +1,17 @@
|
|
1
|
+
Evrobone.AppMixins.WindowRefresh =
|
2
|
+
|
3
|
+
windowRefreshEvents: ['DOMContentLoaded', 'load', 'scroll', 'resize', 'orientationchange', 'touchmove']
|
4
|
+
windowRefreshBound: false
|
5
|
+
|
6
|
+
_bindRefresh: ($scrollable, name) ->
|
7
|
+
@windowRefreshBound = true
|
8
|
+
events = _.map(@windowRefreshEvents, (e) -> "#{e}.#{name}-refresh").join(' ')
|
9
|
+
$scrollable.on events, => @trigger "#{name}Refresh", $scrollable.scrollTop(), $scrollable
|
10
|
+
return
|
11
|
+
|
12
|
+
onWindowRefresh: (callback, context) ->
|
13
|
+
@_bindRefresh(@$window, 'window') unless @windowRefreshBound
|
14
|
+
context.listenTo @, 'windowRefresh', callback
|
15
|
+
|
16
|
+
offWindowRefresh: (callback, context) ->
|
17
|
+
context.stopListening @, 'windowRefresh', callback
|
@@ -0,0 +1,60 @@
|
|
1
|
+
window.DEBUG_MODE ?= <%= Rails.env.development? %>
|
2
|
+
window.TEST_MODE ?= <%= Rails.env.test? %>
|
3
|
+
window.LOG_TODO ?= DEBUG_MODE
|
4
|
+
|
5
|
+
window.cout = =>
|
6
|
+
args = _.toArray(arguments)
|
7
|
+
method = if args[0] in ['log', 'info', 'warn', 'error', 'assert', 'clear'] then args.shift() else 'log'
|
8
|
+
if DEBUG_MODE and console?
|
9
|
+
method = console[method]
|
10
|
+
if method.apply?
|
11
|
+
method.apply(console, args)
|
12
|
+
else
|
13
|
+
method(args)
|
14
|
+
args[0]
|
15
|
+
|
16
|
+
window._cout = ->
|
17
|
+
console.log(arguments) if console?
|
18
|
+
arguments[0]
|
19
|
+
|
20
|
+
window.todo = (subject, location = null, numberOrString = null) =>
|
21
|
+
if LOG_TODO
|
22
|
+
cout 'warn', "TODO: #{subject}#{if location then " ### #{location}" else ''}#{if numberOrString then (if _.isNumber(numberOrString) then ":#{numberOrString}" else " > #{numberOrString}") else ''}"
|
23
|
+
|
24
|
+
window.getParams = (searchString = location.search) ->
|
25
|
+
q = searchString.replace(/^\?/, '').split('&')
|
26
|
+
r = {}
|
27
|
+
for e in q
|
28
|
+
t = e.split('=')
|
29
|
+
r[decodeURIComponent(t[0])] = decodeURIComponent(t[1])
|
30
|
+
r
|
31
|
+
|
32
|
+
window.waitFor = (delay, times, check, success, fail) ->
|
33
|
+
startWaitFor = ->
|
34
|
+
setTimeout ( ->
|
35
|
+
times--
|
36
|
+
if check()
|
37
|
+
success?(times)
|
38
|
+
else if times > 0
|
39
|
+
startWaitFor()
|
40
|
+
else
|
41
|
+
fail?()
|
42
|
+
), delay
|
43
|
+
startWaitFor()
|
44
|
+
|
45
|
+
window.prepareFilterParams = (serializedArray) ->
|
46
|
+
filteredParams = _.filter(serializedArray, (param) -> param.name isnt 'utf8' and param.value isnt '')
|
47
|
+
params = []
|
48
|
+
names = []
|
49
|
+
for param in filteredParams
|
50
|
+
index = if /\[\]$/.test(param.name) then -1 else _.indexOf(names, param.name)
|
51
|
+
if index < 0
|
52
|
+
names.push param.name
|
53
|
+
params.push param
|
54
|
+
else
|
55
|
+
params[index] = param
|
56
|
+
params.sort (a, b) ->
|
57
|
+
if a.name is b.name
|
58
|
+
if a.value >= b.value then 1 else -1
|
59
|
+
else
|
60
|
+
if a.name > b.name then 1 else -1
|
File without changes
|
@@ -0,0 +1,89 @@
|
|
1
|
+
# coffeelint: disable=cyclomatic_complexity
|
2
|
+
do ($ = jQuery) =>
|
3
|
+
|
4
|
+
$.fn.nearestFind = (selector, extremeEdgeSelector = 'form, body') ->
|
5
|
+
$edge = @
|
6
|
+
$result = $edge.find(selector)
|
7
|
+
until $result.length or $edge.is(extremeEdgeSelector)
|
8
|
+
$edge = $edge.parent()
|
9
|
+
$result = $edge.find(selector)
|
10
|
+
$result
|
11
|
+
|
12
|
+
# [showOrHide[, duration[, callback]]]
|
13
|
+
$.fn.slideToggleByState = ->
|
14
|
+
if @length
|
15
|
+
if arguments.length > 0
|
16
|
+
a = _.toArray(arguments)
|
17
|
+
if a.shift()
|
18
|
+
@slideDown.apply @, a
|
19
|
+
else
|
20
|
+
@slideUp.apply @, a
|
21
|
+
else
|
22
|
+
@slideToggle()
|
23
|
+
@
|
24
|
+
|
25
|
+
# http://css-tricks.com/snippets/jquery/mover-cursor-to-end-of-textarea/
|
26
|
+
$.fn.focusToEnd = ->
|
27
|
+
@each ->
|
28
|
+
$this = $(@)
|
29
|
+
val = $this.val()
|
30
|
+
$this.focus().val('').val val
|
31
|
+
return
|
32
|
+
|
33
|
+
$.regexp ||= {}
|
34
|
+
|
35
|
+
$.regexp.rorId ||= /(\w+)_(\d+)$/
|
36
|
+
|
37
|
+
@ror_id = ($elementOrString) ->
|
38
|
+
if $elementOrString instanceof jQuery
|
39
|
+
id = $elementOrString.data('rorId')
|
40
|
+
unless id?
|
41
|
+
id = ror_id($elementOrString.attr('id'))
|
42
|
+
$elementOrString.data('rorId', id) if id?
|
43
|
+
id
|
44
|
+
else if _.isString($elementOrString)
|
45
|
+
matchResult = $elementOrString.match($.regexp.rorId)
|
46
|
+
if matchResult then parseInt(matchResult[2]) else null
|
47
|
+
else
|
48
|
+
null
|
49
|
+
|
50
|
+
$.fn.rorId = (id = null, prefix = null) ->
|
51
|
+
if arguments.length
|
52
|
+
$element = @first()
|
53
|
+
elementId = @attr('id')
|
54
|
+
if not prefix? and _.isString(elementId)
|
55
|
+
prefix = matchResult[1] if matchResult = elementId.match($.regexp.rorId)
|
56
|
+
@data('rorId', id)
|
57
|
+
if prefix?
|
58
|
+
$element.attr 'id', "#{prefix}_#{id}"
|
59
|
+
$element
|
60
|
+
else
|
61
|
+
ror_id @
|
62
|
+
|
63
|
+
$.fn.getFileName = ->
|
64
|
+
fileName = @val()
|
65
|
+
if matches = fileName.match(/(.+\\)?(.+)$/)
|
66
|
+
fileName = matches[2] or fileName
|
67
|
+
fileName
|
68
|
+
|
69
|
+
$.fn.$each = (callback) ->
|
70
|
+
@each (index, element) ->
|
71
|
+
callback $(element)
|
72
|
+
|
73
|
+
$.fn.closestScrollable = ->
|
74
|
+
$(_.find(@parents().get(), (p) -> $(p).css('overflowY') is 'auto') or window)
|
75
|
+
|
76
|
+
# RESEARCH
|
77
|
+
# $.fn.blockHide = ->
|
78
|
+
# @each -> jQuery._data @, 'olddisplay', 'block'
|
79
|
+
# @css 'display', 'none'
|
80
|
+
# @
|
81
|
+
|
82
|
+
$.getCachedScript = (url, callback) ->
|
83
|
+
$.ajax
|
84
|
+
type: 'GET'
|
85
|
+
url: url
|
86
|
+
success: callback
|
87
|
+
dataType: 'script'
|
88
|
+
cache: true
|
89
|
+
# coffeelint: enable=cyclomatic_complexity
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# jQuery UI Effects Stacked Drop based on jQuery UI Effects Drop 1.10.0
|
2
|
+
#= require jquery-ui/effect
|
3
|
+
|
4
|
+
# coffeelint: disable=cyclomatic_complexity
|
5
|
+
do ($ = jQuery) ->
|
6
|
+
|
7
|
+
$.effects.effect.stackedDrop = (o, done) ->
|
8
|
+
el = $(@)
|
9
|
+
props = ['position', 'top', 'bottom', 'left', 'right', 'opacity', 'height', 'width']
|
10
|
+
mode = $.effects.setMode(el, o.mode or 'hide')
|
11
|
+
show = mode is 'show'
|
12
|
+
direction = o.direction or 'left'
|
13
|
+
ref = if (direction is 'up' or direction is 'down') then 'top' else 'left'
|
14
|
+
motion = if (direction is 'up' or direction is 'left') then 'pos' else 'neg'
|
15
|
+
animation = opacity: (if show then 1 else 0)
|
16
|
+
|
17
|
+
# Adjust
|
18
|
+
$.effects.save el, props
|
19
|
+
el.show()
|
20
|
+
$.effects.createWrapper el
|
21
|
+
distance = o.distance or el[(if ref is 'top' then 'outerHeight' else 'outerWidth')](true) / 2
|
22
|
+
el.css('opacity', 0).css ref, (if motion is 'pos' then -distance else distance) if show
|
23
|
+
|
24
|
+
# Animation
|
25
|
+
animationValue = if show then (if motion is 'pos' then '+=' else '-=') else (if motion is 'pos' then '-=' else '+=')
|
26
|
+
animation[ref] = animationValue + distance
|
27
|
+
|
28
|
+
_complete = ->
|
29
|
+
$.effects.restore el, props
|
30
|
+
$.effects.removeWrapper el
|
31
|
+
done()
|
32
|
+
|
33
|
+
# Animate
|
34
|
+
el.animate animation,
|
35
|
+
queue: false
|
36
|
+
duration: o.duration
|
37
|
+
easing: o.easing
|
38
|
+
complete: ->
|
39
|
+
if mode is 'hide'
|
40
|
+
el.hide()
|
41
|
+
wrapper = el.parent('.ui-effects-wrapper')
|
42
|
+
if wrapper.length
|
43
|
+
wrapper.slideUp
|
44
|
+
duration: o.duration
|
45
|
+
easing: o.easing
|
46
|
+
complete: _complete
|
47
|
+
else
|
48
|
+
_complete()
|
49
|
+
undefined
|
50
|
+
# coffeelint: enable=cyclomatic_complexity
|
@@ -0,0 +1,75 @@
|
|
1
|
+
class @LiveReloadPluginRails
|
2
|
+
@identifier = 'rails'
|
3
|
+
@version = '1.0'
|
4
|
+
|
5
|
+
window: null
|
6
|
+
host: null
|
7
|
+
document: null
|
8
|
+
console: null
|
9
|
+
|
10
|
+
constructor: (@window, @host) ->
|
11
|
+
@document = @host._reloader.document
|
12
|
+
@console = @host._reloader.console
|
13
|
+
return
|
14
|
+
|
15
|
+
debounced: null
|
16
|
+
|
17
|
+
reload: (path, options) ->
|
18
|
+
# в path бывает полный путь до руби файла или имя файла последнего звена пайплайна
|
19
|
+
# в options.originalPath же бывает бывает полный путь до файла первого звена пайплайна
|
20
|
+
#cout 'reload', path, options
|
21
|
+
if /\.css_scsslint_tmp\d+\.css$/.test(path)
|
22
|
+
true
|
23
|
+
else if /\.css$/i.test(path)
|
24
|
+
@reloadStylesheet(path)
|
25
|
+
else if App?.refreshPage? and ( /\.(rb|html)$/i.test(path) or /\.haml$/i.test(options.originalPath) )
|
26
|
+
@debounced ||= _.debounce(( -> App.refreshPage() ), 300)
|
27
|
+
@debounced()
|
28
|
+
true
|
29
|
+
else
|
30
|
+
false
|
31
|
+
|
32
|
+
reloadStylesheet: (path) ->
|
33
|
+
# has to be a real array, because DOMNodeList will be modified
|
34
|
+
# coffeelint: disable=max_line_length
|
35
|
+
links = (link for link in @document.getElementsByTagName('link') when link.rel.match(/^stylesheet$/i) and not link.__LiveReload_pendingRemoval)
|
36
|
+
# coffeelint: enable=max_line_length
|
37
|
+
# handle prefixfree
|
38
|
+
if @window.StyleFix and @document.querySelectorAll
|
39
|
+
for style in @document.querySelectorAll('style[data-href]')
|
40
|
+
links.push style
|
41
|
+
@console.log "!!! LiveReload found #{links.length} LINKed stylesheets"
|
42
|
+
pathRX = new RegExp(path.replace(/\.css$/, '(\\.self)?(-[\\da-f]{32,64})?\\.css$'))
|
43
|
+
links = (link for link in links when pathRX.test(pathFromUrl(@host._reloader.linkHref(link))))
|
44
|
+
@console.log "!!! Detected #{links.length} LINKed stylesheets for path: #{path}"
|
45
|
+
if links.length
|
46
|
+
for link in links
|
47
|
+
@host._reloader.reattachStylesheetLink(link)
|
48
|
+
true
|
49
|
+
else
|
50
|
+
false
|
51
|
+
|
52
|
+
|
53
|
+
|
54
|
+
splitUrl = (url) ->
|
55
|
+
if (index = url.indexOf('#')) >= 0
|
56
|
+
hash = url.slice(index)
|
57
|
+
url = url.slice(0, index)
|
58
|
+
else
|
59
|
+
hash = ''
|
60
|
+
if (index = url.indexOf('?')) >= 0
|
61
|
+
params = url.slice(index)
|
62
|
+
url = url.slice(0, index)
|
63
|
+
else
|
64
|
+
params = ''
|
65
|
+
return { url, params, hash }
|
66
|
+
|
67
|
+
pathFromUrl = (url) ->
|
68
|
+
url = splitUrl(url).url
|
69
|
+
if url.indexOf('file://') is 0
|
70
|
+
path = url.replace ///^ file:// (localhost)? ///, ''
|
71
|
+
else
|
72
|
+
# http : // hostname :8080 /
|
73
|
+
path = url.replace ///^ ([^:]+ :)? // ([^:/]+) (:\d*)? / ///, '/'
|
74
|
+
# decodeURI has special handling of stuff like semicolons, so use decodeURIComponent
|
75
|
+
return decodeURIComponent(path)
|
@@ -0,0 +1,63 @@
|
|
1
|
+
@Evrobone ||= {}
|
2
|
+
|
3
|
+
delegateEventSplitter = /^(\S+)\s*(.*)$/
|
4
|
+
|
5
|
+
class Evrobone.View extends Backbone.View
|
6
|
+
|
7
|
+
@mixinNames: null
|
8
|
+
|
9
|
+
constructor: (options) ->
|
10
|
+
@reflectOptions options
|
11
|
+
super
|
12
|
+
|
13
|
+
reflectOptions: (options = @$el.data()) ->
|
14
|
+
@[attr] = value for attr, value of options when not _.isUndefined(@[attr])
|
15
|
+
@
|
16
|
+
|
17
|
+
# Rest mixinNames
|
18
|
+
@mixinable: ->
|
19
|
+
@mixinNames = if @mixinNames then _.clone(@mixinNames) else []
|
20
|
+
|
21
|
+
# Mixins support
|
22
|
+
@include: (mixin, name = null) ->
|
23
|
+
@mixinNames ||= []
|
24
|
+
if name?
|
25
|
+
@mixinNames = _.without(@mixinNames, name)
|
26
|
+
@mixinNames.push(name)
|
27
|
+
unless mixin?
|
28
|
+
cout 'error', "ViewMixin #{name or '_no_name_'} is undefined"
|
29
|
+
_.extend @::, mixin
|
30
|
+
|
31
|
+
mixinsEvents: (events = {}) ->
|
32
|
+
_.reduce( @constructor.mixinNames, ( (memo, name) -> _.extend(memo, _.result(@, name + 'Events')) ), events, @ )
|
33
|
+
|
34
|
+
mixinsInitialize: ->
|
35
|
+
for name in @constructor.mixinNames
|
36
|
+
@[name + 'Initialize']? arguments...
|
37
|
+
return
|
38
|
+
|
39
|
+
mixinsLeave: ->
|
40
|
+
if @constructor.mixinNames
|
41
|
+
for name in @constructor.mixinNames
|
42
|
+
@[name + 'Leave']? arguments...
|
43
|
+
return
|
44
|
+
|
45
|
+
leave: ->
|
46
|
+
@mixinsLeave()
|
47
|
+
@stopListening()
|
48
|
+
return
|
49
|
+
|
50
|
+
destroy: (destroyDOM = true) =>
|
51
|
+
App.unbindViews [@]
|
52
|
+
@$el.remove() if destroyDOM
|
53
|
+
return
|
54
|
+
|
55
|
+
# TODO: see https://github.com/jashkenas/backbone/pull/3003/files
|
56
|
+
delegateEvents: (events) ->
|
57
|
+
return super unless App.touchDevice
|
58
|
+
return @ unless events or events = _.result(@, 'events')
|
59
|
+
patchedEvents = {}
|
60
|
+
for key, method of events
|
61
|
+
[[], eventName, selector] = key.match(delegateEventSplitter)
|
62
|
+
patchedEvents[if eventName is 'click' then "touchend #{selector}" else key] = method
|
63
|
+
super patchedEvents
|
File without changes
|
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'bundler/setup'
|
4
|
+
require 'evrobone'
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require 'pry'
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require 'irb'
|
14
|
+
IRB.start
|
data/bin/setup
ADDED
data/coffeelint.json
ADDED
@@ -0,0 +1,129 @@
|
|
1
|
+
{
|
2
|
+
"arrow_spacing": {
|
3
|
+
"level": "error"
|
4
|
+
},
|
5
|
+
"braces_spacing": {
|
6
|
+
"level": "warn",
|
7
|
+
"spaces": 1,
|
8
|
+
"empty_object_spaces": 0
|
9
|
+
},
|
10
|
+
"camel_case_classes": {
|
11
|
+
"level": "error"
|
12
|
+
},
|
13
|
+
"coffeescript_error": {
|
14
|
+
"level": "error"
|
15
|
+
},
|
16
|
+
"colon_assignment_spacing": {
|
17
|
+
"level": "ignore",
|
18
|
+
"spacing": {
|
19
|
+
"left": 0,
|
20
|
+
"right": 1
|
21
|
+
}
|
22
|
+
},
|
23
|
+
"cyclomatic_complexity": {
|
24
|
+
"value": 10,
|
25
|
+
"level": "warn"
|
26
|
+
},
|
27
|
+
"duplicate_key": {
|
28
|
+
"level": "error"
|
29
|
+
},
|
30
|
+
"empty_constructor_needs_parens": {
|
31
|
+
"level": "warn"
|
32
|
+
},
|
33
|
+
"ensure_comprehensions": {
|
34
|
+
"level": "warn"
|
35
|
+
},
|
36
|
+
"eol_last": {
|
37
|
+
"level": "warn"
|
38
|
+
},
|
39
|
+
"indentation": {
|
40
|
+
"value": 2,
|
41
|
+
"level": "error"
|
42
|
+
},
|
43
|
+
"line_endings": {
|
44
|
+
"level": "warn",
|
45
|
+
"value": "unix"
|
46
|
+
},
|
47
|
+
"max_line_length": {
|
48
|
+
"value": 120,
|
49
|
+
"level": "error",
|
50
|
+
"limitComments": true
|
51
|
+
},
|
52
|
+
"missing_fat_arrows": {
|
53
|
+
"level": "ignore",
|
54
|
+
"is_strict": false
|
55
|
+
},
|
56
|
+
"newlines_after_classes": {
|
57
|
+
"value": 3,
|
58
|
+
"level": "warn"
|
59
|
+
},
|
60
|
+
"no_backticks": {
|
61
|
+
"level": "error"
|
62
|
+
},
|
63
|
+
"no_debugger": {
|
64
|
+
"level": "warn",
|
65
|
+
"console": false
|
66
|
+
},
|
67
|
+
"no_empty_functions": {
|
68
|
+
"level": "warn"
|
69
|
+
},
|
70
|
+
"no_empty_param_list": {
|
71
|
+
"level": "warn"
|
72
|
+
},
|
73
|
+
"no_implicit_braces": {
|
74
|
+
"level": "ignore",
|
75
|
+
"strict": true
|
76
|
+
},
|
77
|
+
"no_implicit_parens": {
|
78
|
+
"strict": true,
|
79
|
+
"level": "ignore"
|
80
|
+
},
|
81
|
+
"no_interpolation_in_single_quotes": {
|
82
|
+
"level": "warn"
|
83
|
+
},
|
84
|
+
"no_plusplus": {
|
85
|
+
"level": "warn"
|
86
|
+
},
|
87
|
+
"no_stand_alone_at": {
|
88
|
+
"level": "ignore"
|
89
|
+
},
|
90
|
+
"no_tabs": {
|
91
|
+
"level": "error"
|
92
|
+
},
|
93
|
+
"no_this": {
|
94
|
+
"level": "warn"
|
95
|
+
},
|
96
|
+
"no_throwing_strings": {
|
97
|
+
"level": "error"
|
98
|
+
},
|
99
|
+
"no_trailing_semicolons": {
|
100
|
+
"level": "error"
|
101
|
+
},
|
102
|
+
"no_trailing_whitespace": {
|
103
|
+
"level": "error",
|
104
|
+
"allowed_in_comments": false,
|
105
|
+
"allowed_in_empty_lines": true
|
106
|
+
},
|
107
|
+
"no_unnecessary_double_quotes": {
|
108
|
+
"level": "warn"
|
109
|
+
},
|
110
|
+
"no_unnecessary_fat_arrows": {
|
111
|
+
"level": "warn"
|
112
|
+
},
|
113
|
+
"non_empty_constructor_needs_parens": {
|
114
|
+
"level": "warn"
|
115
|
+
},
|
116
|
+
"prefer_english_operator": {
|
117
|
+
"level": "warn",
|
118
|
+
"doubleNotLevel": "warn"
|
119
|
+
},
|
120
|
+
"space_operators": {
|
121
|
+
"level": "warn"
|
122
|
+
},
|
123
|
+
"spacing_after_comma": {
|
124
|
+
"level": "warn"
|
125
|
+
},
|
126
|
+
"transform_messes_up_line_numbers": {
|
127
|
+
"level": "warn"
|
128
|
+
}
|
129
|
+
}
|
data/evrobone.gemspec
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
lib = File.expand_path('../lib', __FILE__)
|
2
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
3
|
+
require 'evrobone/version'
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = 'evrobone'
|
7
|
+
spec.version = Evrobone::VERSION
|
8
|
+
spec.authors = ['Dmitry KODer Karpunin']
|
9
|
+
spec.email = ['koderfunk@gmail.com']
|
10
|
+
|
11
|
+
spec.summary = 'Light-weight client-side framework based on Backbone.js for Ruby on Rails Front-end'
|
12
|
+
spec.description = 'Light-weight client-side framework based on Backbone.js for Ruby on Rails Front-end'
|
13
|
+
spec.homepage = 'http://github.com/KODerFunk/evrobone'
|
14
|
+
spec.license = 'MIT'
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
17
|
+
spec.bindir = 'exe'
|
18
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
19
|
+
spec.require_paths = ['lib']
|
20
|
+
|
21
|
+
spec.add_development_dependency 'bundler', '~> 1.10'
|
22
|
+
spec.add_development_dependency 'rake', '~> 10.0'
|
23
|
+
spec.add_development_dependency 'rubocop'
|
24
|
+
spec.add_development_dependency 'coffeelint'
|
25
|
+
end
|
data/lib/evrobone.rb
ADDED
metadata
ADDED
@@ -0,0 +1,127 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: evrobone
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Dmitry KODer Karpunin
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-09-10 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.10'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.10'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '10.0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '10.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rubocop
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: coffeelint
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
description: Light-weight client-side framework based on Backbone.js for Ruby on Rails
|
70
|
+
Front-end
|
71
|
+
email:
|
72
|
+
- koderfunk@gmail.com
|
73
|
+
executables: []
|
74
|
+
extensions: []
|
75
|
+
extra_rdoc_files: []
|
76
|
+
files:
|
77
|
+
- ".gitignore"
|
78
|
+
- ".rubocop.yml"
|
79
|
+
- ".travis.yml"
|
80
|
+
- Gemfile
|
81
|
+
- LICENSE.txt
|
82
|
+
- README.md
|
83
|
+
- Rakefile
|
84
|
+
- app/assets/javascripts/evrobone/app-class.js.coffee
|
85
|
+
- app/assets/javascripts/evrobone/app-mixins/custom-element-binding.js.coffee
|
86
|
+
- app/assets/javascripts/evrobone/app-mixins/views-management.js.coffee
|
87
|
+
- app/assets/javascripts/evrobone/app-mixins/window-navigation.js.coffee
|
88
|
+
- app/assets/javascripts/evrobone/app-mixins/window-refresh.js.coffee
|
89
|
+
- app/assets/javascripts/evrobone/helpers.js.coffee.erb
|
90
|
+
- app/assets/javascripts/evrobone/initializers/.keep
|
91
|
+
- app/assets/javascripts/evrobone/jquery-additions.js.coffee
|
92
|
+
- app/assets/javascripts/evrobone/lib/jquery.ui.effect-stacked-drop.js.coffee
|
93
|
+
- app/assets/javascripts/evrobone/lib/livereload-plugin-rails.js.coffee
|
94
|
+
- app/assets/javascripts/evrobone/view.js.coffee
|
95
|
+
- app/assets/javascripts/evrobone/views/.keep
|
96
|
+
- bin/console
|
97
|
+
- bin/setup
|
98
|
+
- coffeelint.json
|
99
|
+
- evrobone.gemspec
|
100
|
+
- lib/evrobone.rb
|
101
|
+
- lib/evrobone/version.rb
|
102
|
+
homepage: http://github.com/KODerFunk/evrobone
|
103
|
+
licenses:
|
104
|
+
- MIT
|
105
|
+
metadata: {}
|
106
|
+
post_install_message:
|
107
|
+
rdoc_options: []
|
108
|
+
require_paths:
|
109
|
+
- lib
|
110
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
111
|
+
requirements:
|
112
|
+
- - ">="
|
113
|
+
- !ruby/object:Gem::Version
|
114
|
+
version: '0'
|
115
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
116
|
+
requirements:
|
117
|
+
- - ">="
|
118
|
+
- !ruby/object:Gem::Version
|
119
|
+
version: '0'
|
120
|
+
requirements: []
|
121
|
+
rubyforge_project:
|
122
|
+
rubygems_version: 2.4.8
|
123
|
+
signing_key:
|
124
|
+
specification_version: 4
|
125
|
+
summary: Light-weight client-side framework based on Backbone.js for Ruby on Rails
|
126
|
+
Front-end
|
127
|
+
test_files: []
|