evrobone 0.1.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.
- 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: []
|