nali 0.0.2
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.
- 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
|