sprockets-jsrender 0.1.1 → 0.1.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.
@@ -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 );