-
Notifications
You must be signed in to change notification settings - Fork 130
/
jquery.views.js
4041 lines (3719 loc) · 154 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
/*! jquery.views.js v1.0.15: http://jsviews.com/ */
/*
* Interactive data-driven views using JsRender templates.
* Subcomponent of JsViews
* Requires jQuery and jsrender.js (Best-of-breed templating in browser or on Node.js)
* See JsRender at http://jsviews.com/#download and http://github.com/BorisMoore/jsrender
* Also requires jquery.observable.js
* See JsObservable at http://jsviews.com/#download and http://github.com/BorisMoore/jsviews
*
* Copyright 2024, Boris Moore
* Released under the MIT License.
*/
//jshint -W018, -W041, -W120
(function(factory, global) {
// global var is the this object, which is window when running in the usual browser environment
var $ = global.jQuery;
if (typeof exports === "object") { // CommonJS e.g. Browserify
module.exports = $
? factory(global, $)
: function($) { // If no global jQuery, take jQuery passed as parameter (with JsRender and JsObservable): require("jquery.views")(jQuery)
return factory(global, $);
};
} else if (typeof define === "function" && define.amd) { // AMD script loader, e.g. RequireJS
define(["jquery", "./jsrender", "./jquery.observable"], function($, jsr, jso) {
return factory(global, $, jsr, jso);
}); // Require jQuery, JsRender, JsObservable
} else { // Browser using plain <script> tag
factory(global, false);
}
} (
// factory (for jquery.views.js)
function(global, $, jsr, jso) {
"use strict";
//========================== Top-level vars ==========================
// global var is the this object, which is window when running in the usual browser environment
var setGlobals = $ === false; // Only set globals if script block in browser (not AMD and not CommonJS)
jsr = jsr || setGlobals && global.jsrender;
$ = $ || global.jQuery;
var versionNumber = "v1.0.15",
requiresStr = "jquery.views.js requires ";
if (!$ || !$.fn) {
// jQuery is not loaded.
throw requiresStr + "jQuery"; // We require jQuery
}
if (jsr && !jsr.fn) {
jsr.views.sub._jq($); // map over from jsrender namespace to jQuery namespace
}
var $observe, $observable,
$isArray = $.isArray,
$views = $.views;
if (!$.render) {
// JsRender is not loaded.
throw requiresStr + "jsrender.js"; // jsrender.js must be loaded before JsViews and after jQuery
}
if ($views.jsviews !== versionNumber) {
throw requiresStr + "query.observable.js " + versionNumber; // Wrong version number
}
if (!$views || !$views.map || $views.jsviews !== versionNumber) {
// JsRender is not loaded.
throw requiresStr + "jsrender.js " + versionNumber; // jsrender.js must be loaded before JsViews and after jQuery
}
var document = global.document,
$viewsSettings = $views.settings,
$sub = $views.sub,
$subSettings = $sub.settings,
$extend = $sub.extend,
$isFunction = $.isFunction,
$expando = $.expando,
$converters = $views.converters,
$tags = $views.tags,
$subSettingsAdvanced = $subSettings.advanced,
// These two settings can be overridden on settings after loading jsRender, and prior to loading jquery.observable.js and/or JsViews
propertyChangeStr = $sub.propChng = $sub.propChng || "propertyChange",
arrayChangeStr = $sub.arrChng = $sub.arrChng || "arrayChange",
STRING = "string",
HTML = "html",
_ocp = "_ocp", // Observable contextual parameter
syntaxError = $sub.syntaxErr,
rFirstElem = /<(?!script)(\w+)[>\s]/,
error = $sub._er,
onRenderError = $sub._err,
delimOpenChar0, delimOpenChar1, delimCloseChar0, delimCloseChar1, linkChar, topView,
rEscapeQuotes = /['"\\]/g; // Escape quotes and \ character
if ($.link) { return $; } // JsViews is already loaded
$subSettings.trigger = true;
var activeBody, rTagDatalink, $view, $viewsLinkAttr, linkViewsSel, wrapMap, viewStore, oldAdvSet, useInput,
isIE = window.navigator.userAgent,
TEXTCONTENT = document.textContent !== undefined ? "textContent" : "innerText",
jsvAttrStr = "data-jsv",
elementChangeStr = "change.jsv",
onBeforeChangeStr = "onBeforeChange",
onAfterChangeStr = "onAfterChange",
onAfterCreateStr = "onAfterCreate",
CHECKED = "checked",
CHECKBOX = "checkbox",
RADIO = "radio",
RADIOINPUT = "input[type=",
CHECKBOXINPUT = RADIOINPUT + CHECKBOX + "]", // input[type=checkbox]
NONE = "none",
VALUE = "value",
SCRIPT = "SCRIPT",
TRUE = "true",
closeScript = '"></script>',
openScript = '<script type="jsv',
deferAttr = jsvAttrStr + "-df",
bindElsSel = "script,[" + jsvAttrStr + "]",
fnSetters = {
value: "val",
input: "val",
html: HTML,
text: "text"
},
valueBinding = {from: VALUE, to: VALUE},
isCleanCall = 0,
oldCleanData = $.cleanData,
oldJsvDelimiters = $viewsSettings.delimiters,
linkExprStore = {}, // Compiled functions for data-link expressions
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, svg: 1, svg_ns: 1},
badParent = {tr: "table"},
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 = {},
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,
rTagMarkers = /(?:(#)|(\/))(\d+)(\^)/g,
rOpenTagMarkers = /(#)()(\d+)(\^)/g,
rMarkerTokens = /(?:(#)|(\/))(\d+)([_^])([-+@\d]+)?/g,
rShallowArrayPath = /^[^.]*$/, // No '.' in path
getComputedStyle = global.getComputedStyle,
$inArray = $.inArray;
RADIOINPUT += RADIO + "]"; // input[type=radio]
isIE = isIE.indexOf('MSIE ')>0 || isIE.indexOf('Trident/')>0;
$observable = $.observable;
if (!$observable) {
// JsObservable is not loaded.
throw requiresStr + "jquery.observable.js"; // jquery.observable.js must be loaded before JsViews
}
$observe = $observable.observe;
$subSettings._clFns = function() {
linkExprStore = {};
};
//========================== Top-level functions ==========================
//===============
// Event handlers
//===============
function updateValues(sourceValues, tagElse, async, bindId, ev) {
// Observably update a data value targeted by the binding.to binding of a 2way data-link binding. Called when elem changes
// Called when linkedElem of a tag control changes: as updateValue(val, index, tagElse, bindId, ev) - this: undefined
// Called directly as tag.updateValues(val1, val2, val3, ...) - this: tag
var linkCtx, cvtBack, cnvtName, target, view, binding, sourceValue, origVals, sourceElem, sourceEl,
tos, to, tcpTag, exprOb, contextCb, l, m, tag, vals, ind;
if (bindId && bindId._tgId) {
tag = bindId;
bindId = tag._tgId;
if (!tag.bindTo) {
defineBindToDataTargets(bindingStore[bindId], tag); // If this tag is updating for the first time, we need to create the 'to' bindings first
tag.bindTo = [0];
}
}
if ((binding = bindingStore[bindId]) && (tos = binding.to)) {
tos = tos[tagElse||0];
// The binding has a 'to' field, which is of the form [tosForElse0, tosForElse1, ...]
// where tosForElseX is of the form [[[targetObject, toPath], [targetObject, toPath], ...], cvtBack]
linkCtx = binding.linkCtx;
sourceElem = linkCtx.elem;
view = linkCtx.view;
tag = linkCtx.tag;
if (!tag && tos._cxp) {
tag = tos._cxp.path !== _ocp && tos._cxp.tag;
sourceValue = sourceValues[0];
sourceValues = [];
sourceValues[tos._cxp.ind] = sourceValue;
}
if (tag) {
tag._.chg = 1; // Set 'changing' marker to prevent tag update from updating itself
if (cnvtName = tag.convertBack) {
if ($isFunction(cnvtName)) {
cvtBack = cnvtName;
} else {
cvtBack = view.getRsc("converters", cnvtName);
}
}
}
if (sourceElem.nodeName === "SELECT") {
// data-link <select> to string or (multiselect) array of strings
if (sourceElem.multiple && sourceValues[0] === null) {
// Case where sourceValues was undefined, and set to [null] by $source[setter]() above
sourceValues = [[]];
}
sourceElem._jsvSel = sourceValues;
} else if (sourceElem._jsvSel) {
// Checkbox group (possibly using {{checkboxgroup}} tag) data-linking to array of strings
vals = sourceElem._jsvSel.slice();
ind = $inArray(sourceElem.value, vals);
if (ind > -1 && !sourceElem.checked) {
vals.splice(ind, 1); // Unchecking this checkbox
} else if (ind < 0 && sourceElem.checked) {
vals.push(sourceElem.value); // Checking this checkbox
}
sourceValues = [vals];
}
origVals = sourceValues;
l = tos.length;
if (cvtBack) {
sourceValues = cvtBack.apply(tag, sourceValues);
if (sourceValues === undefined) {
tos = []; // If cvtBack does not return anything, do not update target.
//(But cvtBack may be designed to modify observable values from code as a side effect)
}
if (!$isArray(sourceValues) || (sourceValues.arg0 !== false && (l === 1 || sourceValues.length !== l || sourceValues.arg0))) {
sourceValues = [sourceValues];
// If there are multiple tos (e.g. multiple args on data-linked input) then cvtBack can update not only
// the first arg, but all of them by returning an array.
}
}
while (l--) {
if (to = tos[l]) {
to = typeof to === STRING ? [linkCtx.data, to] : to; // [object, path]
target = to[0];
tcpTag = to.tag; // If this is a tag contextual parameter - the owner tag
sourceValue = (target && target._ocp && !target._vw
? origVals // If to target is for tag contextual parameter set to static expression (or uninitialized) - we are
// binding to tag.ctx.foo._ocp - and we use original values, without applying cvtBack converter
: sourceValues // Otherwise use the converted value
)[l];
if (sourceValue !== undefined && (!tag || !tag.onBeforeUpdateVal || tag.onBeforeUpdateVal(ev, {
change: "change",
data: target,
path: to[1],
index: l,
tagElse: tagElse,
value: sourceValue
}) !== false)) {
if (tcpTag) { // We are modifying a tag contextual parameter ~foo (e.g. from within block) so update 'owner' tag: tcpTag
if ((m = tcpTag._.toIndex[to.ind]) !== undefined) {
tcpTag.updateValue(sourceValue, m, to.tagElse, undefined, undefined, ev); // if doesn't map, don't update, or update scoped tagCtxPrm. But should initialize from outer from binding...
}
tcpTag.setValue(sourceValue, to.ind, to.tagElse);
} else if (sourceValue !== undefined && target) {
if ((tcpTag = ev && (sourceEl = ev.target)._jsvInd === l && sourceEl._jsvLkEl) && (m = tcpTag._.fromIndex[l]) !== undefined) {
// The source is a tag linkedElem (linkedElement: [..., "elemSelector", ...], which is updating
tcpTag.setValue(origVals[l], m, sourceEl._jsvElse);
}
if (target._cpfn) {
contextCb = linkCtx._ctxCb; // This is the exprOb for a computed property
exprOb = target;
target = linkCtx.data;
if (exprOb._cpCtx) { // Computed value for a contextual parameter
target = exprOb.data; // The data for the contextual view (where contextual param expression evaluated/assigned)
contextCb = exprOb._cpCtx; // Context callback for contextual view
}
while (exprOb && exprOb.sb) { // Step through chained computed values to leaf one...
target = contextCb(exprOb);
exprOb = exprOb.sb;
}
}
$observable(target, async).setProperty(to[1], sourceValue, undefined, to.isCpfn); // 2way binding change event - observably updating bound object
}
}
}
}
}
if (tag) {
tag._.chg = undefined; // Clear marker
return tag;
}
}
function onElemChange(ev) {
var bindId, val,
source = ev.target,
fromAttr = defaultAttr(source),
setter = fnSetters[fromAttr],
rSplitBindings = /&(\d+)\+?/g;
if (!source._jsvTr || ev.delegateTarget !== activeBody && source.type !== "number" || ev.type === "input") {
// If this is an element using trigger, ignore event delegated (bubbled) to activeBody
val = $isFunction(fromAttr)
? fromAttr(source)
: setter
? $(source)[setter]()
: $(source).attr(fromAttr);
source._jsvChg = 1; // // Set 'changing' marker to prevent linkedElem change event triggering its own refresh
while (bindId = rSplitBindings.exec(source._jsvBnd)) {
// _jsvBnd is a string with the syntax: "&bindingId1&bindingId2"
updateValue(val, source._jsvInd, source._jsvElse, undefined, bindId[1], ev);
}
source._jsvChg = undefined; // Clear marker
}
}
function onDataLinkedTagChange(ev, eventArgs) {
// Update or initial rendering of any tag (including {{:}}) whether inline or data-linked element.
var attr, sourceValue, noUpdate, forceUpdate, hasError, onError, bindEarly, tagCtx, l,
linkCtx = this,
linkFn = linkCtx.fn,
tag = linkCtx.tag,
source = linkCtx.data,
target = linkCtx.elem,
cvt = linkCtx.convert,
parentElem = target.parentNode,
view = linkCtx.view,
oldLinkCtx = view._lc,
onEvent = eventArgs && changeHandler(view, onBeforeChangeStr, tag);
if (parentElem && (!onEvent || onEvent.call(tag || 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
view._lc = linkCtx;
if (eventArgs || linkCtx._toLk) {
// If eventArgs are defined, this is a data update
// Otherwise this is the initial data-link rendering call. Bind on this the first time it gets called
linkCtx._toLk = 0; // Remove flag to skip unneccessary rebinding next time
if (linkFn._er) {
// data-link="exprUsingTagOrCvt with onerror=..." - e.g. {tag ... {cvt:... {:... convert='cvt'
try {
sourceValue = linkFn(source, view, $sub); // Compiled link expression
// For data-link="{:xxx}" with no cvt or cvtBk returns value. Otherwise returns tagCtxs
} catch (e) {
hasError = linkFn._er;
onError = onRenderError(e,view,(new Function("data,view", "return " + hasError + ";"))(source, view));
sourceValue = [{props: {}, args: [onError], tag: tag}];
}
} else {
sourceValue = linkFn(source, view, $sub); // Compiled link expression
// For data-link="{:xxx}" with no cvt or cvtBk returns value. Otherwise returns tagCtxs
}
// Compiled link expression for linkTag: return value for data-link="{:xxx}" with no cvt or cvtBk, otherwise tagCtx or tagCtxs
attr = tag && tag.attr || linkCtx.attr || (linkCtx._dfAt = defaultAttr(target, true, cvt !== undefined));
if (linkCtx._dfAt === VALUE && (tag && tag.parentElem || linkCtx.elem).type === CHECKBOX) {
attr = CHECKED; // This is a single checkbox (not in a checkboxgroup - which might have data-link="value{:...}" so linkCtx.attr === VALUE, but would not have _dfAt === VALUE)
}
// For {{: ...}} without a convert or convertBack, (tag and linkFn._tag undefined) we already have the sourceValue, and we are done
if (tag) {
// Existing tag instance
forceUpdate = hasError || tag._er;
// If the new tagCtxs hasError or the previous tagCtxs had error, then force update
sourceValue = sourceValue[0] ? sourceValue : [sourceValue];
// Tag will update unless tag.onUpdate is false or is a function which returns false
noUpdate = !forceUpdate && (tag.onUpdate === false || eventArgs && $isFunction(tag.onUpdate) && tag.onUpdate(ev, eventArgs, sourceValue) === false);
mergeCtxs(tag, sourceValue, forceUpdate); // Merge new tagCtxs (in sourceValue var) with current tagCtxs on tag instance
if (tag._.chg && (attr === HTML || attr === VALUE) || noUpdate || attr === NONE) {
// This is an update coming from the tag itself (linkedElem change), or else onUpdate returned false, or attr === "none"
callAfterLink(tag, ev, eventArgs);
if (!tag._.chg) {
// onUpdate returned false, or attr === "none" - so don't refresh the tag: we just use the new tagCtxs merged
// from the sourceValue (which may optionally have been modifed in onUpdate()...) and then bind, and we are done
observeAndBind(linkCtx, source, target);
}
// Remove dynamically added linkCtx from view
view._lc = oldLinkCtx;
if (eventArgs && (onEvent = changeHandler(view, onAfterChangeStr, tag))) {
onEvent.call(tag || linkCtx, ev, eventArgs);
}
if (tag.tagCtx.props.dataMap) {
tag.tagCtx.props.dataMap.map(tag.tagCtx.args[0], tag.tagCtx, tag.tagCtx.map, isRenderCall || !tag._.bnd);
}
return;
}
if (tag.onUnbind) {
tag.onUnbind(tag.tagCtx, linkCtx, tag.ctx, ev, eventArgs);
}
tag.linkedElems = tag.linkedElem = tag.mainElem = tag.displayElem = undefined;
l = tag.tagCtxs.length;
while (l--) {
tagCtx = tag.tagCtxs[l];
tagCtx.linkedElems = tagCtx.mainElem = tagCtx.displayElem = undefined;
}
sourceValue = tag.tagName === ":" // Call convertVal if it is a {{cvt:...}} - otherwise call renderTag
? $sub._cnvt(tag.convert, view, sourceValue[0]) // convertVal(converter, view, tagCtx, onError)
: $sub._tag(tag, view, view.tmpl, sourceValue, true, onError); // renderTag(tagName, parentView, tmpl, tagCtxs, isUpdate, onError)
} else if (linkFn._tag) {
// 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
? $sub._cnvt(cvt, view, sourceValue[0] || sourceValue) // convertVal(converter, view, tagCtx, onError)
: $sub._tag(linkFn._tag, view, view.tmpl, sourceValue, true, onError); // renderTag(tagName, parentView, tmpl, tagCtxs, isUpdate, onError)
addLinkMethods(tag = linkCtx.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 (bindEarly = tag && (!tag.inline || linkCtx.fn._lr) && tag.template) {
// Data-linked tags with templated contents need to be data-linked before their contents, so that observable updates
// will trigger the parent tags before the child tags.
observeAndBind(linkCtx, source, target);
}
updateContent(sourceValue, linkCtx, attr, tag);
linkCtx._noUpd = 0; // For data-link="^{...}" remove _noUpd flag so updates on subsequent calls
if (tag) {
tag._er = hasError;
callAfterLink(tag, ev, eventArgs);
}
}
if (!bindEarly) {
observeAndBind(linkCtx, source, target);
}
if (tag && tag._.ths) {
// Tag has a this=expr binding for which we have created an additional 'to' (defineBindToDataTargets) target (at index bindTo.length)
// We now have the this pointer, so we push it to the binding, using updateValue(index)
tag.updateValue(tag, tag.bindTo ? tag.bindTo.length : 1); // If bindTo not defined yet, it will be [0], so length 1
}
if (eventArgs && (onEvent = changeHandler(view, onAfterChangeStr, tag))) {
onEvent.call(tag || linkCtx, ev, eventArgs);
}
// Remove dynamically added linkCtx from view
view._lc = oldLinkCtx;
}
}
function setDefer(elem, value) {
elem._df = value; // Use both an expando and an attribute to track deferred tokens. Attribute is needed for querySelectorAll for getViewInfos (childTags)
elem[(value ? "set" : "remove") + "Attribute"](deferAttr, "");
}
function updateContent(sourceValue, linkCtx, attr, tag) {
// When called for a tag, either in tag.refresh() or onDataLinkedTagChange(), returns tag
// When called (in onDataLinkedTagChange) for target HTML returns true
// When called (in onDataLinkedTagChange) for other targets returns boolean for "change"
var setter, prevNode, nextNode, late, nodesToRemove, useProp, tokens, id, openIndex, closeIndex, testElem, nodeName, cStyle, jsvSel,
renders = attr !== NONE && sourceValue !== undefined && !linkCtx._noUpd && !((attr === VALUE || attr === HTML) && (!tag && linkCtx.elem._jsvChg)),
// For data-link="^{...}", don't update the first time (no initial render) - e.g. to leave server rendered values.
source = linkCtx.data,
target = tag && tag.parentElem || linkCtx.elem,
targetParent = target.parentNode,
$target = $(target),
view = linkCtx.view,
targetVal = linkCtx._val,
change = tag;
if (tag) {
// Initialize the tag with element references
tag._.unlinked = true; // Set to unlinked, so initialization is triggered after re-rendering, e.g. for setting linkedElem, and calling onBind
tag.parentElem = tag.parentElem || (linkCtx.expr || tag._elCnt) ? target : targetParent;
prevNode = tag._prv;
nextNode = tag._nxt;
}
if (!renders) {
linkCtx._val = sourceValue;
return;
}
if (attr === "visible") {
attr = "css-display";
}
if (/^css-/.test(attr)) {
if (linkCtx.attr === "visible") {
// Get the current display style
cStyle = (target.currentStyle || getComputedStyle.call(global, target, "")).display;
if (sourceValue) {
// We are showing the element.
// Get the cached 'visible' display value from the -jsvd expando
sourceValue = target._jsvd
// Or, if not yet cached, get the current display value
|| cStyle;
if (sourceValue === NONE && !(sourceValue = displayStyles[nodeName = target.nodeName])) {
// Currently display value is '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);
// Get the default style for this HTML tag to use as 'visible' style
sourceValue
// and cache it as a hash against nodeName
= displayStyles[nodeName]
= (testElem.currentStyle || getComputedStyle.call(global, testElem, "")).display;
document.body.removeChild(testElem);
}
} else {
// We are hiding the element.
// Cache the current display value as 'visible' style, on _jsvd expando, for when we show the element again
target._jsvd = cStyle;
sourceValue = NONE; // Hide the element
}
}
if (change = change || targetVal !== sourceValue) {
$.style(target, attr.slice(4), sourceValue);
}
} else if (attr !== "link") { // attr === "link" is for tag controls which do data binding but have no rendered output or target
if (/^data-/.test(attr)) {
$.data(target, attr.slice(5), sourceValue); // Support for binding to data attributes: data-foo{:expr}: data-foo attribute will be
// expr.toString(), but $.data(element, "foo") and $(element).data("foo") will actually return value of expr, even if of type object
} else if (/^prop-/.test(attr)) {
useProp = true;
attr = attr.slice(5);
} else if (attr === CHECKED) {
useProp = true;
if (target.name && $isArray(sourceValue)) {
target._jsvSel = sourceValue; // Checkbox group (possibly using {{checkboxgroup}} tag) data-linking to array of strings
sourceValue = $inArray(target.value, sourceValue) > -1;
} else {
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()
// 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}" .../>
useProp = true;
attr = CHECKED;
sourceValue = target.value === sourceValue;
// If the data value corresponds to the value attribute of this radio button input, set the checked property to true
// Otherwise set the checked property to false
} else if (attr === "selected" || attr === "disabled" || attr === "multiple" || attr === "readonly") {
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
} else if (attr === VALUE && target.nodeName === "SELECT") {
target._jsvSel = $isArray(sourceValue)
? sourceValue
: "" + sourceValue; // If not array, coerce to string
}
if (setter = fnSetters[attr]) {
if (attr === HTML) {
if (tag && tag.inline) {
nodesToRemove = tag.nodes(true);
if (tag._elCnt) {
if (prevNode && prevNode !== nextNode) { // nextNode !== prevNode
// This prevNode will be removed from the DOM, so transfer the view tokens on prevNode to nextNode of this 'viewToRefresh'
transferViewTokens(prevNode, nextNode, target, tag._tgId, "^", true);
} else {
// nextNode === prevNode, or there is no nextNode and so the target._df may have tokens
tokens = prevNode ? prevNode.getAttribute(jsvAttrStr) : target._df;
id = tag._tgId + "^";
openIndex = tokens.indexOf("#" + id) + 1;
closeIndex = tokens.indexOf("/" + id);
if (openIndex && closeIndex > 0) {
// If prevNode, or target._df, include tokens referencing view and tag bindings contained within the open and close tokens
// of the updated tag control, they need to be processed (disposed)
openIndex += id.length;
if (closeIndex > openIndex) {
disposeTokens(tokens.slice(openIndex, closeIndex)); // Dispose view and tag bindings
tokens = tokens.slice(0, openIndex) + tokens.slice(closeIndex);
if (prevNode) {
prevNode.setAttribute(jsvAttrStr, tokens); // Remove tokens of replaced content
} else if (target._df) { // Remove tokens of replaced content
setDefer(target, tokens);
}
}
}
}
prevNode = prevNode
? prevNode.previousSibling
: nextNode
? nextNode.previousSibling
: target.lastChild;
}
// Remove HTML nodes
$(nodesToRemove).remove(); // Note if !tag._elCnt removing the nodesToRemove will process and dispose view and tag bindings contained within the updated tag control
// Insert and link new content
late = view.link(view.data, target, prevNode, nextNode, sourceValue, tag && {tag: tag._tgId});
} else {
// data-linked value targeting innerHTML: data-link="html{:expr}" or contenteditable="true"
renders = renders && targetVal !== sourceValue;
if (renders) {
$target.empty();
late = view.link(source, target, prevNode, nextNode, sourceValue, tag && {tag: tag._tgId});
}
}
} else if (target._jsvSel) {
$target[setter](sourceValue); // <select> (or multiselect)
} else {
if (change = change || targetVal !== sourceValue) {
if (attr === "text" && target.children && !target.children[0]) {
// This code is faster then $target.text()
target[TEXTCONTENT] = sourceValue === null ? "" : sourceValue;
} else {
$target[setter](sourceValue);
}
}
if ((jsvSel = targetParent._jsvSel) !== undefined
// Setting value of <option> element
&& (attr === VALUE || $target.attr(VALUE) === undefined)) { // Setting value attribute, or setting textContent if attribute is null
// Set/unselect selection based on value set on parent <select>. Works for multiselect too
target.selected = $inArray("" + sourceValue, $isArray(jsvSel) ? jsvSel : [jsvSel]) > -1;
}
}
} else if (change = change || targetVal !== sourceValue) {
// Setting an attribute to undefined should remove the attribute
$target[useProp ? "prop" : "attr"](attr, sourceValue === undefined && !useProp ? null : sourceValue);
}
}
linkCtx._val = sourceValue;
lateLink(late); // Do any deferred linking (lateRender)
return change;
}
function arrayChangeHandler(ev, eventArgs) { // array change handler for 'array' views
var self = this,
onBeforeChange = changeHandler(self, onBeforeChangeStr, self.tag),
onAfterChange = changeHandler(self, onAfterChangeStr, self.tag);
if (!onBeforeChange || onBeforeChange.call(self, 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;
self._.srt = eventArgs.refresh; // true if part of a 'sort' on refresh
switch (action) {
case "insert":
self.addViews(index, items, eventArgs._dly);
break;
case "remove":
self.removeViews(index, items.length, undefined, eventArgs._dly);
break;
case "move":
self.moveViews(eventArgs.oldIndex, index, items.length);
break;
case "refresh":
self._.srt = undefined;
self.fixIndex(0);
// Other cases: (e.g.undefined, for setProperty on observable object) etc. do nothing
}
}
if (onAfterChange) {
onAfterChange.call(self, ev, eventArgs);
}
}
}
//=============================
// Utilities for event handlers
//=============================
function setArrayChangeLink(view) {
// Add/remove arrayChange handler on view
var handler, arrayBinding,
type = view.type, // undefined if view is being removed
data = view.data,
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 is not a boolean, so it is the data-linked tag that 'owns' this array binding - e.g. {^{for...}}
if (type) {
bound._.arrVws[view._.id] = view;
} else {
delete bound._.arrVws[view._.id]; // if view.type is undefined, view is being removed
}
} else if (type && 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.type 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 =
$subSettingsAdvanced._fe[nodeName] // get form element binding settings for input textarea select or optgroup
|| elem.contentEditable === TRUE && {to: HTML, from: HTML}; // Or if contentEditable set to "true" set attr to "html"
return attr
? (to
? ((nodeName === "input" && elem.type === RADIO) // For radio buttons, bind from value, but bind to 'radio' - special value.
? RADIO
: attr.to)
: attr.from)
: to
? linkGetVal ? "text" : HTML // Default innerText for data-link="a.b.c" or data-link="{:a.b.c}" (with or without converters)- 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, 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 the container is empty,
// so prevNode = linkToNode = null. But if prevView._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._.useKey && refresh, view, refresh || index, true);
// Pass in view._.useKey as test for noIteration (which corresponds to when self._.useKey > 0 and self.data is an array)
// Link the new HTML nodes to the data
lateLink(view.link(data, parentNode, prevNode, linkToNode, html, prevView));
}
//=====================
// addBindingMarkers
//=====================
function addBindingMarkers(value, view, tag) {
// 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.
// Added view markers: #m_...VIEW.../m_
// Added tag markers: #m^...TAG..../m^
var id, end;
if (tag) {
// This is a binding marker for a data-linked tag {^{...}}
end = "^`";
addLinkMethods(tag); // This is {^{>...}} or {^{tag ...}}, {{cvt:...} or {^{:...}}, and tag was defined in convertVal or renderTag
id = tag._tgId;
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)!=null?v:""; used in {{:name}} or data-link="name"
+ "/" + id + end;
}
//==============================
// Data-linking and data binding
//==============================
//---------------
// observeAndBind
//---------------
function observeAndBind(linkCtx, source, target) {
var binding, l, k, linkedElem, exprFnDeps, exprOb, prop, propDeps, depends, tagDepends, bindId, linkedElems,
tag = linkCtx.tag,
allowArray = !tag,
cvtBk = linkCtx.convertBack,
handler = linkCtx._hdl;
source = typeof source === "object" && source; // If not an object set to false
if (tag) {
// Use the 'depends' paths set on linkCtx.tag, or on the converter
// - which may have been set on declaration or in events: init, render, onAfterLink etc.
if (depends = tag.convert) {
depends = depends === TRUE ? tag.tagCtx.props.convert : depends;
depends = linkCtx.view.getRsc("converters", depends) || depends;
depends = depends && depends.depends;
depends = depends && $sub._dp(depends, source, handler); // dependsPaths
}
if (tagDepends = tag.tagCtx.props.depends || tag.depends) {
tagDepends = $sub._dp(tagDepends, tag, handler);
depends = depends ? depends.concat(tagDepends) : tagDepends;
}
linkedElems = tag.linkedElems;
}
depends = depends || [];
if (!linkCtx._depends || ("" + linkCtx._depends !== "" + depends)) {
// Only bind the first time, or if the new depends (toString) has changed from when last bound
exprFnDeps = linkCtx.fn.deps.slice(); // Make a copy of the dependency paths for the compiled linkCtx expression - to pass to observe(). In getInnerCb(),
// (and whenever the object is updated, in innerCb), we will set exprOb.ob to the current object returned by that computed expression, for this view.
if (linkCtx._depends) {
bindId = linkCtx._depends.bdId;
// Unobserve previous binding
$observable._apply(1, [source], exprFnDeps, linkCtx._depends, handler, linkCtx._ctxCb, true);
}
if (tag) {
// Add dependency paths for declared boundProps (so no need to write ^myprop=... to get binding) and for linkedProp too if there is one
l = tag.boundProps.length;
while (l--) {
prop = tag.boundProps[l];
k = tag._.bnd.paths.length;
while (k--) { // Iterate across tagCtxs
propDeps = tag._.bnd.paths[k]["_" + prop];
if (propDeps && propDeps.length && propDeps.skp) { // Not already a bound prop ^prop=expression;
exprFnDeps = exprFnDeps.concat(propDeps); // Add dependencies for this prop expression
}
}
}
allowArray = tag.onArrayChange === undefined || tag.onArrayChange === true;
}
l = exprFnDeps.length;
while (l--) {
exprOb = exprFnDeps[l];
if (exprOb._cpfn) {
// This path is an 'exprOb', corresponding to a computed property returning an object. We replace the exprOb by
// a view-binding-specific exprOb instance. The current object will be stored as exprOb.ob.
exprFnDeps[l] = $extend({}, exprOb);
}
}
binding = $observable._apply(
allowArray ? 0 : 1, // 'this' pointer for observeAndBind, used to set allowArray to 1 or 0.
[source],
exprFnDeps, // flatten the paths - to gather all the dependencies across args and bound params
depends,
handler,
linkCtx._ctxCb);
// The binding returned by $observe has a bnd array with the source objects of the individual bindings.
if (!bindId) {
bindId = linkCtx._bndId || "" + bindingKey++;
linkCtx._bndId = undefined;
// Store the binding key on the view and on the element, for disposal when the view is removed
target._jsvBnd = (target._jsvBnd || "") + "&" + bindId;
linkCtx.view._.bnds[bindId] = bindId;
}
binding.elem = target; // The target of all the individual bindings
binding.linkCtx = linkCtx;
binding._tgId = bindId;
depends.bdId = bindId;
linkCtx._depends = depends;
// Store the binding.
bindingStore[bindId] = binding; // Note: If this corresponds to a data-linked tag, we are replacing the
// temporarily stored tag by the stored binding. The tag will now be at binding.linkCtx.tag
if (linkedElems || cvtBk !== undefined || tag && tag.bindTo) {
defineBindToDataTargets(binding, tag, cvtBk);
}
if (linkedElems) {
l = linkedElems.length;
while (l--) {
linkedElem = linkedElems[l];
k = linkedElem && linkedElem.length;
while (k--) {
if (linkedElem[k]._jsvLkEl) {
if (!linkedElem[k]._jsvBnd) {
linkedElem[k]._jsvBnd = "&" + bindId + "+"; // Add a "+" for cloned binding - so removing
// elems with cloned bindings will not remove the 'parent' binding from the bindingStore.
}
} else {
linkedElem[k]._jsvLkEl = tag;
bindLinkedElChange(tag, linkedElem[k]);
linkedElem[k]._jsvBnd = "&" + bindId + "+";
}
}
}
} else if (cvtBk !== undefined) {
bindLinkedElChange(tag, target);
}
if (tag && !tag.inline) {
if (!tag.flow) {
target.setAttribute(jsvAttrStr, (target.getAttribute(jsvAttrStr)||"") + "#" + bindId + "^/" + bindId + "^");
}
tag._tgId = "" + bindId;
}
}
}
//-------
// $.link
//-------
function lateLink(late) {
// Do any deferred linking (lateRender)
var lnkCtx;
if (late) {
while (lnkCtx = late.pop()) {
lnkCtx._hdl();
}
}
}
function tmplLink(to, from, context, noIteration, parentView, prevNode, nextNode) {
return $link(this, to, from, context, noIteration, parentView, prevNode, nextNode);
}
function $link(tmplOrLinkExpr, to, from, context, noIteration, parentView, prevNode, nextNode) {
// When linking from a template, prevNode and nextNode parameters are ignored