sprockets-jsrender 0.1.1 → 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,5 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
5
+ .idea/*
data/.rvmrc ADDED
@@ -0,0 +1 @@
1
+ rvm 1.9.3@sprockets-jsrender --create
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in sprockets-jsrender.gemspec
4
+ gemspec
data/README ADDED
@@ -0,0 +1,22 @@
1
+ Sprockets jsRender/jsViews
2
+ ==========================
3
+
4
+ This gem adds jsRender/jsViews templates as tilt templates for Sprockets 2 in Rails 3.1.
5
+
6
+ Thanks
7
+ ======
8
+ Inpired by sprockets-jquery-tmpl by Ryan Dy (https://github.com/rdy/sprockets-jquery-tmpl)
9
+ jsrender and jsviews are created bu Boris Moore (https://github.com/BorisMoore/jsrender)
10
+
11
+ Installing
12
+ ==========
13
+
14
+ 1. Add the gem to bundler or install: `gem install sprockets-jsrender`
15
+ 2. Add to your app/assets/javascripts/application.js the following lines
16
+ //= require jsrender
17
+ //= require jquery.observable
18
+ //= require jquery.views
19
+
20
+ Any files in assets/javascripts/jsrender will be automatically added to list of templates using the path from that directory (i.e. assets/javascripts/jsrender/examples/index.jsr will be mapped to $.render("examples/index") )
21
+
22
+ Copyright (c) 2012 Enrico Rubboli, released under the MIT license
@@ -0,0 +1 @@
1
+ require 'bundler/gem_tasks'
@@ -0,0 +1,174 @@
1
+ /*! jsObservable: http://github.com/BorisMoore/jsviews */
2
+ /*
3
+ * Subcomponent of JsViews
4
+ * Data change events for data-linking
5
+ *
6
+ * Copyright 2012, Boris Moore and Brad Olenick
7
+ * Released under the MIT License.
8
+ */
9
+ (function ( $, undefined ) {
10
+ $.observable = function( data, options ) {
11
+ return $.isArray( data )
12
+ ? new ArrayObservable( data )
13
+ : new ObjectObservable( data );
14
+ };
15
+
16
+ var splice = [].splice;
17
+
18
+ function ObjectObservable( data ) {
19
+ if ( !this.data ) {
20
+ return new ObjectObservable( data );
21
+ }
22
+
23
+ this._data = data;
24
+ return this;
25
+ };
26
+
27
+ $.observable.Object = ObjectObservable;
28
+
29
+ ObjectObservable.prototype = {
30
+ _data: null,
31
+
32
+ data: function() {
33
+ return this._data;
34
+ },
35
+
36
+ setProperty: function( path, value ) { // TODO in the case of multiple changes (object): raise single propertyChanges event (which may span different objects, via paths) with set of changes.
37
+ if ( $.isArray( path ) ) {
38
+ // This is the array format generated by serializeArray. However, this has the problem that it coerces types to string,
39
+ // and does not provide simple support of convertTo and convertFrom functions.
40
+ // TODO: We've discussed an "objectchange" event to capture all N property updates here. See TODO note above about propertyChanges.
41
+ for ( var i = 0, l = path.length; i < l; i++ ) {
42
+ var pair = path[i];
43
+ this.setProperty( pair.name, pair.value );
44
+ }
45
+ } else
46
+ if ( typeof( path ) === "object" ) {
47
+ // Object representation where property name is path and property value is value.
48
+ // TODO: We've discussed an "objectchange" event to capture all N property updates here. See TODO note above about propertyChanges.
49
+ for ( var key in path ) {
50
+ this.setProperty( key, path[ key ]);
51
+ }
52
+ } else {
53
+ // Simple single property case.
54
+ var setter, property,
55
+ object = this._data,
56
+ leaf = getLeafObject( object, path );
57
+
58
+ path = leaf[1];
59
+ leaf = leaf[0];
60
+ if ( leaf ) {
61
+ property = leaf[ path ];
62
+ if ( $.isFunction( property )) {
63
+ // Case of property setter/getter - with convention that property() is getter and property( value ) is setter
64
+ setter = property;
65
+ property = property.call( leaf ); //get
66
+ }
67
+ if ( property != value ) { // test for non-strict equality, since serializeArray, and form-based editors can map numbers to strings, etc.
68
+ if ( setter ) {
69
+ setter.call( leaf, value ); //set
70
+ value = setter.call( leaf ); //get updated value
71
+ } else {
72
+ leaf[ path ] = value;
73
+ }
74
+ $( leaf ).triggerHandler( "propertyChange", { path: path, value: value, oldValue: property });
75
+ }
76
+ }
77
+ }
78
+ return this;
79
+ }
80
+ };
81
+
82
+ function getLeafObject( object, path ) {
83
+ if ( object && path ) {
84
+ var parts = path.split(".");
85
+
86
+ path = parts.pop();
87
+ while ( object && parts.length ) {
88
+ object = object[ parts.shift() ];
89
+ }
90
+ return [ object, path ];
91
+ }
92
+ return [];
93
+ };
94
+
95
+ function ArrayObservable( data ) {
96
+ if ( !this.data ) {
97
+ return new ArrayObservable( data );
98
+ }
99
+
100
+ this._data = data;
101
+ return this;
102
+ };
103
+
104
+ function triggerArrayEvent( array, eventArgs ) {
105
+ $([ array ]).triggerHandler( "arrayChange", eventArgs );
106
+ };
107
+
108
+ function validateIndex( index ) {
109
+ if ( typeof index !== "number" ) {
110
+ throw "Invalid index.";
111
+ }
112
+ };
113
+
114
+ $.observable.Array = ArrayObservable;
115
+
116
+ ArrayObservable.prototype = {
117
+ _data: null,
118
+
119
+ data: function() {
120
+ return this._data;
121
+ },
122
+
123
+ insert: function( index, data ) {
124
+ validateIndex( index );
125
+
126
+ if ( arguments.length > 1 ) {
127
+ data = $.isArray( data ) ? data : [ data ]; // TODO: Clone array here?
128
+ // data can be a single item (including a null/undefined value) or an array of items.
129
+
130
+ if ( data.length > 0 ) {
131
+ splice.apply( this._data, [ index, 0 ].concat( data ));
132
+ triggerArrayEvent( this._data, { change: "insert", index: index, items: data });
133
+ }
134
+ }
135
+ return this;
136
+ },
137
+
138
+ remove: function( index, numToRemove ) {
139
+ validateIndex( index );
140
+
141
+ numToRemove = ( numToRemove === undefined || numToRemove === null ) ? 1 : numToRemove;
142
+ if ( numToRemove && index > -1 ) {
143
+ var items = this._data.slice( index, index + numToRemove );
144
+ numToRemove = items.length;
145
+ if ( numToRemove ) {
146
+ this._data.splice( index, numToRemove );
147
+ triggerArrayEvent( this._data, { change: "remove", index: index, items: items });
148
+ }
149
+ }
150
+ return this;
151
+ },
152
+
153
+ move: function( oldIndex, newIndex, numToMove ) {
154
+ validateIndex( oldIndex );
155
+ validateIndex( newIndex );
156
+
157
+ numToMove = ( numToMove === undefined || numToMove === null ) ? 1 : numToMove;
158
+ if ( numToMove ) {
159
+ var items = this._data.slice( oldIndex, oldIndex + numToMove );
160
+ this._data.splice( oldIndex, numToMove );
161
+ this._data.splice.apply( this._data, [ newIndex, 0 ].concat( items ) );
162
+ triggerArrayEvent( this._data, { change: "move", oldIndex: oldIndex, index: newIndex, items: items });
163
+ }
164
+ return this;
165
+ },
166
+
167
+ refresh: function( newItems ) {
168
+ var oldItems = this._data.slice( 0 );
169
+ splice.apply( this._data, [ 0, this._data.length ].concat( newItems ));
170
+ triggerArrayEvent( this._data, { change: "refresh", oldItems: oldItems });
171
+ return this;
172
+ }
173
+ };
174
+ })(jQuery);
@@ -0,0 +1,802 @@
1
+ /*! JsViews v1.0pre: http://github.com/BorisMoore/jsviews */
2
+ /*
3
+ * Interactive data-driven views using templates and data-linking.
4
+ * Requires jQuery, and jsrender.js (next-generation jQuery Templates, optimized for pure string-based rendering)
5
+ * See JsRender at http://github.com/BorisMoore/jsrender
6
+ *
7
+ * Copyright 2012, Boris Moore
8
+ * Released under the MIT License.
9
+ */
10
+ // informal pre beta commit counter: 2
11
+
12
+ this.jQuery && jQuery.link || (function( global, undefined ) {
13
+ // global is the this object, which is window when running in the usual browser environment.
14
+
15
+ //========================== Top-level vars ==========================
16
+
17
+ var versionNumber = "v1.0pre",
18
+
19
+ rTag, delimOpen0, delimOpen1, delimClose0, delimClose1,
20
+ $ = global.jQuery,
21
+
22
+ // jsviews object (=== $.views) Note: JsViews requires jQuery is loaded)
23
+ jsv = $.views,
24
+ sub = jsv.sub,
25
+ FALSE = false, TRUE = true,
26
+ topView = jsv.topView,
27
+ templates = jsv.templates,
28
+ observable = $.observable,
29
+ jsvData = "_jsvData",
30
+ linkStr = "link",
31
+ viewStr = "view",
32
+ propertyChangeStr = "propertyChange",
33
+ arrayChangeStr = "arrayChange",
34
+ fnSetters = {
35
+ value: "val",
36
+ html: "html",
37
+ text: "text"
38
+ },
39
+ oldCleanData = $.cleanData,
40
+ oldJsvDelimiters = jsv.delimiters,
41
+ rTmplOrItemComment = /^(\/?)(?:(item)|(?:(tmpl)(?:\((.*),([^,)]*)\))?(?:\s+([^\s]+))?))$/,
42
+ // tokens: [ all, slash, 'item', 'tmpl', path, index, tmplParam ]
43
+ //rTmplOrItemComment = /^(\/?)(?:(item)|(?:(tmpl)(?:\(([^,R]*),([^,)]*)\))?(?:\s+([^\s]+))?))$/,
44
+
45
+ rStartTag = /^item|^tmpl(\(\$?[\w.,]*\))?(\s+[^\s]+)?$/;
46
+
47
+ if ( !$ ) {
48
+ // jQuery is not loaded.
49
+ throw "requires jQuery"; // for Beta (at least) we require jQuery
50
+ }
51
+
52
+ if( !(jsv )) {
53
+ throw "requires JsRender";
54
+ }
55
+
56
+ //========================== Top-level functions ==========================
57
+
58
+ //===============
59
+ // event handlers
60
+ //===============
61
+
62
+ function elemChangeHandler( ev ) {
63
+ var setter, cancel, fromAttr, to, linkContext, sourceValue, cnvtBack, target,
64
+ source = ev.target,
65
+ $source = $( source ),
66
+ view = $.view( source ),
67
+ context = view.ctx,
68
+ beforeChange = context.beforeChange;
69
+
70
+ if ( source.getAttribute( jsv.linkAttr ) && (to = jsViewsData( source, "to" ))) {
71
+ fromAttr = defaultAttr( source );
72
+ setter = fnSetters[ fromAttr ];
73
+ sourceValue = $.isFunction( fromAttr ) ? fromAttr( source ) : setter ? $source[setter]() : $source.attr( fromAttr );
74
+
75
+ if ((!beforeChange || !(cancel = beforeChange.call( view, ev ) === FALSE )) && sourceValue !== undefined ) {
76
+ cnvtBack = jsv.converters[ to[ 2 ]];
77
+ target = to[ 0 ];
78
+ to = to[ 1 ];
79
+ linkContext = {
80
+ src: source,
81
+ tgt: target,
82
+ cnvtBack: cnvtBack,
83
+ path: to
84
+ };
85
+ if ( cnvtBack ) {
86
+ sourceValue = cnvtBack( sourceValue );
87
+ }
88
+ if ( sourceValue !== undefined && target ) {
89
+ observable( target ).setProperty( to, sourceValue );
90
+ if ( context.afterChange ) { //TODO only call this if the target property changed
91
+ context.afterChange.call( linkContext, ev );
92
+ }
93
+ }
94
+ ev.stopPropagation(); // Stop bubbling
95
+ }
96
+ if ( cancel ) {
97
+ ev.stopImmediatePropagation();
98
+ }
99
+ }
100
+ }
101
+
102
+ function propertyChangeHandler( ev, eventArgs, bind ) {
103
+ var setter, changed, sourceValue, css,
104
+ link = this,
105
+ source = link.src,
106
+ target = link.tgt,
107
+ $target = $( target ),
108
+ attr = link.attr || defaultAttr( target, TRUE ),
109
+ view = link.view,
110
+ context = view.ctx,
111
+ beforeChange = context.beforeChange;
112
+
113
+ // TODO for <input data-link="a.b" />
114
+ //Currently the following scenarios do work:
115
+ //$.observable(model).setProperty("a.b", "bar");
116
+ //$.observable(model.a).setProperty("b", "bar");
117
+ // TODO Add support for $.observable(model).setProperty("a", { b: "bar" });
118
+ // var testsourceValue = ev.expr( source, view, jsv, ev.bind );
119
+
120
+ // TODO call beforeChange on data-link initialization.
121
+ // if ( changed && context.afterChange ) {
122
+ // context.afterChange.call( link, ev, eventArgs );
123
+ // }
124
+
125
+
126
+ if ((!beforeChange || !(eventArgs && beforeChange.call( this, ev, eventArgs ) === FALSE ))
127
+ // && (!view || view.onDataChanged( eventArgs ) !== FALSE ) // Not currently supported or needed for property change
128
+ ) {
129
+ sourceValue = link.fn( source, link.view, jsv, bind || returnVal );
130
+ if ( $.isFunction( sourceValue )) {
131
+ sourceValue = sourceValue.call( source );
132
+ }
133
+ if ( css = attr.lastIndexOf( "css-", 0 ) === 0 && attr.substr( 4 )) {
134
+ if ( changed = $target.css( css ) !== sourceValue ) {
135
+ $target.css( css, sourceValue );
136
+ }
137
+ } else {
138
+ setter = fnSetters[ attr ];
139
+ if ( setter ) {
140
+ if ( changed = $target[setter]() !== sourceValue ) {
141
+ $target[setter]( sourceValue );
142
+ if ( target.nodeName.toLowerCase() === "input" ) {
143
+ $target.blur(); // Issue with IE. This ensures HTML rendering is updated.
144
+ }
145
+ }
146
+ } else if ( changed = $target.attr( attr ) !== sourceValue ) {
147
+ $target.attr( attr, sourceValue );
148
+ }
149
+ }
150
+
151
+ if ( eventArgs && changed && context.afterChange ) {
152
+ context.afterChange.call( link, ev, eventArgs );
153
+ }
154
+ }
155
+ }
156
+
157
+ function arrayChangeHandler( ev, eventArgs ) {
158
+ var context = this.ctx,
159
+ beforeChange = context.beforeChange;
160
+
161
+ if ( !beforeChange || beforeChange.call( this, ev, eventArgs ) !== FALSE ) {
162
+ this.onDataChanged( eventArgs );
163
+ if ( context.afterChange ) {
164
+ context.afterChange.call( this, ev, eventArgs );
165
+ }
166
+ }
167
+ }
168
+
169
+ function setArrayChangeLink( view ) {
170
+ var handler,
171
+ data = view.data,
172
+ onArrayChange = view._onArrayChange;
173
+
174
+ if ( onArrayChange ) {
175
+ if ( onArrayChange[ 1 ] === data ) {
176
+ return;
177
+ }
178
+ $([ onArrayChange[ 1 ]]).unbind( arrayChangeStr, onArrayChange[ 0 ]);
179
+ }
180
+
181
+ if ( $.isArray( data )) {
182
+ handler = function() {
183
+ arrayChangeHandler.apply( view, arguments );
184
+ };
185
+ $([ data ]).bind( arrayChangeStr, handler );
186
+ view._onArrayChange = [ handler, data ];
187
+ }
188
+ }
189
+
190
+ function defaultAttr( elem, to ) {
191
+ // Merge in the default attribute bindings for this target element
192
+ var attr = jsv.merge[ elem.nodeName.toLowerCase() ];
193
+ return attr
194
+ ? (to
195
+ ? attr.to.toAttr
196
+ : attr.from.fromAttr)
197
+ : "html";
198
+ }
199
+
200
+ function returnVal( value ) {
201
+ return value;
202
+ }
203
+
204
+ //===============
205
+ // view hierarchy
206
+ //===============
207
+
208
+ function linkedView( view ) {
209
+ var i, views, viewsCount;
210
+ if ( !view.render ) {
211
+ view.onDataChanged = view_onDataChanged;
212
+ view.render = view_render;
213
+ view.addViews = view_addViews;
214
+ view.removeViews = view_removeViews;
215
+ view.content = view_content;
216
+ if (view.parent) {
217
+ if ( !$.isArray( view.data )) {
218
+ view.nodes = [];
219
+ view._lnk = 0; // compiled link index.
220
+ }
221
+ views = view.parent.views;
222
+ if ( $.isArray( views )) {
223
+ i = view.index;
224
+ viewsCount = views.length;
225
+ while ( i++ < viewsCount-1 ) {
226
+ observable( views[ i ] ).setProperty( "index", i );
227
+ }
228
+ }
229
+ setArrayChangeLink( view );
230
+ }
231
+ }
232
+ return view;
233
+ }
234
+
235
+ // Additional methods on view object for linked views (i.e. when JsViews is loaded)
236
+
237
+ function view_onDataChanged( eventArgs ) {
238
+ if ( eventArgs ) {
239
+ // This is an observable action (not a trigger/handler call from pushValues, or similar, for which eventArgs will be null)
240
+ var self = this,
241
+ action = eventArgs.change,
242
+ index = eventArgs.index,
243
+ items = eventArgs.items;
244
+ switch ( action ) {
245
+ case "insert":
246
+ self.addViews( index, items );
247
+ break;
248
+ case "remove":
249
+ self.removeViews( index, items.length );
250
+ break;
251
+ case "move":
252
+ self.render(); // Could optimize this
253
+ break;
254
+ case "refresh":
255
+ self.render();
256
+ // Othercases: (e.g.undefined, for setProperty on observable object) etc. do nothing
257
+ }
258
+ }
259
+ return TRUE;
260
+ }
261
+
262
+ function view_render() {
263
+ var self = this,
264
+ tmpl = self.tmpl = getTemplate( self.tmpl ),
265
+ prevNode = self.prevNode,
266
+ nextNode = self.nextNode,
267
+ parentNode = prevNode.parentNode;
268
+
269
+ if ( tmpl ) {
270
+ // Remove HTML nodes
271
+ $( self.nodes ).remove(); // Also triggers cleanData which removes child views.
272
+ // Remove child views
273
+ self.removeViews();
274
+ self.nodes = [];
275
+ $( prevNode ).after( tmpl.render( self.data, self.ctx, self, self.path, true ) );
276
+ // Need to the update the annotation info on the prevNode comment marker
277
+ // TODO - Include the following two lines, but modified, to keep <!-- item --> comments, but add template info: <!-- item fooTemplate -->
278
+ // prevNode.nodeValue = prevNode.nextSibling.nodeValue;
279
+ // nextNode.nodeValue = nextNode.previousSibling.nodeValue;
280
+ // Remove the extra comment nodes
281
+ parentNode.removeChild( prevNode.nextSibling );
282
+ parentNode.removeChild( nextNode.previousSibling );
283
+ // Link the new HTML nodes to the data
284
+ linkViews( parentNode, self, nextNode, 0, undefined, undefined, prevNode, 0 ); //this.index
285
+ setArrayChangeLink( self );
286
+ }
287
+ return self;
288
+ }
289
+
290
+ function view_addViews( index, dataItems, tmpl ) {
291
+ var self = this,
292
+ itemsCount = dataItems.length,
293
+ context = self.ctx,
294
+ views = self.views;
295
+
296
+ if ( index && !views[index-1] ) {
297
+ return; // If subview for provided index does not exist, do nothing
298
+ }
299
+ if ( itemsCount && (tmpl = getTemplate( tmpl || self.tmpl ))) {
300
+ var prevNode = index ? views[ index-1 ].nextNode : self.prevNode,
301
+ nextNode = prevNode.nextSibling,
302
+ parentNode = prevNode.parentNode;
303
+
304
+ // Use passed-in template if provided, since self added view may use a different template than the original one used to render the array.
305
+ $( prevNode ).after( tmpl.render( dataItems, context, self, undefined, index ) );
306
+ // Need to the update the annotation info on the prevNode comment marker
307
+ // self.prevNode.nodeValue = prevNode.nextSibling.nodeValue;
308
+ // Remove the extra comment nodes
309
+ parentNode.removeChild( prevNode.nextSibling );
310
+ parentNode.removeChild( nextNode.previousSibling );
311
+ // Link the new HTML nodes to the data
312
+ linkViews( parentNode, self, nextNode, 0, undefined, undefined, prevNode, index );
313
+ }
314
+ return self;
315
+ }
316
+
317
+ function view_removeViews( index, itemsCount ) {
318
+ // view.removeViews() removes all the child views
319
+ // view.removeViews( index ) removes the child view with specified index or key
320
+ // view.removeViews( index, count ) removes the specified nummber of child views, starting with the specified index
321
+ function removeView( index ) {
322
+ var parentElViews, i,
323
+ view = views[ index ],
324
+ node = view.prevNode,
325
+ nextNode = view.nextNode,
326
+ nodes = [ node ];
327
+ if ( !nextNode ) {
328
+ // this view has not been linked, so nothing to remove.
329
+ return;
330
+ }
331
+ parentElViews = parentElViews || jsViewsData( nextNode.parentNode, viewStr );
332
+ i = parentElViews.length;
333
+ if ( i ) {
334
+ view.removeViews();
335
+ }
336
+
337
+ // Remove this view from the parentElViews collection
338
+ while ( i-- ) {
339
+ if ( parentElViews[ i ] === view ) {
340
+ parentElViews.splice( i, 1 );
341
+ break;
342
+ }
343
+ }
344
+ // Remove the HTML nodes from the DOM
345
+ while ( node !== nextNode ) {
346
+ node = node.nextSibling;
347
+ nodes.push( node );
348
+ }
349
+ $( nodes ).remove();
350
+ view.data = undefined;
351
+ setArrayChangeLink( view );
352
+ }
353
+
354
+ var current,
355
+ self = this,
356
+ views = self.views,
357
+ viewsCount = views.length;
358
+
359
+ if ( index === undefined ) {
360
+ // Remove all child views
361
+ if ( viewsCount === undefined ) {
362
+ // views and data are objects
363
+ for ( index in views ) {
364
+ // Remove by key
365
+ removeView( index );
366
+ }
367
+ self.views = {};
368
+ } else {
369
+ // views and data are arrays
370
+ current = viewsCount;
371
+ while ( current-- ) {
372
+ removeView( current );
373
+ }
374
+ self.views = [];
375
+ }
376
+ } else {
377
+ if ( itemsCount === undefined ) {
378
+ if ( viewsCount === undefined ) {
379
+ // Remove child view with key 'index'
380
+ removeView( index );
381
+ delete views[ index ];
382
+ } else {
383
+ // The parentView is data array view.
384
+ // Set itemsCount to 1, to remove this item
385
+ itemsCount = 1;
386
+ }
387
+ }
388
+ if ( itemsCount ) {
389
+ current = index + itemsCount;
390
+ // Remove indexed items (parentView is data array view);
391
+ while ( current-- > index ) {
392
+ removeView( current );
393
+ }
394
+ views.splice( index, itemsCount );
395
+ if ( viewsCount = views.length ) {
396
+ // Fixup index on following view items...
397
+ while ( index < viewsCount ) {
398
+ observable( views[ index ] ).setProperty( "index", index++ );
399
+ }
400
+ }
401
+ }
402
+ }
403
+ return this;
404
+ }
405
+
406
+ function view_content( select ) {
407
+ return select ? $( select, this.nodes ) : $( this.nodes );
408
+ }
409
+
410
+ //===============
411
+ // data-linking
412
+ //===============
413
+
414
+ function linkViews( node, parent, nextNode, depth, data, context, prevNode, index ) {
415
+
416
+ var tokens, links, link, attr, linkIndex, parentElViews, convertBack, cbLength, view, parentNode, linkMarkup, expression,
417
+ currentView = parent,
418
+ viewDepth = depth;
419
+ context = context || {};
420
+ node = prevNode || node;
421
+
422
+ if ( !prevNode && node.nodeType === 1 ) {
423
+ if ( viewDepth++ === 0 ) {
424
+ // Add top-level element nodes to view.nodes
425
+ currentView.nodes.push( node );
426
+ }
427
+ if ( linkMarkup = node.getAttribute( jsv.linkAttr ) ) {
428
+ linkIndex = currentView._lnk++;
429
+ // Compiled linkFn expressions are stored in the tmpl.links array of the template
430
+ links = currentView.links || currentView.tmpl.links;
431
+ if ( !(link = links[ linkIndex ] )) {
432
+ link = links [ linkIndex ] = {};
433
+ if ( linkMarkup.charAt(linkMarkup.length-1) !== "}" ) {
434
+ // Simplified syntax is used: data-link="expression"
435
+ // Convert to data-link="{:expression}", or for inputs, data-link="{:expression:}" for (default) two-way binding
436
+ linkMarkup = delimOpen1 + ":" + linkMarkup + ($.nodeName( node, "input" ) ? ":" : "") + delimClose0;
437
+ }
438
+ while( tokens = rTag.exec( linkMarkup )) { // TODO require } to be followed by whitespace or $, and remove the \}(!\}) option.
439
+ // Iterate over the data-link expressions, for different target attrs, e.g. <input data-link="{:firstName:} title{:~description(firstName, lastName)}"
440
+ // tokens: [all, attr, tag, converter, colon, html, code, linkedParams]
441
+ attr = tokens[ 1 ];
442
+ expression = tokens[ 2 ];
443
+ if ( tokens[ 5 ]) {
444
+ // Only for {:} link"
445
+ if ( !attr && (convertBack = /^.*:([\w$]*)$/.exec( tokens[ 8 ] ))) {
446
+ // two-way binding
447
+ convertBack = convertBack[ 1 ];
448
+ if ( cbLength = convertBack.length ) {
449
+ // There is a convertBack function
450
+ expression = tokens[ 2 ].slice( 0, -cbLength - 1 ) + delimClose0; // Remove the convertBack string from expression.
451
+ }
452
+ }
453
+ if ( convertBack === null ) {
454
+ convertBack = undefined;
455
+ }
456
+ }
457
+ // Compile the linkFn expression which evaluates and binds a data-link expression
458
+ // TODO - optimize for the case of simple data path with no conversion, helpers, etc.:
459
+ // i.e. data-link="a.b.c". Avoid creating new instances of Function every time. Can use a default function for all of these...
460
+ link[ attr ] = jsv.tmplFn( delimOpen0 + expression + delimClose1, undefined, TRUE );
461
+ if ( !attr && convertBack !== undefined ) {
462
+ link[ attr ].to = convertBack;
463
+ }
464
+ }
465
+ }
466
+ for ( attr in link ) {
467
+ bindDataLinkTarget(
468
+ currentView.data|| data, //source
469
+ node, //target
470
+ attr, //attr
471
+ link[ attr ], //compiled link markup expression
472
+ currentView //view
473
+ );
474
+ }
475
+ // TODO - Add one-way-to-source support
476
+ // if ( linkMarkup.lastIndexOf( "toSrc{", 0 ) === 0 ) {
477
+ // linkMarkup = "{toSrc " + linkMarkup.slice(6);
478
+ // }
479
+ }
480
+ node = node.firstChild;
481
+ } else {
482
+ node = node.nextSibling;
483
+ }
484
+
485
+ while ( node && node !== nextNode ) {
486
+ if ( node.nodeType === 1 ) {
487
+ linkViews( node, currentView, nextNode, viewDepth, data, context );
488
+ } else if ( node.nodeType === 8 && (tokens = rTmplOrItemComment.exec( node.nodeValue ))) {
489
+ // tokens: [ all, slash, 'item', 'tmpl', path, index, tmplParam ]
490
+ parentNode = node.parentNode;
491
+ if ( tokens[ 1 ]) {
492
+ // <!--/item--> or <!--/tmpl-->
493
+ currentView.nextNode = node;
494
+ if ( currentView.ctx.onAfterCreate ) {
495
+ currentView.ctx.onAfterCreate.call( currentView, currentView );
496
+ }
497
+ if ( tokens[ 2 ]) {
498
+ // An item close tag: <!--/item-->
499
+ currentView = parent;
500
+ } else {
501
+ // A tmpl close tag: <!--/tmpl-->
502
+ return node;
503
+ }
504
+ } else {
505
+ // <!--item--> or <!--tmpl-->
506
+ parentElViews = parentElViews || jsViewsData( parentNode, viewStr, TRUE );
507
+ if ( tokens[ 2 ]) {
508
+ // An item open tag: <!--item-->
509
+ parentElViews.push(
510
+ currentView = linkedView( currentView.views[ index ] )
511
+ );
512
+ index++;
513
+ currentView.prevNode = node;
514
+ } else {
515
+ // A tmpl open tag: <!--tmpl(path) name-->
516
+ parentElViews.push(
517
+ view = linkedView( currentView.views[ tokens[ 5 ]] )
518
+ );
519
+ view.prevNode = node;
520
+ // Jump to the nextNode of the tmpl view
521
+ node = linkViews( node, view, nextNode, 0, undefined, undefined, undefined, 0 );
522
+ }
523
+ }
524
+ } else if ( viewDepth === 0 ) {
525
+ // Add top-level non-element nodes to view.nodes
526
+ currentView.nodes.push( node );
527
+ }
528
+ node = node.nextSibling;
529
+ }
530
+ }
531
+
532
+ function bindDataLinkTarget( source, target, attr, linkFn, view ) {
533
+ //Add data link bindings for a link expression in data-link attribute markup
534
+ var boundParams = [],
535
+ storedLinks = jsViewsData( target, linkStr, TRUE ),
536
+ handler = function() {
537
+ propertyChangeHandler.apply({ tgt: target, src: source, attr: attr, fn: linkFn, view: view }, arguments );
538
+ };
539
+
540
+ // Store for unbinding
541
+ storedLinks[ attr ] = { srcs: boundParams, hlr: handler };
542
+
543
+ // Call the handler for initialization and parameter binding
544
+ handler( undefined, undefined, function ( object, leafToken ) {
545
+ // Binding callback called on each dependent object (parameter) that the link expression depends on.
546
+ // For each path add a propertyChange binding to the leaf object, to trigger the compiled link expression,
547
+ // and upate the target attribute on the target element
548
+ boundParams.push( object );
549
+ if ( linkFn.to !== undefined ) {
550
+ // If this link is a two-way binding, add the linkTo info to JsViews stored data
551
+ $.data( target, jsvData ).to = [ object, leafToken, linkFn.to ];
552
+ // For two-way binding, there should be only one path. If not, will bind to the last one.
553
+ }
554
+ if ( $.isArray( object )) {
555
+ $([ object ]).bind( arrayChangeStr, function() {
556
+ handler();
557
+ });
558
+ } else {
559
+ $( object ).bind( propertyChangeStr, handler );
560
+ }
561
+ return object;
562
+ });
563
+ // Note that until observable deals with managing listeners on object graphs, we can't support changing objects higher up the chain, so there is no reason
564
+ // to attach listeners to them. Even $.observable( person ).setProperty( "address.city", ... ); is in fact triggering propertyChange on the leaf object (address)
565
+ }
566
+
567
+ //===============
568
+ // helpers
569
+ //===============
570
+
571
+ function jsViewsData( el, type, create ) {
572
+ var jqData = $.data( el, jsvData ) || (create && $.data( el, jsvData, { view: [], link: {} }));
573
+ return jqData ? jqData[ type ] : {};
574
+ }
575
+
576
+ function inputAttrib( elem ) {
577
+ return elem.type === "checkbox" ? elem.checked : $( elem ).val();
578
+ }
579
+
580
+ function getTemplate( tmpl ) {
581
+ // Get nested templates from path
582
+ if ( "" + tmpl === tmpl ) {
583
+ var tokens = tmpl.split("[");
584
+ tmpl = templates[ tokens.shift() ];
585
+ while( tmpl && tokens.length ) {
586
+ tmpl = tmpl.tmpls[ tokens.shift().slice( 0, -1 )];
587
+ }
588
+ }
589
+ return tmpl;
590
+ }
591
+
592
+ //========================== Initialize ==========================
593
+
594
+ //=======================
595
+ // JsRender integration
596
+ //=======================
597
+
598
+ sub.onStoreItem = function( store, name, item, process ) {
599
+
600
+ if ( name && item && store === templates ) {
601
+ item.link = function( container, data, context, parentView ) {
602
+ $.link( container, data, context, parentView, item );
603
+ };
604
+ $.link[ name ] = function() {
605
+ return item.link.apply( item, arguments );
606
+ };
607
+ }
608
+ };
609
+ sub.onRenderItem = function( value, props ) {
610
+ return "<!--item-->" + value + "<!--/item-->";
611
+ };
612
+ sub.onRenderItems = function( value, path, index, tmpl, props ) {
613
+ return "<!--tmpl(" + (path||"") + "," + index + ") " + tmpl.name + "-->" + value + "<!--/tmpl-->";
614
+ };
615
+
616
+ //=======================
617
+ // Extend $.views namespace
618
+ //=======================
619
+
620
+ $.extend( jsv, {
621
+ linkAttr: "data-link",
622
+ merge: {
623
+ input: {
624
+ from: {
625
+ fromAttr: inputAttrib
626
+ },
627
+ to: {
628
+ toAttr: "value"
629
+ }
630
+ }
631
+ },
632
+ delimiters: function( openChars, closeChars ) {
633
+ oldJsvDelimiters( openChars, closeChars );
634
+ rTag = new RegExp( "(?:^|s*)([\\w-]*)(" + jsv.rTag + ")", "g" );
635
+ delimOpen0 = openChars.charAt( 0 );
636
+ delimOpen1 = openChars.charAt( 1 );
637
+ delimClose0 = closeChars.charAt( 0 );
638
+ delimClose1 = closeChars.charAt( 1 );
639
+ return this;
640
+ }
641
+ });
642
+
643
+ //=======================
644
+ // Extend jQuery namespace
645
+ //=======================
646
+
647
+ $.extend({
648
+
649
+ //=======================
650
+ // jQuery $.view() plugin
651
+ //=======================
652
+
653
+ view: function( node, inner ) {
654
+ // $.view() returns top node
655
+ // $.view( node ) returns view that contains node
656
+ // $.view( selector ) returns view that contains first selected element
657
+
658
+ node = ("" + node === node ? $( node )[0] : node);
659
+ var returnView, view, parentElViews, i, finish,
660
+ topNode = global.document.body,
661
+ startNode = node;
662
+
663
+ if ( inner ) {
664
+ // Treat supplied node as a container element, step through content, and return the first view encountered.
665
+ finish = node.nextSibling || node.parentNode;
666
+ while ( finish !== (node = node.firstChild || node.nextSibling || node.parentNode.nextSibling )) {
667
+ if ( node.nodeType === 8 && rStartTag.test( node.nodeValue )) {
668
+ view = $.view( node );
669
+ if ( view.prevNode === node ) {
670
+ return view;
671
+ }
672
+ }
673
+ }
674
+ return;
675
+ }
676
+
677
+ node = node || topNode;
678
+ if ( $.isEmptyObject( topView.views )) {
679
+ returnView = topView; // Perf optimization for common case
680
+ } else {
681
+ // Step up through parents to find an element which is a views container, or if none found, create the top-level view for the page
682
+ while( !(parentElViews = jsViewsData( finish = node.parentNode || topNode, viewStr )).length ) {
683
+ if ( !finish || node === topNode ) {
684
+ jsViewsData( topNode.parentNode, viewStr, TRUE ).push( returnView = topView );
685
+ break;
686
+ }
687
+ node = finish;
688
+ }
689
+ if ( !returnView && node === topNode ) {
690
+ returnView = topView; //parentElViews[0];
691
+ }
692
+ while ( !returnView && node ) {
693
+ // Step back through the nodes, until we find an item or tmpl open tag - in which case that is the view we want
694
+ if ( node === finish ) {
695
+ returnView = view;
696
+ break;
697
+ }
698
+ if ( node.nodeType === 8 ) {
699
+ if ( /^\/item|^\/tmpl$/.test( node.nodeValue )) {
700
+ // A tmpl or item close tag: <!--/tmpl--> or <!--/item-->
701
+ i = parentElViews.length;
702
+ while ( i-- ) {
703
+ view = parentElViews[ i ];
704
+ if ( view.nextNode === node ) {
705
+ // If this was the node originally passed in, this is the view we want.
706
+ returnView = (node === startNode && view);
707
+ // If not, jump to the beginning of this item/tmpl and continue from there
708
+ node = view.prevNode;
709
+ break;
710
+ }
711
+ }
712
+ } else if ( rStartTag.test( node.nodeValue )) {
713
+ // A tmpl or item open tag: <!--tmpl--> or <!--item-->
714
+ i = parentElViews.length;
715
+ while ( i-- ) {
716
+ view = parentElViews[ i ];
717
+ if ( view.prevNode === node ) {
718
+ returnView = view;
719
+ break;
720
+ }
721
+ }
722
+ }
723
+ }
724
+ node = node.previousSibling;
725
+ }
726
+ // If not within any of the views in the current parentElViews collection, move up through parent nodes to find next parentElViews collection
727
+ returnView = returnView || $.view( finish );
728
+ }
729
+ return returnView;
730
+ },
731
+
732
+ link: function( container, data, context, parentView, template ) {
733
+ // Bind elementChange on the root element, for links from elements within the content, to data;
734
+ function dataToElem() {
735
+ elemChangeHandler.apply({
736
+ tgt: data
737
+ }, arguments );
738
+ }
739
+
740
+ parentView = parentView || topView;
741
+ template = template && (templates[ template ] || (template.markup ? template : $.templates( template )));
742
+ context = context || parentView.ctx;
743
+ context.link = TRUE;
744
+ container = $( container )
745
+ .bind( "change", dataToElem );
746
+
747
+ if ( template ) {
748
+ // TODO/BUG Currently this will re-render if called a second time, and will leave stale views under the parentView.views.
749
+ // So TODO: make it smart about when to render and when to link on already rendered content
750
+ container.empty().append( template.render( data, context, parentView )); // Supply non-jQuery version of this...
751
+ // Using append, rather than html, as workaround for issues in IE compat mode. (Using innerHTML leads to initial comments being stripped)
752
+ }
753
+ linkViews( container[0], parentView, undefined, undefined, data, context );
754
+ },
755
+
756
+ //=======================
757
+ // override $.cleanData
758
+ //=======================
759
+ cleanData: function( elems ) {
760
+ var l, el, link, attr, parentView, view, srcs, linksAndViews, collData,
761
+ i = elems.length;
762
+ while ( i-- ) {
763
+ el = elems[ i ];
764
+ if ( linksAndViews = $.data( el, jsvData )) {
765
+
766
+ // Get links and unbind propertyChange
767
+ collData = linksAndViews.link;
768
+ for ( attr in collData) {
769
+ link = collData[ attr ];
770
+ srcs = link.srcs;
771
+ l = srcs.length;
772
+ while( l-- ) {
773
+ $( srcs[ l ] ).unbind( propertyChangeStr, link.hlr );
774
+ }
775
+ }
776
+
777
+ // Get views and remove from parent view
778
+ collData = linksAndViews.view;
779
+ if ( l = collData.length ) {
780
+ parentView = $.view( el );
781
+ while( l-- ) {
782
+ view = collData[ l ];
783
+ if ( view.parent === parentView ) {
784
+ parentView.removeViews( view.index ); // NO - ONLY remove view if its top-level nodes are all.. (TODO)
785
+ }
786
+ }
787
+ }
788
+ }
789
+ }
790
+ oldCleanData.call( $, elems );
791
+ }
792
+ });
793
+
794
+ // Initialize default delimiters
795
+ jsv.delimiters( "{{", "}}" );
796
+
797
+ topView._lnk = 0;
798
+ topView.links = [];
799
+ topView.ctx.link = TRUE; // Set this as the default, when JsViews is loaded
800
+ linkedView(topView);
801
+
802
+ })( this );