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 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