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
@@ -0,0 +1,127 @@
|
|
1
|
+
window.Nali =
|
2
|
+
|
3
|
+
sysname: 'Nali'
|
4
|
+
extensions: {}
|
5
|
+
|
6
|
+
starting: ->
|
7
|
+
for name, extension of @extensions
|
8
|
+
extension.runExtensions()
|
9
|
+
extension.initialize() if extension.hasOwnProperty 'initialize'
|
10
|
+
@starting.call extension
|
11
|
+
@
|
12
|
+
|
13
|
+
extend: ( obj ) ->
|
14
|
+
sysname = Object.keys( obj )[0]
|
15
|
+
@[ sysname ] = @extensions[ sysname ] = obj[ sysname ]
|
16
|
+
@[ sysname ].extensions = {}
|
17
|
+
@[ sysname ].sysname = sysname
|
18
|
+
@[ sysname ].__proto__ = @
|
19
|
+
@[ sysname ] :: = @
|
20
|
+
@[ sysname ].initObservation()
|
21
|
+
@[ sysname ]
|
22
|
+
|
23
|
+
clone: ( obj = {} ) ->
|
24
|
+
obj.__proto__ = @
|
25
|
+
obj :: = @
|
26
|
+
obj.initObservation()
|
27
|
+
obj.cloning?()
|
28
|
+
obj
|
29
|
+
|
30
|
+
expand: ( obj ) ->
|
31
|
+
if obj instanceof Object then @[ key ] = value for own key, value of obj
|
32
|
+
else console.error "Expand of %O error - argument is not Object", @
|
33
|
+
@
|
34
|
+
|
35
|
+
copy: ( obj ) ->
|
36
|
+
copy = {}
|
37
|
+
copy[ property ] = value for own property, value of obj
|
38
|
+
copy
|
39
|
+
|
40
|
+
childOf: ( parent ) ->
|
41
|
+
if parent instanceof Object
|
42
|
+
return false unless @::?
|
43
|
+
return true if @:: is parent
|
44
|
+
else
|
45
|
+
return false unless @::?.sysname?
|
46
|
+
return true if @::sysname is parent
|
47
|
+
@::childOf parent
|
48
|
+
|
49
|
+
runExtensions: ( context = @ ) ->
|
50
|
+
@::?.runExtensions context
|
51
|
+
@extension.call context if @hasOwnProperty 'extension'
|
52
|
+
|
53
|
+
getter: ( property, callback ) ->
|
54
|
+
@__defineGetter__ property, callback
|
55
|
+
@
|
56
|
+
|
57
|
+
setter: ( property, callback ) ->
|
58
|
+
@__defineSetter__ property, callback
|
59
|
+
@
|
60
|
+
|
61
|
+
access: ( obj, getter, setter ) ->
|
62
|
+
for own property of obj
|
63
|
+
do( property ) =>
|
64
|
+
@getter property, -> obj[ property ]
|
65
|
+
@setter property, ( value ) -> obj[ property ] = value
|
66
|
+
@
|
67
|
+
|
68
|
+
initObservation: ->
|
69
|
+
@observers = [] unless @hasOwnProperty 'observers'
|
70
|
+
@observables = [] unless @hasOwnProperty 'observables'
|
71
|
+
@
|
72
|
+
|
73
|
+
destroyObservation: ->
|
74
|
+
@unsubscribeAll()
|
75
|
+
@unsubscribeFromAll()
|
76
|
+
@
|
77
|
+
|
78
|
+
addObservationItem: ( to, obj, event, callback ) ->
|
79
|
+
return @ for item in @[ to ] when item[0] is obj and item[1] is event and item[2] in [ undefined, callback ]
|
80
|
+
@[ to ].push if callback then [ obj, event, callback ] else [ obj, event ]
|
81
|
+
@
|
82
|
+
|
83
|
+
removeObservationItem: ( from, obj, event ) ->
|
84
|
+
for item in @[ from ][ 0.. ] when item[0] is obj and ( item[1] is event or not event )
|
85
|
+
@[ from ].splice @[ from ].indexOf( item ), 1
|
86
|
+
@
|
87
|
+
|
88
|
+
subscribe: ( observer, event, callback ) ->
|
89
|
+
@addObservationItem 'observers', observer, event, callback
|
90
|
+
observer.addObservationItem 'observables', @, event, callback
|
91
|
+
@
|
92
|
+
|
93
|
+
subscribeTo: ( observable, event, callback ) ->
|
94
|
+
observable.subscribe @, event, callback
|
95
|
+
@
|
96
|
+
|
97
|
+
subscribeOne: ( observer, event, callback ) ->
|
98
|
+
callbackOne = ( args... ) =>
|
99
|
+
callback.call observer, args...
|
100
|
+
@unsubscribe observer, event
|
101
|
+
@subscribe observer, event, callbackOne
|
102
|
+
@
|
103
|
+
|
104
|
+
subscribeOneTo: ( observable, event, callback ) ->
|
105
|
+
observable.subscribeOne @, event, callback
|
106
|
+
@
|
107
|
+
|
108
|
+
unsubscribe: ( observer, event ) ->
|
109
|
+
@removeObservationItem 'observers', observer, event
|
110
|
+
observer.removeObservationItem 'observables', @, event
|
111
|
+
@
|
112
|
+
|
113
|
+
unsubscribeTo: ( observable, event ) ->
|
114
|
+
observable.unsubscribe @, event
|
115
|
+
@
|
116
|
+
|
117
|
+
unsubscribeAll: ( event ) ->
|
118
|
+
@unsubscribe item[0], event for item in @observers[ 0.. ]
|
119
|
+
@
|
120
|
+
|
121
|
+
unsubscribeFromAll: ( event ) ->
|
122
|
+
@unsubscribeTo item[0], event for item in @observables[ 0.. ]
|
123
|
+
@
|
124
|
+
|
125
|
+
trigger: ( event, args... ) ->
|
126
|
+
item[2].call item[0], args... for item in @observers[ 0.. ] when item[1] is event
|
127
|
+
@
|
@@ -0,0 +1,14 @@
|
|
1
|
+
Nali.Model.extend Notice:
|
2
|
+
initialize: -> @::::Notice = @
|
3
|
+
info: ( params ) -> @build( params ).show 'info'
|
4
|
+
warning: ( params ) -> @build( params ).show 'warning'
|
5
|
+
error: ( params ) -> @build( params ).show 'error'
|
6
|
+
|
7
|
+
Nali.View.extend NoticeInfo:
|
8
|
+
onShow: -> @hide 3000
|
9
|
+
|
10
|
+
Nali.View.extend NoticeWarning:
|
11
|
+
onShow: -> @hide 3000
|
12
|
+
|
13
|
+
Nali.View.extend NoticeError:
|
14
|
+
onShow: -> @hide 3000
|
@@ -0,0 +1,74 @@
|
|
1
|
+
Nali.extend Router:
|
2
|
+
|
3
|
+
initialize: ->
|
4
|
+
@subscribeTo @Connection, 'open', @start
|
5
|
+
@::redirect = ( args... ) => @go args...
|
6
|
+
@
|
7
|
+
|
8
|
+
routes: {}
|
9
|
+
|
10
|
+
start: ->
|
11
|
+
@scanRoutes()
|
12
|
+
@_( window ).on 'popstate', ( event ) =>
|
13
|
+
event.preventDefault()
|
14
|
+
event.stopPropagation()
|
15
|
+
@saveHistory false
|
16
|
+
@go event.target.location.pathname
|
17
|
+
@go()
|
18
|
+
@
|
19
|
+
|
20
|
+
scanRoutes: ->
|
21
|
+
for name, controller of @Controller.extensions when controller.actions?
|
22
|
+
route = '^'
|
23
|
+
route += name.lowercase().replace /s$/, ''
|
24
|
+
route += '('
|
25
|
+
route += Object.keys( controller.routedActions ).join '|'
|
26
|
+
route += ')?'
|
27
|
+
@routes[ route ] = controller
|
28
|
+
@
|
29
|
+
|
30
|
+
go: ( url = window.location.pathname, options = {} ) ->
|
31
|
+
url = @prepare( url ) or @prepare( @Application.defaultUrl )
|
32
|
+
if found = @findRoute url
|
33
|
+
{ controller, action, filters, params } = found
|
34
|
+
params[ name ] = value for name, value in options
|
35
|
+
controller.runAction action, filters, params
|
36
|
+
else if @Application.notFoundUrl
|
37
|
+
@go @Application.notFoundUrl
|
38
|
+
else console.warn "Not exists route to the address %s", url
|
39
|
+
@
|
40
|
+
|
41
|
+
prepare: ( url ) ->
|
42
|
+
url = url.replace "http://#{ window.location.host }", ''
|
43
|
+
url = url[ 1.. ] or '' if url and url[ 0...1 ] is '/'
|
44
|
+
url = url[ ...-1 ] or '' if url and url[ -1.. ] is '/'
|
45
|
+
url
|
46
|
+
|
47
|
+
findRoute: ( url ) ->
|
48
|
+
for route, controller of @routes when match = url.match new RegExp route, 'i'
|
49
|
+
segments = url.split( '/' )[ 1... ]
|
50
|
+
if segments[0] in Object.keys( controller.routedActions )
|
51
|
+
action = segments.shift()
|
52
|
+
else unless action = controller.actions.default
|
53
|
+
console.error 'Unspecified controller action'
|
54
|
+
filters = {}
|
55
|
+
for name in controller.routedActions[ action ].filters when segments[0]?
|
56
|
+
filters[ name ] = segments.shift()
|
57
|
+
params = {}
|
58
|
+
for name in controller.routedActions[ action ].params when segments[0]?
|
59
|
+
params[ name ] = segments.shift()
|
60
|
+
return controller: controller, action: action, filters: filters, params: params
|
61
|
+
false
|
62
|
+
|
63
|
+
saveHistory: ( value ) ->
|
64
|
+
@saveHistorySwitcher ?= true
|
65
|
+
if value in [ true, false ]
|
66
|
+
@saveHistorySwitcher = value
|
67
|
+
@
|
68
|
+
else @saveHistorySwitcher
|
69
|
+
|
70
|
+
setUrl: ( url ) ->
|
71
|
+
if @saveHistory()
|
72
|
+
history.pushState null, null, '/' + ( @url = url ) if url isnt @url
|
73
|
+
else @saveHistory true
|
74
|
+
@
|
@@ -0,0 +1,286 @@
|
|
1
|
+
Nali.extend View:
|
2
|
+
|
3
|
+
extension: ->
|
4
|
+
if @sysname isnt 'View'
|
5
|
+
@parseTemplate()
|
6
|
+
@parseEvents()
|
7
|
+
@
|
8
|
+
|
9
|
+
cloning: ->
|
10
|
+
@my = @model
|
11
|
+
@
|
12
|
+
|
13
|
+
layout: -> null
|
14
|
+
|
15
|
+
onSourceUpdated: -> @draw()
|
16
|
+
|
17
|
+
onSourceDestroyed: -> @hide()
|
18
|
+
|
19
|
+
getOf: ( source, property ) ->
|
20
|
+
@subscribeTo source, "update.#{ property }", @onSourceUpdated
|
21
|
+
source[ property ]
|
22
|
+
|
23
|
+
insertTo: ->
|
24
|
+
if ( layout = @layout() )?.childOf? 'View' then layout.show().element.find '.yield'
|
25
|
+
else @Application.htmlContainer
|
26
|
+
|
27
|
+
draw: ->
|
28
|
+
assistant.call @ for assistant in @assistants
|
29
|
+
@onDraw?()
|
30
|
+
@
|
31
|
+
|
32
|
+
show: ( insertTo = @insertTo() ) ->
|
33
|
+
@prepareElement().draw().bindEvents()
|
34
|
+
unless @visible
|
35
|
+
@model.beforeShow?[ @sysname ]?.call @model
|
36
|
+
@subscribeTo @model, 'update', @onSourceUpdated
|
37
|
+
@subscribeTo @model, 'destroy', @onSourceDestroyed
|
38
|
+
@element.appendTo insertTo
|
39
|
+
@showRelations()
|
40
|
+
setTimeout ( => @onShow() ), 5 if @onShow?
|
41
|
+
@visible = true
|
42
|
+
@model.afterShow?[ @sysname ]?.call @model
|
43
|
+
@
|
44
|
+
|
45
|
+
hide: ( delay = 0 ) ->
|
46
|
+
if @visible
|
47
|
+
@model.beforeHide?[ @sysname ]?.call @model
|
48
|
+
@hideDelay = delay if typeof( delay ) is 'number' and delay
|
49
|
+
@onHide?()
|
50
|
+
@trigger 'hide'
|
51
|
+
@hideElement()
|
52
|
+
@destroyObservation()
|
53
|
+
@visible = false
|
54
|
+
@model.afterHide?[ @sysname ]?.call @model
|
55
|
+
@
|
56
|
+
|
57
|
+
hideElement: ->
|
58
|
+
if @hideDelay? then setTimeout ( => @removeElement() ), @hideDelay else @removeElement()
|
59
|
+
@
|
60
|
+
|
61
|
+
removeElement: ->
|
62
|
+
@element[0].parentNode.removeChild @element[0]
|
63
|
+
@
|
64
|
+
|
65
|
+
showRelations: ->
|
66
|
+
for { selector, name, view } in @relationsMap
|
67
|
+
if ( relation = @model[ name ] )?
|
68
|
+
insertTo = @element.find selector
|
69
|
+
if relation.childOf 'Collection'
|
70
|
+
relation.show view, insertTo, true
|
71
|
+
relation.subscribeTo @, 'hide', relation.reset
|
72
|
+
else
|
73
|
+
view = relation.show view, insertTo
|
74
|
+
view.subscribeTo @, 'hide', view.hide
|
75
|
+
else console.warn "Relation %s does not exist of model %O", name, @model
|
76
|
+
@
|
77
|
+
|
78
|
+
runLink: ( event ) ->
|
79
|
+
event.preventDefault()
|
80
|
+
@runUrl event.currentTarget.getAttribute 'href'
|
81
|
+
@
|
82
|
+
|
83
|
+
runForm: ( event ) ->
|
84
|
+
event.preventDefault()
|
85
|
+
@runUrl event.currentTarget.getAttribute( 'action' ), @formToHash event.currentTarget
|
86
|
+
@
|
87
|
+
|
88
|
+
runUrl: ( url, params = {} ) ->
|
89
|
+
if match = url.match /^(@@?)(.+)/
|
90
|
+
[ method, data ] = match[2].split '?'
|
91
|
+
if data
|
92
|
+
for specification in data.split /&|&/ when specification
|
93
|
+
[ name, value ] = specification.split '='
|
94
|
+
params[ name ] = value
|
95
|
+
obj = if match[1].length is 1 then @ else @model
|
96
|
+
if obj[ method ]? and typeof obj[ method ] is 'function' then obj[ method ] params
|
97
|
+
else console.warn "Method %s not exists", method
|
98
|
+
else @Router.go url, params
|
99
|
+
@
|
100
|
+
|
101
|
+
formToHash: ( form ) ->
|
102
|
+
params = {}
|
103
|
+
for element in form.elements
|
104
|
+
if name = element.name or element.id
|
105
|
+
property = ( keys = name.match /[^\[\]]+/g ).pop()
|
106
|
+
target = params
|
107
|
+
for key in keys
|
108
|
+
target = if target[ key ] instanceof Object then target[ key ] else target[ key ] = {}
|
109
|
+
target[ property ] = element.value
|
110
|
+
params
|
111
|
+
|
112
|
+
parseEvents: ->
|
113
|
+
@eventsMap = []
|
114
|
+
if @events
|
115
|
+
@events = [ @events ] if typeof @events is 'string'
|
116
|
+
for event in @events
|
117
|
+
try
|
118
|
+
[ handlers, type, other ] = event.split /\s+(on|one)\s+/
|
119
|
+
[ events, selector ] = other.split /\s+at\s+/
|
120
|
+
handlers = handlers.split /\s*,\s*/
|
121
|
+
events = events.replace /\s*,\s*/, ' '
|
122
|
+
throw true unless type and events.length and handlers.length
|
123
|
+
catch
|
124
|
+
console.warn "Events parsing error: \"%s\" of %O", event, @
|
125
|
+
error = true
|
126
|
+
if error then error = false else @eventsMap.push [ selector, type, events, handlers ]
|
127
|
+
@
|
128
|
+
|
129
|
+
bindEvents: ->
|
130
|
+
unless @binded?
|
131
|
+
@element.find( 'a' ).on 'click', ( event ) => @runLink event
|
132
|
+
@element.find( 'form' ).on 'submit', ( event ) => @runForm event
|
133
|
+
@element.on 'click', ( event ) => @runLink event if @element.is 'a'
|
134
|
+
@element.on 'submit', ( event ) => @runForm event if @element.is 'form'
|
135
|
+
for [ selector, type, events, handlers ] in @eventsMap
|
136
|
+
for handler in handlers
|
137
|
+
do ( selector, type, events, handler ) =>
|
138
|
+
@element[ type ] events, selector, ( event ) => @[ handler ] event
|
139
|
+
@binded = true
|
140
|
+
@
|
141
|
+
|
142
|
+
prepareElement: ->
|
143
|
+
unless @element
|
144
|
+
@element = @_ @template
|
145
|
+
@element[0].view = @
|
146
|
+
@addAssistants()
|
147
|
+
@
|
148
|
+
|
149
|
+
getNode: ( path ) ->
|
150
|
+
node = @element[0]
|
151
|
+
node = node[ sub ] for sub in path
|
152
|
+
node
|
153
|
+
|
154
|
+
parseTemplate: ->
|
155
|
+
if container = document.querySelector '#' + @sysname.underscore()
|
156
|
+
@template = container.innerHTML.trim().replace( /\s+/g, ' ' )
|
157
|
+
.replace( /({\s*\+.+?\s*})/g, ' <assist>$1</assist>' )
|
158
|
+
.replace( /{\s*yield\s*}/g, '<div class="yield"></div>' )
|
159
|
+
unless RegExp( "^<[^>]+" + @sysname ).test @template
|
160
|
+
@template = "<div class=\"#{ @sysname }\">#{ @template }</div>"
|
161
|
+
@parseRelations()
|
162
|
+
container.parentNode.removeChild container
|
163
|
+
else console.warn 'Template %s not exists', @sysname
|
164
|
+
@
|
165
|
+
|
166
|
+
parseRelations: ->
|
167
|
+
@relationsMap = []
|
168
|
+
@template = @template.replace /{\s*(\w+) of @(\w+)\s*}/g, ( match, view, relation ) =>
|
169
|
+
className = relation.capitalize() + view.capitalize() + 'Relation'
|
170
|
+
@relationsMap.push selector: '.' + className, name: relation, view: view
|
171
|
+
"<div class=\"#{ className }\"></div>"
|
172
|
+
@parseAssistants()
|
173
|
+
@
|
174
|
+
|
175
|
+
parseAssistants: ->
|
176
|
+
@assistantsMap = []
|
177
|
+
if /{\s*.+?\s*}|bind=".+?"/.test @template
|
178
|
+
tmp = document.createElement 'div'
|
179
|
+
tmp.innerHTML = @template
|
180
|
+
@scanAssistants tmp.children[0]
|
181
|
+
@
|
182
|
+
|
183
|
+
scanAssistants: ( node, path = [] ) ->
|
184
|
+
if node.nodeType is 3 and /{\s*.+?\s*}/.test node.textContent
|
185
|
+
@assistantsMap.push nodepath: path, type: 'Text'
|
186
|
+
else if node.nodeName is 'ASSIST'
|
187
|
+
@assistantsMap.push nodepath: path, type: 'Html'
|
188
|
+
else
|
189
|
+
if node.attributes
|
190
|
+
for attribute, index in node.attributes
|
191
|
+
if attribute.name is 'bind'
|
192
|
+
@assistantsMap.push nodepath: path, type: 'Form'
|
193
|
+
else if /{\s*.+?\s*}/.test attribute.textContent
|
194
|
+
@assistantsMap.push nodepath: path.concat( 'attributes', index ), type: 'Text'
|
195
|
+
@scanAssistants child, path.concat 'childNodes', index for child, index in node.childNodes
|
196
|
+
@
|
197
|
+
|
198
|
+
addAssistants: ->
|
199
|
+
@assistants = []
|
200
|
+
@[ "add#{ type }Assistant" ] @getNode nodepath for { nodepath, type } in @assistantsMap
|
201
|
+
@
|
202
|
+
|
203
|
+
addTextAssistant: ( node ) ->
|
204
|
+
initialValue = node.textContent
|
205
|
+
@assistants.push -> node.textContent = @analize initialValue
|
206
|
+
@
|
207
|
+
|
208
|
+
addHtmlAssistant: ( node ) ->
|
209
|
+
parent = node.parentNode
|
210
|
+
initialValue = node.innerHTML
|
211
|
+
index = Array::indexOf.call parent.childNodes, node
|
212
|
+
after = parent.childNodes[ index - 1 ] or null
|
213
|
+
before = parent.childNodes[ index + 1 ] or null
|
214
|
+
@assistants.push ->
|
215
|
+
start = if after then Array::indexOf.call( parent.childNodes, after ) + 1 else 0
|
216
|
+
end = if before then Array::indexOf.call parent.childNodes, before else parent.childNodes.length
|
217
|
+
parent.removeChild node for node in Array::slice.call( parent.childNodes, start, end )
|
218
|
+
parent.insertBefore element, before for element in @_( @analize initialValue )
|
219
|
+
@
|
220
|
+
|
221
|
+
addFormAssistant: ( node ) ->
|
222
|
+
if bind = @analizeChain node.attributes.removeNamedItem( 'bind' ).value
|
223
|
+
[ source, property ] = bind
|
224
|
+
if node.type in [ 'text', 'textarea']
|
225
|
+
node.value = source[ property ]
|
226
|
+
|
227
|
+
@_( node ).on 'change', =>
|
228
|
+
( params = {} )[ property ] = node.value
|
229
|
+
source.update params
|
230
|
+
source.save() unless node.form?
|
231
|
+
|
232
|
+
source.subscribe @, "update.#{ property }", =>
|
233
|
+
node.value = source[ property ] if node.value isnt source[ property ]
|
234
|
+
|
235
|
+
if node.type in [ 'checkbox', 'radio' ]
|
236
|
+
node.checked = source[ property ] + '' is node.value
|
237
|
+
|
238
|
+
@_( node ).on 'change', =>
|
239
|
+
if node.checked is true
|
240
|
+
( params = {} )[ property ] = node.value
|
241
|
+
source.update params
|
242
|
+
source.save() unless node.form?
|
243
|
+
|
244
|
+
source.subscribe @, "update.#{ property }", =>
|
245
|
+
node.checked = source[ property ] + '' is node.value
|
246
|
+
|
247
|
+
if node.type is 'select-one'
|
248
|
+
option.selected = true for option in node when source[ property ] + '' is option.value
|
249
|
+
|
250
|
+
@_( node ).on 'change', =>
|
251
|
+
( params = {} )[ property ] = node.value
|
252
|
+
source.update params
|
253
|
+
source.save() unless node.form?
|
254
|
+
|
255
|
+
source.subscribe @, "update.#{ property }", =>
|
256
|
+
option.selected = true for option in node when source[ property ] + '' is option.value
|
257
|
+
|
258
|
+
|
259
|
+
|
260
|
+
analize: ( value ) ->
|
261
|
+
value.replace /{\s*(.+?)\s*}/g, ( match, sub ) => @analizeMatch sub
|
262
|
+
|
263
|
+
analizeMatch: ( sub ) ->
|
264
|
+
if match = sub.match /^@([\w\.]+)(\?)?$/
|
265
|
+
if result = @analizeChain match[1]
|
266
|
+
[ source, property ] = result
|
267
|
+
source.subscribe? @, "update.#{ property }", @onSourceUpdated if source isnt @model
|
268
|
+
if match[2] is '?'
|
269
|
+
if source[ property ] then property else ''
|
270
|
+
else source[ property ]
|
271
|
+
else ''
|
272
|
+
else if match = sub.match /^[=|\+](\w+)$/
|
273
|
+
@helpers?[ match[1] ]?.call @
|
274
|
+
else undefined
|
275
|
+
|
276
|
+
analizeChain: ( chain ) ->
|
277
|
+
segments = chain.split '.'
|
278
|
+
property = segments.pop()
|
279
|
+
source = @model
|
280
|
+
for segment in segments
|
281
|
+
if segment of source then source = source[ segment ]
|
282
|
+
else break
|
283
|
+
unless property of source
|
284
|
+
console.warn "%s: chain \"%s\" is invalid, \"%s\" is not Object", @sysname, chain, segment
|
285
|
+
return null
|
286
|
+
[ source, property ]
|