nali 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in nali.gemspec
4
+ gemspec
@@ -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.
@@ -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
@@ -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
+ #= require ./jbone.min
2
+ #= require ./extensions
3
+ #= require ./nali
4
+ #= require ./application
5
+ #= require ./connection
6
+ #= require ./router
7
+ #= require ./view
8
+ #= require ./model
9
+ #= require ./collection
10
+ #= require_tree .
@@ -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