forked from BorisMoore/jsviews
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathjquery.views.js
2139 lines (1952 loc) · 79.5 KB
/
jquery.views.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/*! JsViews v1.0pre: http://github.com/BorisMoore/jsviews */
/*
* Interactive data-driven views using templates and data-linking.
* Requires jQuery, and jsrender.js (next-generation jQuery Templates, optimized for pure string-based rendering)
* See JsRender at http://github.com/BorisMoore/jsrender
*
* Copyright 2013, Boris Moore
* Released under the MIT License.
*/
// informal pre beta commit counter: 38 (Beta Candidate)
(function(global, $, undefined) {
// global is the this object, which is window when running in the usual browser environment.
// $ is the global var jQuery
"use strict";
if (!$) {
// jQuery is not loaded.
throw "requires jQuery"; // for Beta (at least) we require jQuery
}
if (!$.views) {
// JsRender is not loaded.
throw "requires JsRender"; // JsRender must be loaded before JsViews
}
if (!$.observable) {
// JsRender is not loaded.
throw "requires jquery.observable"; // jquery.observable.js must be loaded before JsViews
}
if ($.link) { return; } // JsViews is already loaded
//========================== Top-level vars ==========================
var versionNumber = "v1.0pre",
LinkedView, activeBody, $view, rTag, delimOpenChar0, delimOpenChar1, delimCloseChar0, delimCloseChar1, linkChar, validate,
document = global.document,
$views = $.views,
$viewsSub = $views.sub,
$viewsSettings = $views.settings,
$extend = $viewsSub.extend,
topView = $viewsSub.View(undefined, "top"), // Top-level view
$isFunction = $.isFunction,
$templates = $views.templates,
$observable = $.observable,
$observe = $observable.observe,
jsvAttrStr = "data-jsv",
$viewsLinkAttr = $viewsSettings.linkAttr || "data-link", // Allows override on settings prior to loading jquery.views.js
// These two settings can be overridden on settings after loading jsRender, and prior to loading jquery.observable.js and/or JsViews
propertyChangeStr = $viewsSettings.propChng = $viewsSettings.propChng || "propertyChange",
arrayChangeStr = $viewsSub.arrChng = $viewsSub.arrChng || "arrayChange",
cbBindingsStore = $viewsSub._cbBnds = $viewsSub._cbBnds || {},
elementChangeStr = "change.jsv",
onBeforeChangeStr = "onBeforeChange",
onAfterChangeStr = "onAfterChange",
onAfterCreateStr = "onAfterCreate",
closeScript = '"></script>',
openScript = '<script type="jsv',
bindElsSel = "script,[" + jsvAttrStr + "]",
linkViewsSel = bindElsSel + ",[" + $viewsLinkAttr + "]",
fnSetters = {
value: "val",
input: "val",
html: "html",
text: "text"
},
valueBinding = { from: { fromAttr: "value" }, to: { toAttr: "value"} },
oldCleanData = $.cleanData,
oldJsvDelimiters = $viewsSettings.delimiters,
error = $viewsSub.error,
syntaxError = $viewsSub.syntaxError,
// rFirstElem = /<(?!script)(\w+)([>]*\s+on\w+\s*=)?[>\s]/, // This was without the DomLevel0 test.
rFirstElem = /<(?!script)(\w+)(?:[^>]*(on\w+)\s*=)?[^>]*>/,
safeFragment = document.createDocumentFragment(),
qsa = document.querySelector,
// elContent maps tagNames which have only element content, so may not support script nodes.
elContent = { ol: 1, ul: 1, table: 1, tbody: 1, thead: 1, tfoot: 1, tr: 1, colgroup: 1, dl: 1, select: 1, optgroup: 1 },
badParent = {tr: "table"},
// wrapMap provide appropriate wrappers for inserting innerHTML, used in insertBefore
wrapMap = $viewsSettings.wrapMap = {
option: [ 1, "<select multiple='multiple'>", "</select>" ],
legend: [ 1, "<fieldset>", "</fieldset>" ],
thead: [ 1, "<table>", "</table>" ],
tr: [ 2, "<table><tbody>", "</tbody></table>" ],
td: [ 3, "<table><tbody><tr>", "</tr></tbody></table>" ],
col: [ 2, "<table><tbody></tbody><colgroup>", "</colgroup></table>" ],
area: [ 1, "<map>", "</map>" ],
svg: [1, "<svg>", "</svg>"],
div: [1, "x<div>", "</div>"] // Needed in IE7 to serialize link tags correctly, insert comments correctly, etc.
},
voidElems = {br: 1, img: 1, input: 1, hr: 1, area: 1, base: 1, col: 1, link: 1, meta: 1,
command: 1, embed: 1, keygen: 1, param: 1, source: 1, track: 1, wbr: 1},
displayStyles = {},
viewStore = { 0: topView },
bindingStore = {},
bindingKey = 1,
rViewPath = /^#(view\.?)?/,
rConvertMarkers = /(^|(\/>)|(<\/\w+>)|>|)(\s*)([#\/]\d+[_^])`(\s*)(<\w+(?=[\s\/>]))?|\s*(?:(<\w+(?=[\s\/>]))|(<\/\w+>)(\s*)|(\/>)\s*)/g,
rOpenViewMarkers = /(#)()(\d+)(_)/g,
rOpenMarkers = /(#)()(\d+)([_^])/g,
rViewMarkers = /(?:(#)|(\/))(\d+)(_)/g,
rOpenTagMarkers = /(#)()(\d+)(\^)/g,
rMarkerTokens = /(?:(#)|(\/))(\d+)([_^])([-+@\d]+)?/g;
wrapMap.optgroup = wrapMap.option;
wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead;
wrapMap.th = wrapMap.td;
//========================== Top-level functions ==========================
//===============
// Event handlers
//===============
function elemChangeHandler(ev) {
var setter, cancel, fromAttr, linkCtx, sourceValue, cvtBack, cnvtName, target, $source, view, binding, bindings, l,
source = ev.target, onBeforeChange, onAfterChange,
to = source._jsvBnd;
// _jsvBnd is a string with the syntax: "&bindingId1&bindingId2"
if (to) {
bindings = to.slice(1).split("&");
l = bindings.length;
while (l--) {
if (binding = bindingStore[bindings[l]]) {
linkCtx = binding.linkCtx;
view = linkCtx.view;
if (to = binding.to) {
// The binding has a 'to' field, which is of the form [[targetObject, toPath], cvtBack]
$source = $(source);
onBeforeChange = view.hlp(onBeforeChangeStr);
onAfterChange = view.hlp(onAfterChangeStr);
fromAttr = defaultAttr(source);
setter = fnSetters[fromAttr];
sourceValue = $isFunction(fromAttr) ? fromAttr(source) : setter ? $source[setter]() : $source.attr(fromAttr);
if ((!onBeforeChange || !(cancel = onBeforeChange.call(view, ev) === false)) && sourceValue !== undefined) {
cnvtName = to[1];
to = to[0]; // [object, path]
target = to[0];
target = target._jsvOb ? target._ob : target;
to = to[2] || to[1];
if ($isFunction(cnvtName)) {
cvtBack = cnvtName;
} else {
cvtBack = view.tmpl.converters;
cvtBack = cvtBack && cvtBack[cnvtName] || $views.converters[cnvtName];
}
if (cvtBack) {
sourceValue = cvtBack.call(linkCtx.tag, sourceValue);
}
if (sourceValue !== undefined && target) {
$observable(target).setProperty(to, sourceValue);
if (onAfterChange) {
onAfterChange.call(linkCtx, ev);
}
}
}
if (cancel) {
ev.stopImmediatePropagation();
}
}
}
}
}
}
function propertyChangeHandler(ev, eventArgs, linkFn) {
var attr, setter, changed, sourceValue, css, tag, ctx, prevNode, nextNode, oldLinkCtx, cancel, inlineTag, elCnt, nodesToRemove,
linkCtx = this,
source = linkCtx.data,
target = linkCtx.elem,
cvt = linkCtx.cvt,
attrOrProp = "attr",
parentElem = target.parentNode,
targetElem = parentElem,
$target = $(target),
view = linkCtx.view,
onEvent = view.hlp(onBeforeChangeStr);
if (parentElem && (!onEvent || !(eventArgs && onEvent.call(linkCtx, ev, eventArgs) === false))
// If data changed, the ev.data is set to be the path. Use that to filter the handler action...
&& !(eventArgs && ev.data.prop !== "*" && ev.data.prop !== eventArgs.path)) {
// Set linkCtx on view, dynamically, just during this handler call
oldLinkCtx = view.linkCtx;
view.linkCtx = linkCtx;
if (eventArgs) {
linkCtx.eventArgs = eventArgs;
}
if (!ev || eventArgs || linkCtx._initVal) {
delete linkCtx._initVal;
sourceValue = linkFn.call(view.tmpl, source, view, $views);
// Compiled link expression for linkTag: return tagCtx or tagCtxs
attr = linkCtx.attr || defaultAttr(target, true, cvt !== undefined);
if (tag = linkCtx.tag) {
// Existing tag instance
if (eventArgs && tag.onUpdate && tag.onUpdate(ev, eventArgs, sourceValue) === false || attr === "none") {
// onUpdate returned false, or attr === "none", so we just need to bind, and we are done
observeAndBind(linkCtx, source, target, linkFn);
view.linkCtx = oldLinkCtx;
return;
}
sourceValue = tag.tagName.slice(-1) === ":" // Call convertVal if it is a {{cvt:...}} - otherwise call renderTag
? $views._cnvt(tag.tagName.slice(0, -1), view, sourceValue)
: $views._tag(tag.tagName, view, view.tmpl, sourceValue);
} else if (linkFn._ctxs) {
// For {{: ...}} without a convert or convertBack, we already have the sourceValue, and we are done
// For {{: ...}} with either cvt or cvtBack we call convertVal to get the sourceValue and instantiate the tag
// If cvt is undefined then this is a tag, and we call renderTag to get the rendered content and instantiate the tag
cvt = cvt === "" ? "true" : cvt; // If there is a cvtBack but no cvt, set cvt to "true"
sourceValue = cvt // Call convertVal if it is a {{cvt:...}} - otherwise call renderTag
? $views._cnvt(cvt, view, sourceValue) // convertVal
: $views._tag(linkFn._ctxs, view, view.tmpl, sourceValue); // renderTag
tag = view._.tag; // In both convertVal and renderTag we have instantiated a tag
attr = linkCtx.attr || attr; // linkCtx.attr may have been set to tag.attr during tag instantiation in renderTag
}
if (tag) {
// Initialize the tag with element references
tag.parentElem = (linkCtx.expr || tag._elCnt) ? target : target.parentNode;
prevNode = tag._prv;
nextNode = tag._nxt;
tag.refresh = refreshTag;
}
if ($isFunction(sourceValue)) {
error(linkCtx.expr + ": missing parens");
}
if (attr === "visible") {
attr = "css-display";
sourceValue = sourceValue
// Make sure we set the correct display style for showing this particular element ("block", "inline" etc.)
? getElementDefaultDisplay(target)
: "none";
}
if (css = attr.lastIndexOf("css-", 0) === 0 && attr.substr(4)) {
// Possible optimization for perf on integer values
// prev = $.style(target, css);
// if (+sourceValue === sourceValue) {
// // support using integer data values, e.g. 100 for width:"100px"
// prev = parseInt(prev);
// }
// if (changed = prev !== sourceValue) {
// $.style(target, css, sourceValue);
// }
if (changed = $.style(target, css) !== sourceValue) {
$.style(target, css, sourceValue);
}
} else if (attr !== "link") { // attr === "link" is for tag controls which do data binding but have no rendered output or target
if (attr === "value") {
if (target.type === "checkbox") {
sourceValue = sourceValue && sourceValue !== "false";
// The string value "false" can occur with data-link="checked{attr:expr}" - as a result of attr, and hence using convertVal()
attrOrProp = "prop";
attr = "checked";
// We will set the "checked" property
// We will compare this with the current value
}
} else if (attr === "radio") {
// This is a special binding attribute for radio buttons, which corresponds to the default 'to' binding.
// This allows binding both to value (for each input) and to the default checked radio button (for each input in named group,
// e.g. binding to parent data).
// Place value binding first: <input type="radio" data-link="value{:name} {:#get('data').data.currency:} " .../>
// or (allowing any order for the binding expressions):
// <input type="radio" value="{{:name}}" data-link="{:#get('data').data.currency:} value^{:name}" .../>
if (target.value === ("" + sourceValue)) {
// If the data value corresponds to the value attribute of this radio button input, set the checked property to true
sourceValue = true;
attrOrProp = "prop";
attr = "checked";
} else {
// Otherwise, go straight to observeAndBind, without updating.
// (The browser will remove the 'checked' attribute, when another radio button in the group is checked).
observeAndBind(linkCtx, source, target, linkFn);
view.linkCtx = oldLinkCtx;
return;
}
} else if (attr === "selected" || attr === "disabled" || attr === "multiple" || attr === "readlonly") {
sourceValue = (sourceValue && sourceValue !== "false") ? attr : null;
// Use attr, not prop, so when the options (for example) are changed dynamically, but include the previously selected value,
// they will still be selected after the change
}
setter = fnSetters[attr];
if (setter) {
if (changed = tag || $target[setter]() !== sourceValue) {
if (attr === "html") {
if (tag) {
inlineTag = tag._.inline;
tag.refresh(sourceValue);
if (!inlineTag && tag._.inline) {
// data-linked tag: data-link="{tagname ...}" has been converted to inline
// We will skip the observeAndBind call below, since the inserted tag binding above replaces that binding
view.linkCtx = oldLinkCtx;
return;
}
} else {
// data-linked value: data-link="expr" or data-link="{:expr}" or data-link="{:expr:}" (with no convert or convertBack)
$target.empty();
targetElem = target;
view.link(source, targetElem, prevNode, nextNode, sourceValue, tag && {tag: tag._tgId});
}
} else if (attr === "text" && !target.children[0]) {
// This code is faster then $target,text()
if (target.textContent !== undefined) {
target.textContent = sourceValue;
} else {
target.innerText = sourceValue === null ? "" : sourceValue;
}
} else {
$target[setter](sourceValue);
}
// Removing this for now, to avoid side-effects when you programmatically set the value, and want the focus to stay on the text box
// if (target.nodeName.toLowerCase() === "input") {
// $target.blur(); // Issue with IE. This ensures HTML rendering is updated.
// }
}
} else if (changed = $target[attrOrProp](attr) != sourceValue) {
// Setting an attribute to undefined should remove the attribute
$target[attrOrProp](attr, sourceValue === undefined && attrOrProp === "attr" ? null : sourceValue);
}
}
if (eventArgs && changed && (onEvent = view.hlp(onAfterChangeStr))) {
onEvent.call(linkCtx, ev, eventArgs);
}
}
observeAndBind(linkCtx, source, target, linkFn);
// Remove dynamically added linkCtx from view
view.linkCtx = oldLinkCtx;
}
}
function arrayChangeHandler(ev, eventArgs) {
var self = this,
onBeforeChange = self.hlp(onBeforeChangeStr),
onAfterChange = self.hlp(onAfterChangeStr);
if (!onBeforeChange || onBeforeChange.call(ev, eventArgs) !== false) {
if (eventArgs) {
// This is an observable action (not a trigger/handler call from pushValues, or similar, for which eventArgs will be null)
var action = eventArgs.change,
index = eventArgs.index,
items = eventArgs.items;
switch (action) {
case "insert":
self.addViews(index, items);
break;
case "remove":
self.removeViews(index, items.length);
break;
case "move":
self.refresh(); // Could optimize this
break;
case "refresh":
self.refresh();
break;
// Othercases: (e.g.undefined, for setProperty on observable object) etc. do nothing
}
}
if (onAfterChange) {
onAfterChange.call(this, ev, eventArgs);
}
}
}
//=============================
// Utilities for event handlers
//=============================
function getElementDefaultDisplay(elem) {
// Get the 'visible' display style for the element
var testElem, nodeName,
getComputedStyle = global.getComputedStyle,
cStyle = (elem.currentStyle || getComputedStyle.call(global, elem, "")).display;
if (cStyle === "none" && !(cStyle = displayStyles[nodeName = elem.nodeName])) {
// Currently display: none, and the 'visible' style has not been cached.
// We create an element to find the correct visible display style for this nodeName
testElem = document.createElement(nodeName);
document.body.appendChild(testElem);
cStyle = (getComputedStyle ? getComputedStyle.call(global, testElem, "") : testElem.currentStyle).display;
// Cache the result as a hash against nodeName
displayStyles[nodeName] = cStyle;
document.body.removeChild(testElem);
}
return cStyle;
}
function setArrayChangeLink(view) {
// Add/remove arrayChange handler on view
var handler, arrayBinding,
data = view.data, // undefined if view is being removed
bound = view._.bnd; // true for top-level link() or data-link="{for}", or the for tag instance for {^{for}} (or for any custom tag that has an onArrayChange handler)
if (!view._.useKey && bound) {
// This is an array view. (view._.useKey not defined => data is array), and is data-bound to collection change events
if (arrayBinding = view._.bndArr) {
// First remove the current handler if there is one
$([arrayBinding[1]]).off(arrayChangeStr, arrayBinding[0]);
view._.bndArr = undefined;
}
if (bound !== !!bound && !bound.linkCtx) {
// bound is not a boolean, so it is the bound tag that 'owns' this array binding - e.g. {^{for...}}
if (data) {
bound._.arrVws[view._.id] = view;
} else {
delete bound._.arrVws[view._.id]; // if view.data is undefined, view is being removed
}
} else if (data) {
// If this view is not being removed, but the data array has been replaced, then bind to the new data array
handler = function(ev) {
if (!(ev.data && ev.data.off)) {
// Skip if !!ev.data.off: - a handler that has already been removed (maybe was on handler collection at call time - then removed by another handler)
// If view.data is undefined, do nothing. (Corresponds to case where there is another handler on the same data whose
// effect was to remove this view, and which happened to precede this event in the trigger sequence. So although this
// event has been removed now, it is still called since already on the trigger sequence)
arrayChangeHandler.apply(view, arguments);
}
};
$([data]).on(arrayChangeStr, handler);
view._.bndArr = [handler, data];
}
}
}
function defaultAttr(elem, to, linkGetVal) {
// to: true - default attribute for setting data value on HTML element; false: default attribute for getting value from HTML element
// Merge in the default attribute bindings for this target element
var nodeName = elem.nodeName.toLowerCase(),
attr = $viewsSettings.merge[nodeName];
return attr
? (to
? ((nodeName === "input" && elem.type === "radio") // For radio buttons, bind from value, but bind to 'radio' - special value.
? "radio"
: attr.to.toAttr)
: attr.from.fromAttr)
: to
? linkGetVal ? "text" : "html" // Default innerText for data-link="a.b.c" or data-link="{:a.b.c}" - otherwise innerHTML
: ""; // Default is not to bind from
}
//==============================
// Rendering and DOM insertion
//==============================
function renderAndLink(view, index, tmpl, views, data, context, refresh) {
var html, linkToNode, prevView, tag, nodesToRemove, bindId,
parentNode = view.parentElem,
prevNode = view._prv,
nextNode = view._nxt,
elCnt = view._elCnt;
if (prevNode && prevNode.parentNode !== parentNode) {
error("Missing parentNode");
// Abandon, since node has already been removed, or wrapper element has been inserted between prevNode and parentNode
}
if (refresh) {
nodesToRemove = view.nodes();
if (elCnt && prevNode && prevNode !== nextNode) {
// This prevNode will be removed from the DOM, so transfer the view tokens on prevNode to nextNode of this 'viewToRefresh'
transferViewTokens(prevNode, nextNode, parentNode, view._.id, "_", true);
}
// Remove child views
view.removeViews(undefined, undefined, true);
linkToNode = nextNode;
if (elCnt) {
prevNode = prevNode
? prevNode.previousSibling
: nextNode
? nextNode.previousSibling
: parentNode.lastChild;
}
// Remove HTML nodes
$(nodesToRemove).remove();
for (bindId in view._.bnds) {
// The view bindings may have already been removed above in: $(nodesToRemove).remove();
// If not, remove them here:
removeViewBinding(bindId);
}
} else {
// addViews. Only called if view is of type "array"
if (index) {
// index is a number, so indexed view in view array
prevView = views[index - 1];
if (!prevView) {
return false; // If subview for provided index does not exist, do nothing
}
prevNode = prevView._nxt;
}
if (elCnt) {
linkToNode = prevNode;
prevNode = linkToNode
? linkToNode.previousSibling // There is a linkToNode, so insert after previousSibling, or at the beginning
: parentNode.lastChild; // If no prevView and no prevNode, index is 0 and there are the container is empty,
// so prevNode = linkToNode = null. But if prevNode._nxt is null then we set prevNode to parentNode.lastChild
// (which must be before the prevView) so we insert after that node - and only link the inserted nodes
} else {
linkToNode = prevNode.nextSibling;
}
}
html = tmpl.render(data, context, view, refresh || index, view._.useKey && refresh, true);
// Pass in self._.useKey as test for layout template (which corresponds to when self._.useKey > 0 and self.data is an array)
// Link the new HTML nodes to the data
view.link(data, parentNode, prevNode, linkToNode, html, prevView);
//}, 0);
}
//=====================
// addBindingMarkers
//=====================
function addBindingMarkers(value, view, tmplBindingKey) {
// Insert binding markers into the rendered template output, which will get converted to appropriate
// data-jsv attributes (element-only content) or script marker nodes (phrasing or flow content), in convertMarkers,
// within view.link, prior to inserting into the DOM. Linking will then bind based on these markers in the DOM.
var id, tag, end;
if (tmplBindingKey) {
// This is a binding marker for a data-bound tag {^{...}}
end = "^`";
tag = view._.tag // This is {^{>...}} or {^{tag ...}} or {{cvt:...} - so tag was defined in convertVal or renderTag
|| { // This is {^{:...}} so tag is not yet defined
_: {
inline: true,
bnd: tmplBindingKey
},
tagCtx: {
view:view
},
flow: true
};
id = tag._tgId;
tag.refresh = refreshTag;
if (!id) {
bindingStore[id = bindingKey++] = tag; // Store the tag temporarily, ready for databinding.
// During linking, in addDataBinding, the tag will be attached to the linkCtx,
// and then in observeAndBind, bindingStore[bindId] will be replaced by binding info.
tag._tgId = "" + id;
}
} else {
// This is a binding marker for a view
// Add the view to the store of current linked views
end = "_`";
viewStore[id = view._.id] = view;
}
// Example: "_#23`TheValue_/23`"
return "#" + id + end
+ (value === undefined ? "" : value) // For {^{:name}} this gives the equivalent semantics to compiled (v=data.name)!=u?v:""; used in {{:name}} or data-link="name"
+ "/" + id + end;
}
//==============================
// Data-linking and data binding
//==============================
//---------------
// observeAndBind
//---------------
function observeAndBind(linkCtx, source, target, linkFn) { //TODO? linkFnArgs) {;
var tag, binding, cvtBack, paths, lastPath, pathIndex,
depends = [],
bindId = linkCtx._bndId || "" + bindingKey++,
handler = linkCtx._hdlr;
delete linkCtx._bndId;
if (tag = linkCtx.tag) {
// Use the 'depends' paths set on linkCtx.tag - which may have been set on declaration
// or in events: init, render, onBeforeLink, onAfterLink etc.
depends = tag.depends || depends;
depends = $isFunction(depends) ? tag.depends(tag) : depends;
cvtBack = tag.onChange;
}
cvtBack = cvtBack || linkCtx._cvtBk;
if (!linkCtx._depends || ("" + linkCtx._depends !== "" + depends)) {
// Only bind the first time, or if the new depends (toString) has changed from when last bound
if (linkCtx._depends) {
// Unobserve previous binding
$observe(source, linkCtx._depends, handler, true);
}
binding = $observe($.isArray(source) ? [source] : source , paths = linkCtx.fn.paths || linkCtx.fn, depends, handler, linkCtx._ctxCb);
// The binding returned by $observe has a bnd array with the source objects of the individual bindings.
binding.elem = target; // The target of all the individual bindings
binding.linkCtx = linkCtx;
binding._tgId = bindId;
// Add to the _jsvBnd on the target the view id and binding id - for unbinding when the target element is removed
target._jsvBnd = target._jsvBnd || "";
target._jsvBnd += "&" + bindId;
linkCtx._depends = depends;
// Store the binding key on the view, for disposal when the view is removed
linkCtx.view._.bnds[bindId] = bindId;
// Store the binding.
bindingStore[bindId] = binding; // Note: If this corresponds to a bound tag, we are replacing the
// temporarily stored tag by the stored binding. The tag will now be at binding.linkCtx.tag
if (cvtBack !== undefined) {
// Two-way binding.
// We set the binding.to[1] to be the cvtBack, and binding.to[0] to be either the path to the target, or [object, path] where the target is the path on the provided object.
// So for a path with an object call: a.b.getObject().d.e, then we set to[0] to be [returnedObject, "d.e"], and we bind to the path on the returned object as target
// Otherwise our target is the first path, paths[0], which we will convert with contextCb() for paths like ~a.b.c or #x.y.z
//TODO add support for two-way binding with named props <input data-link="{:a foo=b:}" - currently will not bind to the correct target
pathIndex = paths.length;
while ("" + (lastPath = paths[--pathIndex]) !== lastPath) {}; // If the lastPath is an object (e.g. with _jsvOb property), take preceding one
lastPath = paths[pathIndex] = lastPath.split("^").join("."); // We don't need the "^" since binding has happened. For to binding, require just "."s
binding.to = lastPath.charAt(0) === "."
? [[paths[pathIndex-1], lastPath.slice(1)], cvtBack]
: [linkCtx._ctxCb(paths[0]) || [source, paths[0]], cvtBack];
}
}
}
//-------
// $.link
//-------
function tmplLink(to, from, context, parentView, prevNode, nextNode) {
return $link(this, to, from, context, parentView, prevNode, nextNode);
}
function $link(tmplOrLinkTag, to, from, context, parentView, prevNode, nextNode) {
if (tmplOrLinkTag && to) {
to = to.jquery ? to : $(to); // to is a jquery object or an element or selector
if (!activeBody) {
activeBody = document.body;
$(activeBody).on(elementChangeStr, elemChangeHandler);
}
var i, k, html, vwInfos, view, placeholderParent, targetEl,
onRender = addBindingMarkers,
replaceMode = context && context.target === "replace",
l = to.length;
while (l--) {
targetEl = to[l];
if ("" + tmplOrLinkTag === tmplOrLinkTag) {
// tmplOrLinkTag is a string: treat as data-link expression.
addDataBinding(tmplOrLinkTag, targetEl, $view(targetEl), from, context);
} else {
parentView = parentView || $view(targetEl);
if (tmplOrLinkTag.markup !== undefined) {
// This is a call to template.link()
if (parentView.link === false) {
context = context || {};
context.link = onRender = false; // If link=false, don't allow nested context to switch on linking
}
// Set link=false, explicitly, to disable linking within a template nested within a linked template
if (replaceMode) {
placeholderParent = targetEl.parentNode;
}
html = tmplOrLinkTag.render(from, context, parentView, undefined, undefined, onRender);
// TODO Consider finding a way to bind data (link) within template without html being different for each view, the HTML can
// be evaluated once outside the while (l--), and pushed into a document fragment, then cloned and inserted at each target.
if (placeholderParent) {
// This is target="replace" mode
prevNode = targetEl.previousSibling;
nextNode = targetEl.nextSibling;
$.cleanData([targetEl], true);
placeholderParent.removeChild(targetEl);
targetEl = placeholderParent;
} else {
prevNode = nextNode = undefined; // When linking from a template, prevNode and nextNode parameters are ignored
$(targetEl).empty();
}
} else if (tmplOrLinkTag !== true) {
break;
}
// TODO Consider deferred linking API feature on per-template basis - {@{ instead of {^{ which allows the user to see the rendered content
// before that content is linked, with better perceived perf. Have view.link return a deferred, and pass that to onAfterLink...
// or something along those lines.
// setTimeout(function() {
if (targetEl._dfr && !nextNode) {
// We are inserting new content and the target element has some deferred binding annotations,and there is no nextNode.
// Those views may be stale views (that will be recreated in this new linking action) so we will first remove them
// (if not already removed).
vwInfos = viewInfos(targetEl._dfr, true, rOpenViewMarkers);
for (i = 0, k = vwInfos.length; i < k; i++) {
view = vwInfos[i];
if ((view = viewStore[view.id]) && view.data !== undefined) {
// If this is the _prv (prevNode) for a view, remove the view
// - unless view.data is undefined, in which case it is already being removed
view.parent.removeViews(view._.key, undefined, true);
}
}
targetEl._dfr = "";
}
// Link the content of the element, since this is a call to template.link(), or to $(el).link(true, ...),
parentView.link(from, targetEl, prevNode, nextNode, html);
//}, 0);
}
}
}
return to; // Allow chaining, to attach event handlers, etc.
}
//----------
// view.link
//----------
function viewLink(outerData, parentNode, prevNode, nextNode, html, refresh) {
// Optionally insert HTML into DOM using documentFragments (and wrapping HTML appropriately).
// Data-link existing contents of parentNode, or the inserted HTML, if provided
// Depending on the content model for the HTML elements, the standard data-linking markers inserted in the HTML by addBindingMarkers during
// template rendering will be converted either to script marker nodes or, for element-only content sections, by data-jsv element annotations.
// Data-linking will then add _prv and _nxt to views, where:
// _prv: References the previous node (script element of type "jsv123"), or (for elCnt=true), the first element node in the view
// _nxt: References the last node (script element of type "jsv/123"), or (for elCnt=true), the next element node after the view.
//==== nested functions ====
function convertMarkers(all, preceding, selfClose, closeTag, spaceBefore, id, spaceAfter, tag1, tag2, closeTag2, spaceAfterClose, selfClose2) {
//rConvertMarkers = /(^|(\/>)|(<\/\w+>)|>|)(\s*)_([#\/]\d+_)`(\s*)(<\w+(?=[\s\/>]))?|\s*(?:(<\w+(?=[\s\/>]))|(<\/\w+>)(\s*)|(\/>)\s*)/g,
// prec, slfCl, clTag, spaceBefore, id, spaceAfter, tag1, tag2, clTag2, sac slfCl2,
// Convert the markers that were included by addBindingMarkers in template output, to appropriate DOM annotations:
// data-jsv attributes (for element-only content) or script marker nodes (within phrasing or flow content).
// TODO consider detecting 'quoted' contexts (attribute strings) so that attribute encoding does not need to encode >
// Currently rAttrEncode = /[><"'&]/g includes '>' encoding in order to avoid erroneous parsing of <span title="<a/>">
var endOfElCnt = "";
tag = tag1 || tag2 || "";
closeTag = closeTag || selfClose || closeTag2 || selfClose2;
if (closeTag) {
if (validate && (selfClose || selfClose2) && !voidElems[parentTag]) {
syntaxError("'<" + parentTag + "... />' in:\n" + html);
}
prevElCnt = elCnt;
parentTag = tagStack.shift();
elCnt = elContent[parentTag];
if (prevElCnt) {
// If there are ids (markers since the last tag), move them to the defer string
defer += ids;
ids = "";
if (!elCnt) {
endOfElCnt = (closeTag2 || "") + openScript + "@" + defer + closeScript + (spaceAfterClose || "");
defer = deferStack.shift();
} else {
defer += "-"; // Will be used for stepping back through deferred tokens
}
}
}
if (elCnt) {
// elContent maps tagNames which have only element content, so may not support script nodes.
// We are in element-only content, can remove white space, and use data-jsv attributes on elements as markers
// Example: <tr data-jsv="/2_#6_"> - close marker for view 2 and open marker for view 6
if (id) {
// append marker for this id, to ids string
ids += id;
} else {
preceding = (closeTag2 || selfClose2 || "");
}
if (tag) {
preceding += tag;
if (ids) {
preceding += ' ' + jsvAttrStr + '="' + ids + '"';
ids = "";
}
}
} else {
// We are in phrasing or flow content, so use script marker nodes
// Example: <script type="jsv3/"></script> - data-bound tag, close marker
preceding = id
? (preceding + endOfElCnt + spaceBefore + openScript + id + closeScript + spaceAfter + tag)
: endOfElCnt || all;
}
if (tag) {
// If there are ids (markers since the last tag), move them to the defer string
tagStack.unshift(parentTag);
parentTag = tag.slice(1);
if (tagStack[0] === badParent[parentTag]) {
// TODO: move this to design-time validation check
error('"' + parentTag + '" has incorrect parent tag');
}
if ((elCnt = elContent[parentTag]) && !prevElCnt) {
deferStack.unshift(defer);
defer = "";
}
prevElCnt = elCnt;
//TODO Consider providing validation which throws if you place <span> as child of <tr>, etc. - since if not caught,
//this can cause errors subsequently which are difficult to debug.
// if (elContent[tagStack[0]]>2 && !elCnt) {
// error(parentTag + " in " + tagStack[0]);
// }
if (defer && elCnt) {
defer += "+"; // Will be used for stepping back through deferred tokens
}
}
return preceding;
}
function processViewInfos(vwInfos, targetParent) {
// If targetParent, we are processing viewInfos (which may include navigation through '+-' paths) and hooking up to the right parentElem etc.
// (and elem may also be defined - the next node)
// If no targetParent, then we are processing viewInfos on newly inserted content
var deferPath, deferChar, bindChar, parentElem, id, onAftCr, deep,
addedBindEls = [];
// In elCnt context (element-only content model), prevNode is the first node after the open, nextNode is the first node after the close.
// If both are null/undefined, then open and close are at end of parent content, so the view is empty, and its placeholder is the
// 'lastChild' of the parentNode. If there is a prevNode, then it is either the first node in the view, or the view is empty and
// its placeholder is the 'previousSibling' of the prevNode, which is also the nextNode.
if (vwInfos) {
//targetParent = targetParent || targetElem && targetElem.previousSibling;
//targetParent = targetElem ? targetElem.previousSibling : targetParent;
len = vwInfos.length;
if (vwInfos.tokens.charAt(0) === "@") {
// This is a special script element that was created in convertMarkers() to process deferred bindings, and inserted following the
// target parent element - because no element tags were encountered to carry those binding tokens.
targetParent = elem.previousSibling;
elem.parentNode.removeChild(elem);
elem = null;
}
len = vwInfos.length;
while (len--) {
vwInfo = vwInfos[len];
//if (prevIds.indexOf(vwInfo.token) < 0) { // This token is a newly created view or tag binding
bindChar = vwInfo.ch;
if (deferPath = vwInfo.path) {
// We have a 'deferred path'
j = deferPath.length - 1;
while (deferChar = deferPath.charAt(j--)) {
// Use the "+" and"-" characters to navigate the path back to the original parent node where the deferred bindings ocurred
if (deferChar === "+") {
if (deferPath.charAt(j) === "-") {
j--;
targetParent = targetParent.previousSibling;
} else {
targetParent = targetParent.parentNode;
}
} else {
targetParent = targetParent.lastChild;
}
// Note: Can use previousSibling and lastChild, not previousElementSibling and lastElementChild,
// since we have removed white space within elCnt. Hence support IE < 9
}
}
if (bindChar === "^") {
if (tag = bindingStore[id = vwInfo.id]) {
// The binding may have been deleted, for example in a different handler to an array collectionChange event
// This is a tag binding
deep = targetParent && (!elem || elem.parentNode !== targetParent);
if (!elem || deep) {
tag.parentElem = targetParent;
}
if (vwInfo.elCnt) {
if (vwInfo.open) {
if (targetParent) {
// This is an 'open view' node (preceding script marker node,
// or if elCnt, the first element in the view, with a data-jsv annotation) for binding
targetParent._dfr = "#" + id + bindChar + (targetParent._dfr || "");
}
} else if (deep) {
// There is no ._nxt so add token to _dfr. It is deferred.
targetParent._dfr = "/" + id + bindChar + (targetParent._dfr || "");
}
}
// This is an open or close marker for a data-bound tag {^{...}}. Add it to bindEls.
addedBindEls.push([deep ? null : elem, vwInfo]);
}
} else if (view = viewStore[id = vwInfo.id]) {
// The view may have been deleted, for example in a different handler to an array collectionChange event
if (!view.link) {
// If view is not already extended for JsViews, extend and initialize the view object created in JsRender, as a JsViews view
view.parentElem = targetParent || elem && elem.parentNode || parentNode;
$extend(view, LinkedView);
view._.onRender = addBindingMarkers;
view._.onArrayChange = arrayChangeHandler;
setArrayChangeLink(view);
}
parentElem = view.parentElem;
if (vwInfo.open) {
// This is an 'open view' node (preceding script marker node,
// or if elCnt, the first element in the view, with a data-jsv annotation) for binding
view._elCnt = vwInfo.elCnt;
if (targetParent) {
targetParent._dfr = "#" + id + bindChar + (targetParent._dfr || "");
} else {
// No targetParent, so there is a ._nxt elem (and this is processing tokens on the elem)
if (!view._prv) {
parentElem._dfr = removeSubStr(parentElem._dfr, "#" + id + bindChar);
}
view._prv = elem;
}
} else {
// This is a 'close view' marker node for binding
if (targetParent && (!elem || elem.parentNode !== targetParent)) {
// There is no ._nxt so add token to _dfr. It is deferred.
targetParent._dfr = "/" + id + bindChar + (targetParent._dfr || "");
view._nxt = undefined;
} else if (elem) {
// This view did not have a ._nxt, but has one now, so token may be in _dfr, and must be removed. (No longer deferred)
if (!view._nxt) {
parentElem._dfr = removeSubStr(parentElem._dfr, "/" + id + bindChar);
}
view._nxt = elem;
}
linkCtx = view.linkCtx;
if (onAftCr = onAfterCreate || (view.ctx && view.ctx.onAfterCreate)) {
onAftCr.call(linkCtx, view);
}
}
//}
}
}
len = addedBindEls.length;
while (len--) {
// These were added in reverse order to addedBindEls. We push them in BindEls in the correct order.
bindEls.push(addedBindEls[len]);
}
}
return !vwInfos || vwInfos.elCnt;
}
function getViewInfos(vwInfos) {
// Used by view.childTags() and tag.childTags()
// Similar to processViewInfos in how it steps through bindings to find tags. Only finds data-bound tags.
var level, parentTag;
if (len = vwInfos && vwInfos.length) {
for (j = 0; j < len; j++) {
vwInfo = vwInfos[j];
if (get.id) {
get.id = get.id !== vwInfo.id && get.id;
} else {
// This is an open marker for a data-bound tag {^{...}}, within the content of the tag whose id is get.id. Add it to bindEls.
parentTag = tag = bindingStore[vwInfo.id].linkCtx.tag;
if (!tag.flow) {
if (!deep) {
level = 1;
while (parentTag = parentTag.parent) {
level++;
}
tagDepth = tagDepth || level; // The level of the first tag encountered.
}
if ((deep || level === tagDepth) && (!tagName || tag.tagName === tagName)) {
// Filter on top-level or tagName as appropriate
tags.push(tag);
}
}
}
}
}
}
function dataLink() {
//================ Data-link and fixup of data-jsv annotations ================
elems = qsa ? parentNode.querySelectorAll(linkViewsSel) : $(linkViewsSel, parentNode).get();
l = elems.length;
// The prevNode will be in the returned query, since we called markPrevOrNextNode() on it.
// But it may have contained nodes that satisfy the selector also.
if (prevNode) {
// Find the last contained node one to use as the prevNode - so we only link subsequent elems in the query
prevNodes = qsa ? prevNode.querySelectorAll(linkViewsSel) : $(linkViewsSel, prevNode).get();
prevNode = prevNodes.length ? prevNodes[prevNodes.length - 1] : prevNode;
}
tagDepth = 0;
for (i = 0; i < l; i++) {
elem = elems[i];
if (prevNode && !found) {
// If prevNode is set, not false, skip linking. If this element is the prevNode, set to false so subsequent elements will link.
found = (elem === prevNode);
} else if (nextNode && elem === nextNode) {
// If nextNode is set then break when we get to nextNode
break;
} else if (elem.parentNode
// elem has not been removed from DOM
&& processInfos(viewInfos(elem, undefined, tags && rOpenTagMarkers))
// If a link() call, processViewInfos() adds bindings to bindEls, and returns true for non-script nodes, for adding data-link bindings
// If a childTags() call getViewInfos adds tag bindings to tags array.