nali 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +29 -0
- data/Rakefile +1 -0
- data/lib/assets/javascripts/nali/application.js.coffee +27 -0
- data/lib/assets/javascripts/nali/collection.js.coffee +147 -0
- data/lib/assets/javascripts/nali/connection.js.coffee +58 -0
- data/lib/assets/javascripts/nali/controller.js.coffee +45 -0
- data/lib/assets/javascripts/nali/cookie.js.coffee +21 -0
- data/lib/assets/javascripts/nali/extensions.js.coffee +16 -0
- data/lib/assets/javascripts/nali/index.js.coffee +10 -0
- data/lib/assets/javascripts/nali/jbone.min.js +10 -0
- data/lib/assets/javascripts/nali/model.js.coffee +237 -0
- data/lib/assets/javascripts/nali/nali.js.coffee +127 -0
- data/lib/assets/javascripts/nali/notice.js.coffee +14 -0
- data/lib/assets/javascripts/nali/router.js.coffee +74 -0
- data/lib/assets/javascripts/nali/view.js.coffee +286 -0
- data/lib/nali.rb +6 -0
- data/lib/nali/application.rb +112 -0
- data/lib/nali/connection.rb +96 -0
- data/lib/nali/controller.rb +68 -0
- data/lib/nali/extensions.rb +11 -0
- data/lib/nali/helpers.rb +18 -0
- data/lib/nali/model.rb +48 -0
- data/lib/nali/version.rb +3 -0
- metadata +279 -0
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2014 4urbanoff
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
# Nali
|
2
|
+
|
3
|
+
TODO: Write a gem description
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
gem 'nali'
|
10
|
+
|
11
|
+
And then execute:
|
12
|
+
|
13
|
+
$ bundle
|
14
|
+
|
15
|
+
Or install it yourself as:
|
16
|
+
|
17
|
+
$ gem install nali
|
18
|
+
|
19
|
+
## Usage
|
20
|
+
|
21
|
+
TODO: Write usage instructions here
|
22
|
+
|
23
|
+
## Contributing
|
24
|
+
|
25
|
+
1. Fork it
|
26
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
27
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
28
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
29
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
@@ -0,0 +1,27 @@
|
|
1
|
+
Nali.extend Application:
|
2
|
+
|
3
|
+
domEngine: jBone.noConflict()
|
4
|
+
wsServer: 'ws://' + window.location.host
|
5
|
+
defaultUrl: ''
|
6
|
+
notFoundUrl: ''
|
7
|
+
htmlContainer: 'body'
|
8
|
+
title: 'Application'
|
9
|
+
|
10
|
+
run: ( options ) ->
|
11
|
+
@::starting()
|
12
|
+
@[ key ] = value for key, value of options
|
13
|
+
document.addEventListener 'DOMContentLoaded', =>
|
14
|
+
document.removeEventListener 'DOMContentLoaded', arguments.callee, false
|
15
|
+
@::_ = @domEngine
|
16
|
+
@htmlContainer = @_ @htmlContainer
|
17
|
+
@setTitle @title
|
18
|
+
@trigger 'start'
|
19
|
+
, false
|
20
|
+
|
21
|
+
setTitle: ( @title ) ->
|
22
|
+
unless @titleBox
|
23
|
+
@_( '<title>' ).appendTo 'head' unless @_( 'head title' ).lenght
|
24
|
+
@titleBox = @_ 'head title'
|
25
|
+
@titleBox.text @title
|
26
|
+
@trigger 'update.title'
|
27
|
+
@
|
@@ -0,0 +1,147 @@
|
|
1
|
+
Nali.extend Collection:
|
2
|
+
|
3
|
+
toShowViews: []
|
4
|
+
visibleViews: []
|
5
|
+
length: 0
|
6
|
+
|
7
|
+
cloning: ->
|
8
|
+
@subscribeTo @Model, "create.#{ @model.sysname.lowercase() }", @onModelCreated
|
9
|
+
@adaptations = []
|
10
|
+
@ordering = {}
|
11
|
+
@adaptCollection()
|
12
|
+
@
|
13
|
+
|
14
|
+
onModelCreated: ( model ) ->
|
15
|
+
@add model if model.isCorrect @filters
|
16
|
+
@
|
17
|
+
|
18
|
+
onModelUpdated: ( model ) ->
|
19
|
+
@remove model unless model.isCorrect @filters
|
20
|
+
@
|
21
|
+
|
22
|
+
onModelDestroyed: ( model ) ->
|
23
|
+
@remove model
|
24
|
+
@
|
25
|
+
|
26
|
+
adaptCollection: ->
|
27
|
+
for name, method of @model when /^_\w+$/.test( name ) and typeof method is 'function'
|
28
|
+
do ( name, method ) =>
|
29
|
+
@[ name = name[ 1.. ] ] = ( args... ) =>
|
30
|
+
@adaptation ( model ) -> model[ name ] args...
|
31
|
+
@
|
32
|
+
@
|
33
|
+
|
34
|
+
adaptModel: ( model ) ->
|
35
|
+
adaptation.call @, model for adaptation in @adaptations
|
36
|
+
@
|
37
|
+
|
38
|
+
adaptation: ( callback ) ->
|
39
|
+
callback.call @, model for model in @
|
40
|
+
@adaptations.push callback
|
41
|
+
@
|
42
|
+
|
43
|
+
add: ( model ) ->
|
44
|
+
Array::push.call @, model
|
45
|
+
@adaptModel model
|
46
|
+
@subscribeTo model, 'destroy', @onModelDestroyed
|
47
|
+
@subscribeTo model, 'update', @onModelUpdated
|
48
|
+
@reorder()
|
49
|
+
@trigger 'update'
|
50
|
+
@trigger 'update.length'
|
51
|
+
@
|
52
|
+
|
53
|
+
indexOf: ( model ) ->
|
54
|
+
Array::indexOf.call @, model
|
55
|
+
|
56
|
+
remove: ( model ) ->
|
57
|
+
Array::splice.call @, @indexOf( model ), 1
|
58
|
+
@unsubscribeTo model
|
59
|
+
@reorder()
|
60
|
+
@trigger 'update'
|
61
|
+
@trigger 'update.length'
|
62
|
+
@
|
63
|
+
|
64
|
+
removeAll: ->
|
65
|
+
delete @[ index ] for model, index in @
|
66
|
+
@length = 0
|
67
|
+
@
|
68
|
+
|
69
|
+
sort: ( sorter ) ->
|
70
|
+
Array::sort.call @, sorter
|
71
|
+
@
|
72
|
+
|
73
|
+
order: ( @ordering ) ->
|
74
|
+
@reorder()
|
75
|
+
@
|
76
|
+
|
77
|
+
reorder: ->
|
78
|
+
if @ordering.by?
|
79
|
+
clearTimeout @ordering.timer if @ordering.timer?
|
80
|
+
@ordering.timer = setTimeout =>
|
81
|
+
@sort ( one, two ) =>
|
82
|
+
one = one[ @ordering.by ]
|
83
|
+
two = two[ @ordering.by ]
|
84
|
+
if @ordering.as is 'number'
|
85
|
+
one = parseFloat one
|
86
|
+
two = parseFloat two
|
87
|
+
if @ordering.as is 'string'
|
88
|
+
one = one + ''
|
89
|
+
two = two + ''
|
90
|
+
( if one > two then 1 else if one < two then -1 else 0 ) * ( if @ordering.desc then -1 else 1 )
|
91
|
+
@orderViews()
|
92
|
+
delete @ordering.timer
|
93
|
+
, 5
|
94
|
+
@
|
95
|
+
|
96
|
+
orderViews: ->
|
97
|
+
if @inside
|
98
|
+
children = Array::slice.call @inside.children
|
99
|
+
children.sort ( one, two ) => @indexOf( one.view.model ) - @indexOf( two.view.model )
|
100
|
+
@inside.appendChild child for child in children
|
101
|
+
@
|
102
|
+
|
103
|
+
show: ( viewName, insertTo, isRelation = false ) ->
|
104
|
+
@adaptation ( model ) ->
|
105
|
+
view = model.view viewName
|
106
|
+
if isRelation
|
107
|
+
view.subscribeTo @, 'reset', view.hide
|
108
|
+
else unless @visible
|
109
|
+
@visible = true
|
110
|
+
@prepareViewToShow view
|
111
|
+
@hideVisibleViews()
|
112
|
+
else
|
113
|
+
@::visibleViews.push view
|
114
|
+
view.show insertTo
|
115
|
+
@inside ?= view.element[0].parentNode
|
116
|
+
@
|
117
|
+
|
118
|
+
prepareViewToShow: ( view ) ->
|
119
|
+
unless view in @::toShowViews
|
120
|
+
@::toShowViews.push view
|
121
|
+
@prepareViewToShow layout if ( layout = view.layout() )?.childOf? 'View'
|
122
|
+
@
|
123
|
+
|
124
|
+
hideVisibleViews: ->
|
125
|
+
view.hide() for view in @::visibleViews when not( view in @::toShowViews )
|
126
|
+
@::visibleViews = @::toShowViews
|
127
|
+
@::toShowViews = []
|
128
|
+
@
|
129
|
+
|
130
|
+
first: ->
|
131
|
+
@[0]
|
132
|
+
|
133
|
+
last: ->
|
134
|
+
@[ @length - 1 ]
|
135
|
+
|
136
|
+
reset: ->
|
137
|
+
@inside = null
|
138
|
+
@adaptations.length = 0
|
139
|
+
@trigger 'reset'
|
140
|
+
@
|
141
|
+
|
142
|
+
destroy: ->
|
143
|
+
@trigger 'destroy'
|
144
|
+
@destroyObservation()
|
145
|
+
@removeAll()
|
146
|
+
@reset()
|
147
|
+
@
|
@@ -0,0 +1,58 @@
|
|
1
|
+
Nali.extend Connection:
|
2
|
+
|
3
|
+
initialize: ->
|
4
|
+
@subscribeTo @Application, 'start', @open
|
5
|
+
@::query = ( args... ) => @query args...
|
6
|
+
@
|
7
|
+
|
8
|
+
open: ->
|
9
|
+
@dispatcher = new WebSocket @Application.wsServer
|
10
|
+
@dispatcher.onopen = ( event ) => @onOpen event
|
11
|
+
@dispatcher.onclose = ( event ) => @onClose event
|
12
|
+
@dispatcher.onmessage = ( event ) => @onMessage JSON.parse event.data
|
13
|
+
|
14
|
+
journal: []
|
15
|
+
|
16
|
+
onOpen: ( event ) ->
|
17
|
+
@trigger 'open'
|
18
|
+
|
19
|
+
onMessage: ( message ) ->
|
20
|
+
@[ message.action ] message
|
21
|
+
|
22
|
+
onClose: ( event ) ->
|
23
|
+
@trigger 'close'
|
24
|
+
|
25
|
+
send: ( msg ) ->
|
26
|
+
@dispatcher.send JSON.stringify msg
|
27
|
+
@
|
28
|
+
|
29
|
+
sync: ( message ) ->
|
30
|
+
@Model.sync message.params
|
31
|
+
@
|
32
|
+
|
33
|
+
notice: ( { model, notice, params } ) ->
|
34
|
+
if model?
|
35
|
+
[ model, id ] = model.split '.'
|
36
|
+
@Model.notice model: model, id: id, notice: notice, params: params
|
37
|
+
else @Notice[ notice ] params
|
38
|
+
@
|
39
|
+
|
40
|
+
success: ( message ) ->
|
41
|
+
@journal[ message.journal_id ].success message.params
|
42
|
+
delete @journal[ message.journal_id ]
|
43
|
+
@
|
44
|
+
|
45
|
+
failure: ( message ) ->
|
46
|
+
@journal[ message.journal_id ].failure message.params
|
47
|
+
delete @journal[ message.journal_id ]
|
48
|
+
@
|
49
|
+
|
50
|
+
query: ( to, params, success, failure ) ->
|
51
|
+
[ controller, action ] = to.split '.'
|
52
|
+
@journal.push callbacks = success: success, failure: failure
|
53
|
+
@send
|
54
|
+
controller: controller
|
55
|
+
action: action
|
56
|
+
params: params
|
57
|
+
journal_id: @journal.indexOf callbacks
|
58
|
+
@
|
@@ -0,0 +1,45 @@
|
|
1
|
+
Nali.extend Controller:
|
2
|
+
|
3
|
+
extension: ->
|
4
|
+
if @sysname isnt 'Controller'
|
5
|
+
@prepareActions()
|
6
|
+
@modelSysname = @sysname.replace /s$/, ''
|
7
|
+
@
|
8
|
+
|
9
|
+
prepareActions: ->
|
10
|
+
@routedActions = {}
|
11
|
+
if @actions?
|
12
|
+
for action of @actions when action isnt 'default'
|
13
|
+
[ name, filters... ] = action.split '/'
|
14
|
+
params = []
|
15
|
+
for filter in filters[ 0.. ] when /^:/.test filter
|
16
|
+
filters.splice filters.indexOf( filter ), 1
|
17
|
+
params.push filter[ 1.. ]
|
18
|
+
@routedActions[ name ] = name: action, filters: filters, params: params
|
19
|
+
@
|
20
|
+
|
21
|
+
runAction: ( name, filters, params ) ->
|
22
|
+
collection = @Model.extensions[ @modelSysname ].where filters
|
23
|
+
result = @actions[ @routedActions[ name ].name ].call @, collection, params
|
24
|
+
if result instanceof Object and result.render is false
|
25
|
+
collection.destroy()
|
26
|
+
else
|
27
|
+
collection.show name
|
28
|
+
@changeUrl name, filters
|
29
|
+
@
|
30
|
+
|
31
|
+
redirect: ( args... ) ->
|
32
|
+
@::redirect args...
|
33
|
+
render: false
|
34
|
+
|
35
|
+
query: ( args... ) ->
|
36
|
+
@::query args...
|
37
|
+
render: false
|
38
|
+
|
39
|
+
changeUrl: ( action, filters ) ->
|
40
|
+
params = ( value for own key, value of filters )
|
41
|
+
url = @sysname.lowercase().replace /s$/, ''
|
42
|
+
url += if action is @actions.default then '' else '/' + action
|
43
|
+
url += '/' + params.join '/' if params.length
|
44
|
+
@Router.setUrl url
|
45
|
+
@
|
@@ -0,0 +1,21 @@
|
|
1
|
+
Nali.extend Cookie:
|
2
|
+
|
3
|
+
set: ( name, value, options = {} ) ->
|
4
|
+
set = "#{ name }=#{ escape( value ) }"
|
5
|
+
if options.live? and typeof options.live is 'number'
|
6
|
+
date = new Date
|
7
|
+
date.setDate date.getDate() + options.live
|
8
|
+
date.setMinutes date.getMinutes() - date.getTimezoneOffset()
|
9
|
+
set += "; expires=#{ date.toUTCString() }"
|
10
|
+
set += '; domain=' + escape options.domain if options.domain?
|
11
|
+
set += '; path=' + if options.path? then escape options.path else '/'
|
12
|
+
set += '; secure' if options.secure?
|
13
|
+
document.cookie = set
|
14
|
+
value
|
15
|
+
|
16
|
+
get: ( name ) ->
|
17
|
+
get = document.cookie.match "(^|;) ?#{ name }=([^;]*)(;|$)"
|
18
|
+
if get then unescape( get[2] ) else null
|
19
|
+
|
20
|
+
remove: ( name ) ->
|
21
|
+
@set name, '', live: -1
|
@@ -0,0 +1,16 @@
|
|
1
|
+
String::uppercase = ->
|
2
|
+
"#{ @toUpperCase() }"
|
3
|
+
|
4
|
+
String::lowercase = ->
|
5
|
+
"#{ @toLowerCase() }"
|
6
|
+
|
7
|
+
String::capitalize = ->
|
8
|
+
@charAt(0).uppercase() + @slice(1)
|
9
|
+
|
10
|
+
String::camelcase = ->
|
11
|
+
@replace /(_[^_]+)/g, ( match ) -> match[ 1.. ].capitalize()
|
12
|
+
|
13
|
+
String::underscore = ->
|
14
|
+
str = @replace /([A-Z])/g, ( match ) -> '_' + match.lowercase()
|
15
|
+
if str[ 0...1 ] is '_' then str[ 1.. ] else str
|
16
|
+
|
@@ -0,0 +1,10 @@
|
|
1
|
+
/*!
|
2
|
+
* jBone v1.0.18 - 2014-07-08 - Library for DOM manipulation
|
3
|
+
*
|
4
|
+
* https://github.com/kupriyanenko/jbone
|
5
|
+
*
|
6
|
+
* Copyright 2014 Alexey Kupriyanenko
|
7
|
+
* Released under the MIT license.
|
8
|
+
*/
|
9
|
+
|
10
|
+
!function(a){function b(b){var c=b.length,d=typeof b;return o(d)||b===a?!1:1===b.nodeType&&c?!0:p(d)||0===c||"number"==typeof c&&c>0&&c-1 in b}function c(a,b){var c,d;this.originalEvent=a,d=function(a,b){this[a]="preventDefault"===a?function(){return this.defaultPrevented=!0,b[a]()}:o(b[a])?function(){return b[a]()}:b[a]};for(c in a)(a[c]||"function"==typeof a[c])&&d.call(this,c,a);q.extend(this,b)}var d,e=a.$,f=a.jBone,g=/^<(\w+)\s*\/?>$/,h=/^(?:[^#<]*(<[\w\W]+>)[^>]*$|#([\w\-]*)$)/,i=[].slice,j=[].splice,k=Object.keys,l=document,m=function(a){return"string"==typeof a},n=function(a){return a instanceof Object},o=function(a){var b={};return a&&"[object Function]"===b.toString.call(a)},p=function(a){return Array.isArray(a)},q=function(a,b){return new d.init(a,b)};q.noConflict=function(){return a.$=e,a.jBone=f,q},d=q.fn=q.prototype={init:function(a,b){var c,d,e,f;if(!a)return this;if(m(a)){if(d=g.exec(a))return this[0]=l.createElement(d[1]),this.length=1,n(b)&&this.attr(b),this;if((d=h.exec(a))&&d[1]){for(f=l.createDocumentFragment(),e=l.createElement("div"),e.innerHTML=a;e.lastChild;)f.appendChild(e.firstChild);return c=i.call(f.childNodes),q.merge(this,c)}if(q.isElement(b))return q(b).find(a);try{return c=l.querySelectorAll(a),q.merge(this,c)}catch(j){return this}}return a.nodeType?(this[0]=a,this.length=1,this):o(a)?a():a instanceof q?a:q.makeArray(a,this)},pop:[].pop,push:[].push,reverse:[].reverse,shift:[].shift,sort:[].sort,splice:[].splice,slice:[].slice,indexOf:[].indexOf,forEach:[].forEach,unshift:[].unshift,concat:[].concat,join:[].join,every:[].every,some:[].some,filter:[].filter,map:[].map,reduce:[].reduce,reduceRight:[].reduceRight,length:0},d.constructor=q,d.init.prototype=d,q.setId=function(b){var c=b.jid;b===a?c="window":void 0===b.jid&&(b.jid=c=++q._cache.jid),q._cache.events[c]||(q._cache.events[c]={})},q.getData=function(b){b=b instanceof q?b[0]:b;var c=b===a?"window":b.jid;return{jid:c,events:q._cache.events[c]}},q.isElement=function(a){return a&&a instanceof q||a instanceof HTMLElement||m(a)},q._cache={events:{},jid:0},q.merge=function(a,b){for(var c=b.length,d=a.length,e=0;c>e;)a[d++]=b[e++];return a.length=d,a},q.contains=function(a,b){var c;return a.reverse().some(function(a){return a.contains(b)?c=a:void 0}),c},q.extend=function(a){var b,c,d,e;return j.call(arguments,1).forEach(function(f){if(f)for(b=k(f),c=b.length,d=0,e=a;c>d;d++)e[b[d]]=f[b[d]]}),a},q.makeArray=function(a,c){var d=c||[];return null!==a&&(b(a)?q.merge(d,m(a)?[a]:a):d.push(a)),d},q.Event=function(a,b){var c,d;return a.type&&!b&&(b=a,a=a.type),c=a.split(".").splice(1).join("."),d=a.split(".")[0],a=l.createEvent("Event"),a.initEvent(d,!0,!0),q.extend(a,{namespace:c,isDefaultPrevented:function(){return a.defaultPrevented}},b)},d.on=function(a){var b,d,e,f,g,h,i,j,k=arguments,l=this.length,m=0;for(2===k.length?b=k[1]:(d=k[1],b=k[2]),j=function(j){q.setId(j),g=q.getData(j).events,a.split(" ").forEach(function(a){h=a.split(".")[0],e=a.split(".").splice(1).join("."),g[h]=g[h]||[],f=function(a){a.namespace&&a.namespace!==e||(i=null,d?(~q(j).find(d).indexOf(a.target)||(i=q.contains(q(j).find(d),a.target)))&&(i=i||a.target,a=new c(a,{currentTarget:i}),b.call(i,a)):b.call(j,a))},g[h].push({namespace:e,fn:f,originfn:b}),j.addEventListener&&j.addEventListener(h,f,!1)})};l>m;m++)j(this[m]);return this},d.one=function(a){var b,c,d,e=arguments,f=0,g=this.length;for(2===e.length?b=e[1]:(c=e[1],b=e[2]),d=function(d){a.split(" ").forEach(function(a){var e=function(c){q(d).off(a,e),b.call(d,c)};c?q(d).on(a,c,e):q(d).on(a,e)})};g>f;f++)d(this[f]);return this},d.trigger=function(a){var b,c=[],d=0,e=this.length;if(!a)return this;for(m(a)?c=a.split(" ").map(function(a){return q.Event(a)}):(a=a instanceof Event?a:q.Event(a),c=[a]),b=function(a){c.forEach(function(b){b.type&&a.dispatchEvent&&a.dispatchEvent(b)})};e>d;d++)b(this[d]);return this},d.off=function(a,b){var c,d,e,f,g=0,h=this.length,i=function(a,c,d,e,f){var g;(b&&f.originfn===b||!b)&&(g=f.fn),a[c][d].fn===g&&(e.removeEventListener(c,g),q._cache.events[q.getData(e).jid][c].splice(d,1))};for(e=function(b){var e,g,h;return(c=q.getData(b).events)?!a&&c?k(c).forEach(function(a){for(g=c[a],e=g.length;e--;)i(c,a,e,b,g[e])}):void a.split(" ").forEach(function(a){if(f=a.split(".")[0],d=a.split(".").splice(1).join("."),c[f])for(g=c[f],e=g.length;e--;)h=g[e],(!d||d&&h.namespace===d)&&i(c,f,e,b,h);else d&&k(c).forEach(function(a){for(g=c[a],e=g.length;e--;)h=g[e],h.namespace.split(".")[0]===d.split(".")[0]&&i(c,a,e,b,h)})}):void 0};h>g;g++)e(this[g]);return this},d.find=function(a){for(var b=[],c=0,d=this.length,e=function(c){o(c.querySelectorAll)&&[].forEach.call(c.querySelectorAll(a),function(a){b.push(a)})};d>c;c++)e(this[c]);return q(b)},d.get=function(a){return this[a]},d.eq=function(a){return q(this[a])},d.parent=function(){for(var a,b=[],c=0,d=this.length;d>c;c++)!~b.indexOf(a=this[c].parentElement)&&a&&b.push(a);return q(b)},d.toArray=function(){return i.call(this)},d.is=function(){var a=arguments;return this.some(function(b){return b.tagName.toLowerCase()===a[0]})},d.has=function(){var a=arguments;return this.some(function(b){return b.querySelectorAll(a[0]).length})},d.attr=function(a,b){var c,d=arguments,e=0,f=this.length;if(m(a)&&1===d.length)return this[0]&&this[0].getAttribute(a);for(2===d.length?c=function(c){c.setAttribute(a,b)}:n(a)&&(c=function(b){k(a).forEach(function(c){b.setAttribute(c,a[c])})});f>e;e++)c(this[e]);return this},d.val=function(a){var b=0,c=this.length;if(0===arguments.length)return this[0]&&this[0].value;for(;c>b;b++)this[b].value=a;return this},d.css=function(b,c){var d,e=arguments,f=0,g=this.length;if(m(b)&&1===e.length)return this[0]&&a.getComputedStyle(this[0])[b];for(2===e.length?d=function(a){a.style[b]=c}:n(b)&&(d=function(a){k(b).forEach(function(c){a.style[c]=b[c]})});g>f;f++)d(this[f]);return this},d.data=function(a,b){var c,d=arguments,e={},f=0,g=this.length,h=function(a,b,c){n(c)?(a.jdata=a.jdata||{},a.jdata[b]=c):a.dataset[b]=c},i=function(a){return"true"===a?!0:"false"===a?!1:a};if(0===d.length)return this[0].jdata&&(e=this[0].jdata),k(this[0].dataset).forEach(function(a){e[a]=i(this[0].dataset[a])},this),e;if(1===d.length&&m(a))return this[0]&&i(this[0].dataset[a]||this[0].jdata&&this[0].jdata[a]);for(1===d.length&&n(a)?c=function(b){k(a).forEach(function(c){h(b,c,a[c])})}:2===d.length&&(c=function(c){h(c,a,b)});g>f;f++)c(this[f]);return this},d.removeData=function(a){for(var b,c,d=0,e=this.length;e>d;d++)if(b=this[d].jdata,c=this[d].dataset,a)b&&b[a]&&delete b[a],delete c[a];else{for(a in b)delete b[a];for(a in c)delete c[a]}return this},d.html=function(a){var b,c=arguments;return 1===c.length&&void 0!==a?this.empty().append(a):0===c.length&&(b=this[0])?b.innerHTML:this},d.append=function(a){var b,c=0,d=this.length;for(m(a)&&h.exec(a)?a=q(a):n(a)||(a=document.createTextNode(a)),a=a instanceof q?a:q(a),b=function(b,c){a.forEach(function(a){b.appendChild(c?a.cloneNode():a)})};d>c;c++)b(this[c],c);return this},d.appendTo=function(a){return q(a).append(this),this},d.empty=function(){for(var a,b=0,c=this.length;c>b;b++)for(a=this[b];a.lastChild;)a.removeChild(a.lastChild);return this},d.remove=function(){var a,b=0,c=this.length;for(this.off();c>b;b++)a=this[b],delete a.jdata,a.parentNode&&a.parentNode.removeChild(a);return this},"object"==typeof module&&module&&"object"==typeof module.exports?module.exports=q:"function"==typeof define&&define.amd?(define(function(){return q}),a.jBone=a.$=q):"object"==typeof a&&"object"==typeof a.document&&(a.jBone=a.$=q)}(window);
|
@@ -0,0 +1,237 @@
|
|
1
|
+
Nali.extend Model:
|
2
|
+
|
3
|
+
extension: ->
|
4
|
+
if @sysname isnt 'Model'
|
5
|
+
@table = @tables[ @sysname ] ?= []
|
6
|
+
@table.index = {}
|
7
|
+
@adapt()
|
8
|
+
@
|
9
|
+
|
10
|
+
tables: {}
|
11
|
+
hasOne: []
|
12
|
+
hasMany: []
|
13
|
+
attributes: {}
|
14
|
+
updated: 0
|
15
|
+
noticesWait: []
|
16
|
+
|
17
|
+
adapt: ->
|
18
|
+
for name, method of @ when /^_\w+/.test name
|
19
|
+
do ( name, method ) =>
|
20
|
+
if typeof method is 'function'
|
21
|
+
@[ name[ 1.. ] ] = ( args... ) -> @[ name ] args...
|
22
|
+
@
|
23
|
+
|
24
|
+
notice: ( params ) ->
|
25
|
+
@noticesWait.push params
|
26
|
+
@runNotices()
|
27
|
+
@
|
28
|
+
|
29
|
+
runNotices: ->
|
30
|
+
for item, index in @noticesWait[ 0.. ]
|
31
|
+
if model = @extensions[ item.model ].find item.id
|
32
|
+
model[ item.notice ] item.params
|
33
|
+
@noticesWait.splice @noticesWait.indexOf( item ), 1
|
34
|
+
@
|
35
|
+
|
36
|
+
force: ( attributes = {} ) ->
|
37
|
+
( attributes = @copy attributes ).id ?= null
|
38
|
+
for name, value of @attributes when not ( name of attributes )
|
39
|
+
if value instanceof Object
|
40
|
+
attributes[ name ] = if value.default? then value.default else null
|
41
|
+
else attributes[ name ] = value or null
|
42
|
+
attributes[ name ] = @normalizeValue value for name, value of attributes
|
43
|
+
@clone( attributes: attributes ).accessing()
|
44
|
+
|
45
|
+
accessing: ->
|
46
|
+
@access @attributes
|
47
|
+
@setRelations()
|
48
|
+
@
|
49
|
+
|
50
|
+
save: ( success, failure ) ->
|
51
|
+
if @isValid()?
|
52
|
+
@query "#{ @sysname.lowercase() }s.save", @attributes, #=> success? @
|
53
|
+
( { attributes, created, updated } ) =>
|
54
|
+
@update( attributes, updated, created ).write()
|
55
|
+
success? @
|
56
|
+
else failure? @
|
57
|
+
@
|
58
|
+
|
59
|
+
sync: ( { sysname, attributes, created, updated, destroyed } ) ->
|
60
|
+
if model = @extensions[ sysname ].find attributes.id
|
61
|
+
if destroyed then model.remove()
|
62
|
+
else model.update attributes, updated, created
|
63
|
+
else
|
64
|
+
model = @extensions[ sysname ].build attributes
|
65
|
+
model.updated = updated
|
66
|
+
model.created = created
|
67
|
+
model.write()
|
68
|
+
@
|
69
|
+
|
70
|
+
select: ( filters, success, failure ) ->
|
71
|
+
@query @sysname.lowercase() + 's.select', filters, success, failure if Object.keys( filters ).length
|
72
|
+
|
73
|
+
destroy: ( success, failure ) ->
|
74
|
+
@query @sysname.lowercase() + 's.destroy', @attributes, success, failure
|
75
|
+
|
76
|
+
write: ->
|
77
|
+
unless @ in @table
|
78
|
+
@table.push @
|
79
|
+
@table.index[ @id ] = @
|
80
|
+
@onCreate?()
|
81
|
+
@Model.trigger "create.#{ @sysname.lowercase() }", @
|
82
|
+
@Model.runNotices()
|
83
|
+
@
|
84
|
+
|
85
|
+
remove: ->
|
86
|
+
if @ in @table
|
87
|
+
delete @table.index[ @id ]
|
88
|
+
@table.splice @table.indexOf( @ ), 1
|
89
|
+
@trigger 'destroy', @
|
90
|
+
@onDestroy?()
|
91
|
+
@unsubscribeAll()
|
92
|
+
@
|
93
|
+
|
94
|
+
build: ( attributes ) ->
|
95
|
+
@force attributes
|
96
|
+
|
97
|
+
create: ( attributes, success, failure ) ->
|
98
|
+
@build( attributes ).save success, failure
|
99
|
+
|
100
|
+
update: ( attributes, updated = 0, created = 0 ) ->
|
101
|
+
if not updated or updated > @updated
|
102
|
+
@created = created if created
|
103
|
+
changed = []
|
104
|
+
changed.push name for name, value of attributes when @update_attribute name, value
|
105
|
+
if changed.length
|
106
|
+
@updated = updated if updated
|
107
|
+
@onUpdate? changed
|
108
|
+
@trigger 'update', @, changed
|
109
|
+
@
|
110
|
+
|
111
|
+
update_attribute: ( name, value ) ->
|
112
|
+
value = @normalizeValue value
|
113
|
+
if @attributes[ name ] isnt value and @isValidAttributeValue( name, value )?
|
114
|
+
@attributes[ name ] = value
|
115
|
+
@[ 'onUpdate' + name.capitalize() ]?()
|
116
|
+
@trigger "update.#{ name }", @
|
117
|
+
true
|
118
|
+
else false
|
119
|
+
|
120
|
+
find: ( id ) ->
|
121
|
+
@table.index[ id ]
|
122
|
+
|
123
|
+
where: ( filters ) ->
|
124
|
+
collection = @Collection.clone model: @, filters: filters
|
125
|
+
collection.add model for model in @table when model.isCorrect filters
|
126
|
+
if @forced and not collection.length
|
127
|
+
attributes = {}
|
128
|
+
attributes[ key ] = value for key, value of filters when typeof value in [ 'number', 'string' ]
|
129
|
+
collection.add @build attributes
|
130
|
+
@select filters
|
131
|
+
collection
|
132
|
+
|
133
|
+
normalizeValue: ( value ) ->
|
134
|
+
if typeof value is 'string'
|
135
|
+
value = "#{ value }".trim()
|
136
|
+
if value is ( ( correct = parseInt( value ) ) + '' ) then correct else value
|
137
|
+
else value
|
138
|
+
|
139
|
+
isCorrect: ( filters = {} ) ->
|
140
|
+
return false unless Object.keys( filters ).length
|
141
|
+
return false for name, filter of filters when not @isCorrectAttribute @attributes[ name ], filter
|
142
|
+
return true
|
143
|
+
|
144
|
+
isCorrectAttribute: ( attribute, filter ) ->
|
145
|
+
return false unless attribute
|
146
|
+
if filter instanceof RegExp
|
147
|
+
filter.test attribute
|
148
|
+
else if typeof filter is 'string'
|
149
|
+
attribute.toString() is filter
|
150
|
+
else if typeof filter is 'number'
|
151
|
+
parseInt( attribute ) is filter
|
152
|
+
else if filter instanceof Array
|
153
|
+
attribute.toString() in filter or parseInt( attribute ) in filter
|
154
|
+
else false
|
155
|
+
|
156
|
+
setRelations: ->
|
157
|
+
@belongsToRelation attribute for attribute of @attributes when /_id$/.test attribute
|
158
|
+
@hasOneRelation attribute for attribute in [].concat @hasOne
|
159
|
+
@hasManyRelation attribute for attribute in [].concat @hasMany
|
160
|
+
@
|
161
|
+
|
162
|
+
belongsToRelation: ( attribute ) ->
|
163
|
+
name = attribute.replace '_id', ''
|
164
|
+
model = @Model.extensions[ name.capitalize() ]
|
165
|
+
@getter name, => model.find @[ attribute ]
|
166
|
+
@
|
167
|
+
|
168
|
+
hasOneRelation: ( name ) ->
|
169
|
+
@getter name, =>
|
170
|
+
delete @[ name ]
|
171
|
+
( filters = {} )[ "#{ @sysname.lowercase() }_id" ] = @id
|
172
|
+
relation = @Model.extensions[ name.capitalize() ].where filters
|
173
|
+
@getter name, => relation.first()
|
174
|
+
relation.first()
|
175
|
+
@
|
176
|
+
|
177
|
+
hasManyRelation: ( name ) ->
|
178
|
+
@getter name, =>
|
179
|
+
delete @[ name ]
|
180
|
+
( filters = {} )[ "#{ @sysname.lowercase() }_id" ] = @id
|
181
|
+
@[ name ] = @Model.extensions[ name[ ...-1 ].capitalize() ].where filters
|
182
|
+
@
|
183
|
+
|
184
|
+
view: ( name ) ->
|
185
|
+
name = @sysname + name.camelcase().capitalize() unless @View.extensions[ name ]?
|
186
|
+
unless ( view = ( @views ?= {} )[ name ] )?
|
187
|
+
if ( view = @View.extensions[ name ] )?
|
188
|
+
view = ( ( @views ?= {} )[ name ] = view.clone( model: @ ) )
|
189
|
+
else console.error "View %s of model %O does not exist", name, @
|
190
|
+
view
|
191
|
+
|
192
|
+
show: ( name, insertTo ) ->
|
193
|
+
if ( view = @view( name ) )? then view.show insertTo else null
|
194
|
+
|
195
|
+
hide: ( name ) ->
|
196
|
+
if ( view = @view( name ) )? then view.hide() else null
|
197
|
+
|
198
|
+
# валидации
|
199
|
+
|
200
|
+
validations:
|
201
|
+
# набор валидационных проверок
|
202
|
+
presence: ( value, filter ) -> if filter then value? else not value?
|
203
|
+
match: ( value, filter ) -> not value? or filter.test value
|
204
|
+
inclusion: ( value, filter ) -> not value? or value in filter
|
205
|
+
exclusion: ( value, filter ) -> not value? or value not in filter
|
206
|
+
length: ( value, filter ) ->
|
207
|
+
if not value? then return true else value += ''
|
208
|
+
return false if filter.in? and value.length not in filter.in
|
209
|
+
return false if filter.min? and value.length < filter.min
|
210
|
+
return false if filter.max? and value.length > filter.max
|
211
|
+
return false if filter.is? and value.length isnt filter.is
|
212
|
+
true
|
213
|
+
format: ( value, filter ) ->
|
214
|
+
return true if not value?
|
215
|
+
return true if filter is 'boolean' and /^true|false$/.test value
|
216
|
+
return true if filter is 'number' and /^[0-9]+$/.test value
|
217
|
+
return true if filter is 'letters' and /^[A-zА-я]+$/.test value
|
218
|
+
return true if filter is 'email' and /^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,4})+$/.test value
|
219
|
+
false
|
220
|
+
|
221
|
+
isValid: ->
|
222
|
+
# проверяет валидна ли модель, вызывается перед сохранением модели на сервер если модель валидна,
|
223
|
+
# то вызов model.isValid()? вернет true, иначе false
|
224
|
+
return null for name, value of @attributes when not @isValidAttributeValue( name, value )?
|
225
|
+
true
|
226
|
+
|
227
|
+
isValidAttributeValue: ( name, value ) ->
|
228
|
+
# проверяет валидно ли значение для определенного атрибута модели, вызывается при проверке
|
229
|
+
# валидности модели, а также в методе update() перед изменением значения атрибута, если значение
|
230
|
+
# валидно то вызов model.isValidAttributeValue( name, value )? вернет true, иначе false
|
231
|
+
for validation, tester of @validations when ( filter = @::attributes[ name ]?[ validation ] )?
|
232
|
+
unless tester.call @, value, filter
|
233
|
+
console.warn 'Attribute %s of model %O has not validate %s', name, @, validation
|
234
|
+
for type in [ 'info', 'warning', 'error' ] when ( message = @::attributes[ name ][ type ] )?
|
235
|
+
@Notice[ type ] message: message
|
236
|
+
return null
|
237
|
+
true
|