vendor: 4,855 bytes, lines 1-131
1/*! jsviews.js v0.9.81 (Beta) single-file version: http://jsviews.com/ */ 2/*! includes JsRender, JsObservable and JsViews - see: http://jsviews.com/#download */ 3 4/* Interactive data-driven views using JsRender templates */ 5 6//<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< JsRender >>>>>>>>>>>>>>>>>>>>>>>>>>>>>> 7/* JsRender: 8 * See http://jsviews.com/#jsrender and http://github.com/BorisMoore/jsrender 9 * Copyright 2016, Boris Moore 10 * Released under the MIT License. 11 */ 12 13//jshint -W018, -W041 14 15(function(factory, global) { 16 // global var is the this object, which is window when running in the usual browser environment 17 var $ = global.jQuery; 18 19 if (typeof exports === "object") { // CommonJS e.g. Browserify 20 module.exports = $ 21 ? factory(global, $) 22 : function($) { // If no global jQuery, take jQuery passed as parameter: require("jsviews")(jQuery) 23 return factory(global, $); 24 }; 25 } else if (typeof define === "function" && define.amd) { // AMD script loader, e.g. RequireJS 26 define(["jquery"], function($) { 27 return factory(global, $); 28 }); // Require jQuery 29 } else { // Browser using plain <script> tag 30 factory(global, false); 31 } 32} ( 33 34// factory (for jsviews.js) 35function(global, $) { 36"use strict"; 37 38//========================== Top-level vars ========================== 39 40// global var is the this object, which is window when running in the usual browser environment 41var setGlobals = $ === false; // Only set globals if script block in browser (not AMD and not CommonJS) 42 43$ = $ || global.jQuery; // $ is jQuery passed in by CommonJS loader (Browserify), or global jQuery. 44 45if (!$ || !$.fn) { 46 // jQuery is not loaded. 47 throw "JsViews requires jQuery"; // We require jQuery 48} 49 50var versionNumber = "v0.9.81", 51 52 jsvStoreName, rTag, rTmplString, topView, $views, $observe, $observable, $expando, 53 54//TODO tmplFnsCache = {}, 55 $isFunction, $isArray, $templates, $converters, $helpers, $tags, $sub, $subSettings, $subSettingsAdvanced, $viewsSettings, delimOpenChar0, delimOpenChar1, delimCloseChar0, delimCloseChar1, linkChar, setting, baseOnError, 56 57 rPath = /^(!*?)(?:null|true|false|\d[\d.]*|([\w$]+|\.|~([\w$]+)|#(view|([\w$]+))?)([\w$.^]*?)(?:[.[^]([\w$]+)\]?)?)$/g, 58 // not object helper view viewProperty pathTokens leafToken 59 60 rParams = /(\()(?=\s*\()|(?:([([])\s*)?(?:(\^?)(!*?[#~]?[\w$.^]+)?\s*((\+\+|--)|\+|-|&&|\|\||===|!==|==|!=|<=|>=|[<>%*:?\/]|(=))\s*|(!*?[#~]?[\w$.^]+)([([])?)|(,\s*)|(\(?)\\?(?:(')|("))|(?:\s*(([)\]])(?=\s*[.^]|\s*$|[^([])|[)\]])([([]?))|(\s+)/g, 61 // lftPrn0 lftPrn bound path operator err eq path2 prn comma lftPrn2 apos quot rtPrn rtPrnDot prn2 space 62 // (left paren? followed by (path? followed by operator) or (path followed by left paren?)) or comma or apos or quot or right paren or space 63 64 isRenderCall, 65 rNewLine = /[ \t]*(\r\n|\n|\r)/g, 66 rUnescapeQuotes = /\\(['"])/g, 67 rEscapeQuotes = /['"\\]/g, // Escape quotes and \ character 68 rBuildHash = /(?:\x08|^)(onerror:)?(?:(~?)(([\w$_\.]+):)?([^\x08]+))\x08(,)?([^\x08]+)/gi, 69 rTestElseIf = /^if\s/, 70 rFirstElem = /<(\w+)[>\s]/, 71 rAttrEncode = /[\x00`><"'&=]/g, // Includes > encoding since rConvertMarkers in JsViews does not skip > characters in attribute strings 72 rIsHtml = /[\x00`><\"'&=]/, 73 rHasHandlers = /^on[A-Z]|^convert(Back)?$/, 74 rWrappedInViewMarker = /^\#\d+_`[\s\S]*\/\d+_`$/, 75 rHtmlEncode = rAttrEncode, 76 viewId = 0, 77 charEntities = { 78 "&": "&", 79 "<": "<", 80 ">": ">", 81 "\x00": "�", 82 "'": "'", 83 '"': """, 84 "`": "`", 85 "=": "=" 86 }, 87 HTML = "html", 88 OBJECT = "object", 89 tmplAttr = "data-jsv-tmpl", 90 jsvTmpl = "jsvTmpl", 91 indexStr = "For #index in nested block use #getIndex().", 92 $render = {}, 93 94 jsr = global.jsrender, 95 jsrToJq = jsr && $ && !$.render, // JsRender already loaded, without jQuery. but we will re-load it now to attach to jQuery 96 97 jsvStores = { 98 template: { 99 compile: compileTmpl 100 }, 101 tag: { 102 compile: compileTag 103 }, 104 viewModel: { 105 compile: compileViewModel 106 }, 107 helper: {}, 108 converter: {} 109 }; 110 111 // views object ($.views if jQuery is loaded, jsrender.views if no jQuery, e.g. in Node.js) 112 $views = { 113 jsviews: versionNumber, 114 sub: { 115 // subscription, e.g. JsViews integration 116 View: View, 117 Err: JsViewsError, 118 tmplFn: tmplFn, 119 parse: parseParams, 120 extend: $extend, 121 extendCtx: extendCtx, 122 syntaxErr: syntaxError, 123 onStore: {}, 124 addSetting: addSetting, 125 settings: { 126 allowCode: false 127 }, 128 advSet: noop, // Update advanced settings 129 _ths: tagHandlersFromProps, 130 _tg: function() {}, // Constructor for tagDef 131 _cnvt: convertVal,
132 _tag: renderTag, 133 _er: error, 134 _err: onRenderError, 135 _html: htmlEncode, 136 _cp: retVal, // Get compiled contextual parameters (or properties) ~foo=expr. In JsRender, simply returns val. 137 _sq: function(token) { 138 if (token === "constructor") { 139 syntaxError(""); 140 } 141 return token; 142 } 143 }, 144 settings: { 145 delimiters: $viewsDelimiters, 146 advanced: function(value) { 147 return value 148 ? ( 149 $extend($subSettingsAdvanced, value), 150 $sub.advSet(), 151 $viewsSettings 152 ) 153 : $subSettingsAdvanced; 154 } 155 }, 156 getCtx: retVal, // Get ctx.foo value. In JsRender, simply returns val. 157 map: dataMap // If jsObservable loaded first, use that definition of dataMap 158 }; 159 160function getDerivedMethod(baseMethod, method) { 161 return function() { 162 var ret, 163 tag = this, 164 prevBase = tag.base; 165 166 tag.base = baseMethod; // Within method call, calling this.base will call the base method 167 ret = method.apply(tag, arguments); // Call the method 168 tag.base = prevBase; // Replace this.base to be the base method of the previous call, for chained calls 169 return ret; 170 }; 171} 172 173function getMethod(baseMethod, method) { 174 // For derived methods (or handlers declared declaratively as in {{:foo onChange=~fooChanged}} replace by a derived method, to allow using this.base(...) 175 // or this.baseApply(arguments) to call the base implementation. (Equivalent to this._super(...) and this._superApply(arguments) in jQuery UI) 176 if ($isFunction(method)) { 177 method = getDerivedMethod( 178 !baseMethod 179 ? noop // no base method implementation, so use noop as base method 180 : baseMethod._d 181 ? baseMethod // baseMethod is a derived method, so us it 182 : getDerivedMethod(noop, baseMethod), // baseMethod is not derived so make its base method be the noop method 183 method 184 ); 185 method._d = 1; // Add flag that this is a derived method 186 } 187 return method; 188} 189 190function tagHandlersFromProps(tag, tagCtx) { 191 for (var prop in tagCtx.props) { 192 if (rHasHandlers.test(prop)) { 193 tag[prop] = getMethod(tag[prop], tagCtx.props[prop]); 194 // Copy over the onFoo props, convert and convertBack from tagCtx.props to tag (overrides values in tagDef). 195 // Note: unsupported scenario: if handlers are dynamically added ^onFoo=expression this will work, but dynamically removing will not work. 196 } 197 } 198} 199 200function retVal(val) { 201 return val; 202} 203 204function noop() { 205 return ""; 206} 207 208function dbgBreak(val) { 209 // Usage examples: {{dbg:...}}, {{:~dbg(...)}}, {{dbg .../}}, {^{for ... onAfterLink=~dbg}} etc. 210 try { 211 console.log("JsRender dbg breakpoint: " + val); 212 throw "dbg breakpoint"; // To break here, stop on caught exceptions. 213 } 214 catch (e) {} 215 return this.base ? this.baseApply(arguments) : val; 216} 217 218function JsViewsError(message) { 219 // Error exception type for JsViews/JsRender 220 // Override of $.views.sub.Error is possible 221 this.name = ($.link ? "JsViews" : "JsRender") + " Error"; 222 this.message = message || this.name; 223} 224 225function $extend(target, source) { 226 for (var name in source) { 227 target[name] = source[name]; 228 } 229 return target; 230} 231 232(JsViewsError.prototype = new Error()).constructor = JsViewsError; 233 234//========================== Top-level functions ========================== 235 236//=================== 237// views.delimiters 238//=================== 239 240function $viewsDelimiters(openChars, closeChars, link) { 241 // Set the tag opening and closing delimiters and 'link' character. Default is "{{", "}}" and "^"
242 // openChars, closeChars: opening and closing strings, each with two characters 243 if (!openChars) { 244 return $subSettings.delimiters; 245 } 246 if ($isArray(openChars)) { 247 return $viewsDelimiters.apply($views, openChars); 248 } 249 250 $subSettings.delimiters = [openChars, closeChars, linkChar = link ? link.charAt(0) : linkChar]; 251 252 delimOpenChar0 = openChars.charAt(0); // Escape the characters - since they could be regex special characters 253 delimOpenChar1 = openChars.charAt(1); 254 delimCloseChar0 = closeChars.charAt(0); 255 delimCloseChar1 = closeChars.charAt(1); 256 openChars = "\\" + delimOpenChar0 + "(\\" + linkChar + ")?\\" + delimOpenChar1; // Default is "{^{" 257 closeChars = "\\" + delimCloseChar0 + "\\" + delimCloseChar1; // Default is "}}" 258 // Build regex with new delimiters 259 // [tag (followed by / space or }) or cvtr+colon or html or code] followed by space+params then convertBack? 260 rTag = "(?:(\\w+(?=[\\/\\s\\" + delimCloseChar0 + "]))|(\\w+)?(:)|(>)|(\\*))\\s*((?:[^\\" 261 + delimCloseChar0 + "]|\\" + delimCloseChar0 + "(?!\\" + delimCloseChar1 + "))*?)"; 262 263 // Make rTag available to JsViews (or other components) for parsing binding expressions 264 $sub.rTag = "(?:" + rTag + ")"; 265 // { ^? { tag+params slash? or closingTag or comment 266 rTag = new RegExp("(?:" + openChars + rTag + "(\\/)?|\\" + delimOpenChar0 + "(\\" + linkChar + ")?\\" + delimOpenChar1 + "(?:(?:\\/(\\w+))\\s*|!--[\\s\\S]*?--))" + closeChars, "g"); 267 268 // Default: bind tagName cvt cln html code params slash bind2 closeBlk comment 269 // /(?:{(\^)?{(?:(\w+(?=[\/\s}]))|(\w+)?(:)|(>)|(\*))\s*((?:[^}]|}(?!}))*?)(\/)?|{(\^)?{(?:(?:\/(\w+))\s*|!--[\s\S]*?--))}} 270 271 $sub.rTmpl = new RegExp("<.*>|([^\\\\]|^)[{}]|" + openChars + ".*" + closeChars); 272 // $sub.rTmpl looks for html tags or { or } char not preceded by \\, or JsRender tags {{xxx}}. Each of these strings are considered 273 // NOT to be jQuery selectors 274 return $viewsSettings; 275} 276 277//========= 278// View.get 279//========= 280 281function getView(inner, type) { //view.get(inner, type) 282 if (!type && inner !== true) { 283 // view.get(type) 284 type = inner; 285 inner = undefined; 286 } 287 288 var views, i, l, found, 289 view = this, 290 root = !type || type === "root"; 291 // If type is undefined, returns root view (view under top view). 292 293 if (inner) { 294 // Go through views - this one, and all nested ones, depth-first - and return first one with given type. 295 // If type is undefined, i.e. view.get(true), return first child view. 296 found = type && view.type === type && view; 297 if (!found) { 298 views = view.views; 299 if (view._.useKey) { 300 for (i in views) { 301 if (found = type ? views[i].get(inner, type) : views[i]) { 302 break; 303 } 304 } 305 } else { 306 for (i = 0, l = views.length; !found && i < l; i++) { 307 found = type ? views[i].get(inner, type) : views[i]; 308 } 309 } 310 } 311 } else if (root) { 312 // Find root view. (view whose parent is top view) 313 while (view.parent) { 314 found = view; 315 view = view.parent; 316 } 317 } else { 318 while (view && !found) { 319 // Go through views - this one, and all parent ones - and return first one with given type. 320 found = view.type === type ? view : undefined; 321 view = view.parent; 322 } 323 } 324 return found; 325} 326 327function getNestedIndex() { 328 var view = this.get("item"); 329 return view ? view.index : undefined; 330} 331 332getNestedIndex.depends = function() { 333 return [this.get("item"), "index"]; 334}; 335 336function getIndex() { 337 return this.index; 338} 339 340getIndex.depends = "index"; 341 342//========== 343// View.hlp 344//========== 345 346function getHelper(helper, isContextCb) { 347 // Helper method called as view.hlp(key) from compiled template, for helpers or template parameters ~foo 348 var wrapped, deps, 349 view = this, 350 res = view.ctx; 351 352 if (res) { 353 res = res[helper]; 354 } 355 if (res === undefined) { 356 res = $helpers[helper]; 357 } 358 if (res && res._cp) { // If this helper resource is a contextual parameter, ~foo=expr
359 if (isContextCb) { // In a context callback for a contextual param, return the [currentData, dependencies...] array - needed for observe call 360 deps = $sub._ceo(res[1].deps); // fn deps, with any exprObs cloned 361 deps.unshift(res[0].data); // view.data 362 deps._cp = true; 363 return deps; 364 } 365 res = $views.getCtx(res); // If a contextual param, but not a context callback, return evaluated param - fn(data, view, j) 366 } 367 368 if (res) { 369 if ($isFunction(res) && !res._wrp) { 370 // If it is of type function, and not already wrapped, we will wrap it, so if called with no this pointer it will be called with the 371 // view as 'this' context. If the helper ~foo() was in a data-link expression, the view will have a 'temporary' linkCtx property too. 372 // Note that helper functions on deeper paths will have specific this pointers, from the preceding path. 373 // For example, ~util.foo() will have the ~util object as 'this' pointer 374 wrapped = function() { 375 return res.apply((!this || this === global) ? view : this, arguments); 376 }; 377 wrapped._wrp = view; 378 $extend(wrapped, res); // Attach same expandos (if any) to the wrapped function 379 } 380 } 381 return wrapped || res; 382} 383 384function getTemplate(tmpl) { 385 return tmpl && (tmpl.fn 386 ? tmpl 387 : this.getRsc("templates", tmpl) || $templates(tmpl)); // not yet compiled 388} 389 390//============== 391// views._cnvt 392//============== 393 394function convertVal(converter, view, tagCtx, onError) { 395 // self is template object or linkCtx object 396 var tag, value, 397 // if tagCtx is an integer, then it is the key for the compiled function to return the boundTag tagCtx 398 boundTag = typeof tagCtx === "number" && view.tmpl.bnds[tagCtx-1], 399 linkCtx = view.linkCtx; // For data-link="{cvt:...}"... 400 401 if (onError !== undefined) { 402 tagCtx = onError = {props: {}, args: [onError]}; 403 } else if (boundTag) { 404 tagCtx = boundTag(view.data, view, $sub); 405 } 406 407 value = tagCtx.args[0]; 408 if (converter || boundTag) { 409 tag = linkCtx && linkCtx.tag; 410 if (!tag) { 411 tag = $extend(new $sub._tg(), { 412 _: { 413 inline: !linkCtx, 414 bnd: boundTag, 415 unlinked: true 416 }, 417 tagName: ":", 418 cvt: converter, 419 flow: true, 420 tagCtx: tagCtx 421 }); 422 if (linkCtx) { 423 linkCtx.tag = tag; 424 tag.linkCtx = linkCtx; 425 } 426 tagCtx.ctx = extendCtx(tagCtx.ctx, (linkCtx ? linkCtx.view : view).ctx); 427 } 428 tag._er = onError && value; 429 tagHandlersFromProps(tag, tagCtx); 430 431 tagCtx.view = view; 432 433 tag.ctx = tagCtx.ctx || tag.ctx || {}; 434 tagCtx.ctx = undefined; 435 436 value = tag.cvtArgs(converter !== "true" && converter)[0]; // If there is a convertBack but no convert, converter will be "true" 437 438 // Call onRender (used by JsViews if present, to add binding annotations around rendered content) 439 value = boundTag && view._.onRender 440 ? view._.onRender(value, view, tag) 441 : value; 442 } 443 return value != undefined ? value : ""; 444} 445 446function convertArgs(converter) { 447 var tag = this, 448 tagCtx = tag.tagCtx, 449 view = tagCtx.view, 450 args = tagCtx.args; 451 452 converter = converter || tag.convert; 453 converter = converter && ("" + converter === converter 454 ? (view.getRsc("converters", converter) || error("Unknown converter: '" + converter + "'")) 455 : converter); 456 457 args = !args.length && !tagCtx.index // On the opening tag with no args, bind to the current data context 458 ? [view.data] 459 : converter 460 ? args.slice() // If there is a converter, use a copy of the tagCtx.args array for rendering, and replace the args[0] in 461 // the copied array with the converted value. But we do not modify the value of tag.tagCtx.args[0] (the original args array) 462 : args; // If no converter, get the original tagCtx.args 463 464 if (converter) { 465 if (converter.depends) { 466 tag.depends = $sub.getDeps(tag.depends, tag, converter.depends, converter); 467 } 468 args[0] = converter.apply(tag, args); 469 } 470 return args; 471} 472 473//============= 474// views._tag 475//============= 476 477function getResource(resourceType, itemName) { 478 var res, store, 479 view = this; 480 while ((res === undefined) && view) { 481 store = view.tmpl && view.tmpl[resourceType]; 482 res = store && store[itemName]; 483 view = view.parent; 484 } 485 return res || $views[resourceType][itemName]; 486} 487 488function renderTag(tagName, parentView, tmpl, tagCtxs, isUpdate, onError) { 489 parentView = parentView || topView; 490 var tag, tag_, tagDef, template, tags, attr, parentTag, i, l, itemRet, tagCtx, tagCtxCtx, content, callInit, mapDef, thisMap, args, props, initialTmpl, tagDataMap, 491 ret = "", 492 linkCtx = parentView.linkCtx || 0, 493 ctx = parentView.ctx, 494 parentTmpl = tmpl || parentView.tmpl, 495 // if tagCtx is an integer, then it is the key for the compiled function to return the boundTag tagCtxs 496 boundTag = typeof tagCtxs === "number" && parentView.tmpl.bnds[tagCtxs-1]; 497 498 if (tagName._is === "tag") { 499 tag = tagName; 500 tagName = tag.tagName; 501 tagCtxs = tag.tagCtxs; 502 template = tag.template; 503 } else { 504 tagDef = parentView.getRsc("tags", tagName) || error("Unknown tag: {{" + tagName + "}} "); 505 template = tagDef.template; 506 } 507 508 if (onError !== undefined) { 509 ret += onError; 510 tagCtxs = onError = [{props: {}, args: []}]; 511 } else if (boundTag) { 512 tagCtxs = boundTag(parentView.data, parentView, $sub); 513 } 514 515 l = tagCtxs.length; 516 for (i = 0; i < l; i++) { 517 tagCtx = tagCtxs[i]; 518 if (!linkCtx || !linkCtx.tag || i && !linkCtx.tag._.inline || tag._er) { 519 // Initialize tagCtx 520 // For block tags, tagCtx.tmpl is an integer > 0 521 if (content = parentTmpl.tmpls && tagCtx.tmpl) { 522 content = tagCtx.content = parentTmpl.tmpls[content - 1]; 523 } 524 tagCtx.index = i; 525 tagCtx.tmpl = content; // Set the tmpl property to the content of the block tag 526 tagCtx.render = renderContent; 527 tagCtx.view = parentView; 528 tagCtx.ctx = extendCtx(tagCtx.ctx, ctx); // Clone and extend parentView.ctx 529 } 530 if (tmpl = tagCtx.props.tmpl) { 531 // If the tmpl property is overridden, set the value (when initializing, or, in case of binding: ^tmpl=..., when updating) 532 tagCtx.tmpl = parentView.getTmpl(tmpl); 533 } 534 535 if (!tag) { 536 // This will only be hit for initial tagCtx (not for {{else}}) - if the tag instance does not exist yet 537 // Instantiate tag if it does not yet exist 538 // If the tag has not already been instantiated, we will create a new instance. 539 // ~tag will access the tag, even within the rendering of the template content of this tag. 540 // From child/descendant tags, can access using ~tag.parent, or ~parentTags.tagName 541 tag = new tagDef._ctr(); 542 callInit = !!tag.init; 543 544 tag.parent = parentTag = ctx && ctx.tag; 545 tag.tagCtxs = tagCtxs; 546 tagDataMap = tag.dataMap; 547 548 if (linkCtx) { 549 tag._.inline = false; 550 linkCtx.tag = tag; 551 tag.linkCtx = linkCtx; 552 } 553 if (tag._.bnd = boundTag || linkCtx.fn) { 554 // Bound if {^{tag...}} or data-link="{tag...}" 555 tag._.arrVws = {}; 556 } else if (tag.dataBoundOnly) { 557 error("{^{" + tagName + "}} tag must be data-bound"); 558 } 559 //TODO better perf for childTags() - keep child tag.tags array, (and remove child, when disposed) 560 // tag.tags = []; 561 } 562 tagCtxs = tag.tagCtxs; 563 tagDataMap = tag.dataMap; 564 565 tagCtx.tag = tag; 566 if (tagDataMap && tagCtxs) { 567 tagCtx.map = tagCtxs[i].map; // Copy over the compiled map instance from the previous tagCtxs to the refreshed ones 568 } 569 if (!tag.flow) { 570 tagCtxCtx = tagCtx.ctx = tagCtx.ctx || {}; 571 572 // tags hash: tag.ctx.tags, merged with parentView.ctx.tags, 573 tags = tag.parents = tagCtxCtx.parentTags = ctx && extendCtx(tagCtxCtx.parentTags, ctx.parentTags) || {}; 574 if (parentTag) { 575 tags[parentTag.tagName] = parentTag; 576 //TODO better perf for childTags: parentTag.tags.push(tag); 577 } 578 tags[tag.tagName] = tagCtxCtx.tag = tag; 579 } 580 } 581 if (!(tag._er = onError)) { 582 tagHandlersFromProps(tag, tagCtxs[0]); 583 tag.rendering = {}; // Provide object for state during render calls to tag and elses. (Used by {{if}} and {{for}}...) 584 for (i = 0; i < l; i++) { 585 tagCtx = tag.tagCtx = tagCtxs[i]; 586 props = tagCtx.props; 587 args = tag.cvtArgs(); 588 589 if (mapDef = props.dataMap || tagDataMap) { 590 if (args.length || props.dataMap) { 591 thisMap = tagCtx.map; 592 if (!thisMap || thisMap.src !== args[0] || isUpdate) { 593 if (thisMap && thisMap.src) { 594 thisMap.unmap();
594 // only called if observable map - not when only used in JsRender, e.g. by {{props}} 595 } 596 thisMap = tagCtx.map = mapDef.map(args[0], props, undefined, !tag._.bnd); 597 } 598 args = [thisMap.tgt]; 599 } 600 } 601 tag.ctx = tagCtx.ctx; 602 603 if (!i) { 604 if (callInit) { 605 initialTmpl = tag.template; 606 tag.init(tagCtx, linkCtx, tag.ctx); 607 callInit = undefined; 608 } 609 if (linkCtx) { 610 // Set attr on linkCtx to ensure outputting to the correct target attribute. 611 // Setting either linkCtx.attr or this.attr in the init() allows per-instance choice of target attrib. 612 linkCtx.attr = tag.attr = linkCtx.attr || tag.attr; 613 } 614 attr = tag.attr; 615 tag._.noVws = attr && attr !== HTML; 616 } 617 618 itemRet = undefined; 619 if (tag.render) { 620 itemRet = tag.render.apply(tag, args); 621 if (parentView.linked && itemRet && tag.linkedElem && !rWrappedInViewMarker.test(itemRet)) { 622 // When a tag renders content from the render method, with data linking, and has a linkedElem binding, then we need to wrap with 623 // view markers, if absent, so the content is a view associated with the tag, which will correctly dispose bindings if deleted. 624 itemRet = renderWithViews($.templates(itemRet), args[0], undefined, undefined, parentView, undefined, undefined, tag); 625 } 626 } 627 if (!args.length) { 628 args = [parentView]; // no arguments - (e.g. {{else}}) get data context from view. 629 } 630 if (itemRet === undefined) { 631 itemRet = tagCtx.render(args[0], true) || (isUpdate ? undefined : ""); 632 } 633 // No return value from render, and no template/content tagCtx.render(...), so return undefined 634 ret = ret ? ret + (itemRet || "") : itemRet; // If no rendered content, this will be undefined 635 } 636 tag.rendering = undefined; 637 } 638 tag.tagCtx = tagCtxs[0]; 639 tag.ctx = tag.tagCtx.ctx; 640 641 if (tag._.noVws) { 642 if (tag._.inline) { 643 // inline tag with attr set to "text" will insert HTML-encoded content - as if it was element-based innerText 644 ret = attr === "text" 645 ? $converters.html(ret) 646 : ""; 647 } 648 } 649 return boundTag && parentView._.onRender 650 // Call onRender (used by JsViews if present, to add binding annotations around rendered content) 651 ? parentView._.onRender(ret, parentView, tag) 652 : ret; 653} 654 655//================= 656// View constructor 657//================= 658 659function View(context, type, parentView, data, template, key, onRender, contentTmpl) { 660 // Constructor for view object in view hierarchy. (Augmented by JsViews if JsViews is loaded) 661 var views, parentView_, tag, self_, 662 self = this, 663 isArray = type === "array"; 664 665 self.content = contentTmpl; 666 self.views = isArray ? [] : {}; 667 self.parent = parentView; 668 self.type = type || "top"; 669 self.data = data; 670 self.tmpl = template; 671 // If the data is an array, this is an 'array view' with a views array for each child 'item view' 672 // If the data is not an array, this is an 'item view' with a views 'hash' object for any child nested views 673 // ._.useKey is non zero if is not an 'array view' (owning a data array). Use this as next key for adding to child views hash 674 self_ = self._ = { 675 key: 0, 676 useKey: isArray ? 0 : 1, 677 id: "" + viewId++, 678 onRender: onRender, 679 bnds: {} 680 }; 681 self.linked = !!onRender; 682 if (parentView) { 683 views = parentView.views; 684 parentView_ = parentView._; 685 if (parentView_.useKey) { 686 // Parent is not an 'array view'. Add this view to its views object 687 // self._key = is the key in the parent view hash 688 views[self_.key = "_" + parentView_.useKey++] = self; 689 self.index = indexStr; 690 self.getIndex = getNestedIndex; 691 } else if (views.length === (self_.key = self.index = key)) { // Parent is an 'array view'. Add this view to its views array 692 views.push(self); // Adding to end of views array. (Using push when possible - better perf than splice) 693 } else { 694 views.splice(key, 0, self); // Inserting in views array 695 } 696 // If no context was passed in, use parent context 697 // If context was passed in, it should have been merged already with parent context 698 self.ctx = context || parentView.ctx; 699 } else { 700 self.ctx = context; 701 } 702} 703 704View.prototype = { 705 get: getView, 706 getIndex: getIndex, 707 getRsc: getResource, 708 getTmpl: getTemplate, 709 hlp: getHelper, 710 _is: "view" 711}; 712 713//==================================================== 714// Registration 715//==================================================== 716 717function compileChildResources(parentTmpl) { 718 var storeName, storeNames, resources; 719 for (storeName in jsvStores) { 720 storeNames = storeName + "s"; 721 if (parentTmpl[storeNames]) { 722 resources = parentTmpl[storeNames]; // Resources not yet compiled 723 parentTmpl[storeNames] = {}; // Remove uncompiled resources 724 $views[storeNames](resources, parentTmpl); // Add back in the compiled resources 725 } 726 } 727} 728 729//=============== 730// compileTag 731//=============== 732 733function compileTag(name, tagDef, parentTmpl) { 734 var tmpl, baseTag, prop, 735 compiledDef = new $sub._tg(); 736 737 function Tag() { 738 var tag = this; 739 tag._ = { 740 inline: true, 741 unlinked: true 742 }; 743 744 tag.tagName = name; 745 } 746 747 if ($isFunction(tagDef)) { 748 // Simple tag declared as function. No presenter instantation. 749 tagDef = { 750 depends: tagDef.depends, 751 render: tagDef 752 }; 753 } else if ("" + tagDef === tagDef) { 754 tagDef = {template: tagDef}; 755 } 756 if (baseTag = tagDef.baseTag) { 757 tagDef.flow = !!tagDef.flow; // Set flow property, so defaults to false even if baseTag has flow=true 758 tagDef.baseTag = baseTag = "" + baseTag === baseTag 759 ? (parentTmpl && parentTmpl.tags[baseTag] || $tags[baseTag]) 760 : baseTag; 761 762 compiledDef = $extend(compiledDef, baseTag); 763 764 for (prop in tagDef) { 765 compiledDef[prop] = getMethod(baseTag[prop], tagDef[prop]); 766 } 767 } else { 768 compiledDef = $extend(compiledDef, tagDef); 769 } 770 771 // Tag declared as object, used as the prototype for tag instantiation (control/presenter) 772 if ((tmpl = compiledDef.template) !== undefined) { 773 compiledDef.template = "" + tmpl === tmpl ? ($templates[tmpl] || $templates(tmpl)) : tmpl; 774 } 775 if (compiledDef.init !== false) { 776 // Set init: false on tagDef if you want to provide just a render method, or render and template, but no constructor or prototype. 777 // so equivalent to setting tag to render function, except you can also provide a template. 778 (Tag.prototype = compiledDef).constructor = compiledDef._ctr = Tag; 779 } 780 781 if (parentTmpl) { 782 compiledDef._parentTmpl = parentTmpl; 783 } 784 return compiledDef; 785} 786 787function baseApply(args) { 788 // In derived method (or handler declared declaratively as in {{:foo onCh
788ange=~fooChanged}} can call base method, 789 // using this.baseApply(arguments) (Equivalent to this._superApply(arguments) in jQuery UI) 790 return this.base.apply(this, args); 791} 792 793//=============== 794// compileTmpl 795//=============== 796 797function compileTmpl(name, tmpl, parentTmpl, options) { 798 // tmpl is either a template object, a selector for a template script block, the name of a compiled template, or a template object 799 800 //==== nested functions ==== 801 function lookupTemplate(value) { 802 // If value is of type string - treat as selector, or name of compiled template 803 // Return the template object, if already compiled, or the markup string 804 var currentName, tmpl; 805 if (("" + value === value) || value.nodeType > 0 && (elem = value)) { 806 if (!elem) { 807 if (/^\.\/[^\\:*?"<>]*$/.test(value)) { 808 // tmpl="./some/file.html" 809 // If the template is not named, use "./some/file.html" as name. 810 if (tmpl = $templates[name = name || value]) { 811 value = tmpl; 812 } else { 813 // BROWSER-SPECIFIC CODE (not on Node.js): 814 // Look for server-generated script block with id "./some/file.html" 815 elem = document.getElementById(value); 816 } 817 } else if ($.fn && !$sub.rTmpl.test(value)) { 818 try { 819 elem = $(document).find(value)[0]; // if jQuery is loaded, test for selector returning elements, and get first element 820 } catch (e) {} 821 }// END BROWSER-SPECIFIC CODE 822 } //BROWSER-SPECIFIC CODE 823 if (elem) { 824 // Generally this is a script element. 825 // However we allow it to be any element, so you can for example take the content of a div, 826 // use it as a template, and replace it by the same content rendered against data. 827 // e.g. for linking the content of a div to a container, and using the initial content as template: 828 // $.link("#content", model, {tmpl: "#content"}); 829 if (options) { 830 // We will compile a new template using the markup in the script element 831 value = elem.innerHTML; 832 } else { 833 // We will cache a single copy of the compiled template, and associate it with the name 834 // (renaming from a previous name if there was one). 835 currentName = elem.getAttribute(tmplAttr); 836 if (currentName) { 837 if (currentName !== jsvTmpl) { 838 value = $templates[currentName]; 839 delete $templates[currentName]; 840 } else if ($.fn) { 841 value = $.data(elem)[jsvTmpl]; 842 } 843 } else { 844 name = name || ($.fn ? jsvTmpl : value); 845 value = compileTmpl(name, elem.innerHTML, parentTmpl, options); 846 } 847 value.tmplName = name = name || currentName; 848 if (name !== jsvTmpl) { 849 $templates[name] = value; 850 } 851 elem.setAttribute(tmplAttr, name); 852 if ($.fn) { 853 $.data(elem, jsvTmpl, value); 854 } 855 } 856 } // END BROWSER-SPECIFIC CODE 857 elem = undefined; 858 } else if (!value.fn) { 859 value = undefined; 860 // If value is not a string. HTML element, or compiled template, return undefined 861 } 862 return value; 863 } 864 865 var elem, compiledTmpl, 866 tmplOrMarkup = tmpl = tmpl || ""; 867 868 //==== Compile the template ==== 869 if (options === 0) { 870 options = undefined; 871 tmplOrMarkup = lookupTemplate(tmplOrMarkup); // Top-level compile so do a template lookup 872 } 873 874 // If options, then this was already compiled from a (script) element template declaration. 875 // If not, then if tmpl is a template object, use it for options 876 options = options || (tmpl.markup ? tmpl : {}); 877 options.tmplName = name; 878 if (parentTmpl) { 879 options._parentTmpl = parentTmpl; 880 } 881 // If tmpl is not a markup string or a selector string, then it must be a template object 882 // In that case, get it from the markup property of the object 883 if (!tmplOrMarkup && tmpl.markup && (tmplOrMarkup = lookupTemplate(tmpl.markup))) { 884 if (tmplOrMarkup.fn) { 885 // If the string references a compiled template object, need to recompile to merge any modified options 886 tmplOrMarkup = tmplOrMarkup.markup; 887 } 888 } 889 if (tmplOrMarkup !== undefined) { 890 if (tmplOrMarkup.fn || tmpl.fn) { 891 // tmpl is already compiled, so use it 892 if (tmplOrMarkup.fn) { 893 compiledTmpl = tmplOrMarkup; 894 } 895 } else { 896 // tmplOrMarkup is a markup string, not a compiled template 897 // Create template object 898 tmpl = tmplObject(tmplOrMarkup, options); 899 // Compile to AST and then to compiled function 900 tmplFn(tmplOrMarkup.replace(rEscapeQuotes, "\\$&"), tmpl); 901 } 902 if (!compiledTmpl) { 903 compiledTmpl = $extend(function() { 904 return compiledTmpl.render.apply(compiledTmpl, arguments); 905 }, tmpl); 906 907 compileChildResources(compiledTmpl); 908 } 909 if (name && !parentTmpl && name !== jsvTmpl) { 910 $render[name] = compiledTmpl; 911 } 912 return compiledTmpl; 913 } 914} 915 916//==== /end of function compileTmpl ==== 917 918//================= 919// compileViewModel 920//================= 921 922function getDefaultVal(defaultVal, data) { 923 return $.isFunction(defaultVal) 924 ? defaultVal.call(data) 925 : defaultVal; 926} 927 928function unmapArray(modelArr) { 929 var i, arr = [], 930 l = modelArr.length; 931 for (i=0; i<l; i++) { 932 arr.push(modelArr[i].unmap()); 933 } 934 return arr; 935} 936 937function compileViewModel(name, type) { 938 var i, constructor, 939 viewModels = this, 940 getters = type.getters, 941 extend = type.extend, 942 id = type.id, 943 proto = $.extend({ 944 _is: name || "unnamed", 945 unmap: unmap, 946 merge: merge 947 }, extend), 948 args = "", 949 body = "", 950 l = getters ? getters.length : 0, 951 $observable = $.observable, 952 getterNames = {}; 953 954 function GetNew(args) { 955 constructor.apply(this, args); 956 } 957 958 function vm() { 959 return new GetNew(arguments); 960 } 961 962 function iterate(data, action) { 963 var j, getterType, defaultVal, prop, ob, 964 m = getters.length; 965 for (j=0; j<m; j++) { 966 prop = getters[j]; 967 getterType = undefined; 968 if (prop + "" !== prop) { 969 getterType = prop; 970 prop = getterType.getter; 971 } 972 if ((ob = data[prop]) === undefined && getterType && (defaultVal = getterType.defaultVal) !== undefined) { 973 ob = getDefaultVal(defaultVal, data); 974 }
975 action(ob, getterType && viewModels[getterType.type], prop); 976 } 977 } 978 979 function map(data) { 980 data = data + "" === data 981 ? JSON.parse(data) // Accept JSON string 982 : data; // or object/array 983 var i, j, l, m, prop, 984 ob = data, 985 arr = []; 986 987 if ($isArray(data)) { 988 data = data || []; 989 l = data.length; 990 for (i=0; i<l; i++) { 991 arr.push(this.map(data[i])); 992 } 993 arr._is = name; 994 arr.unmap = unmap; 995 arr.merge = merge; 996 return arr; 997 } 998 999 if (data) { 1000 iterate(data, function(ob, viewModel) { 1001 if (viewModel) { // Iterate to build getters arg array (value, or mapped value) 1002 ob = viewModel.map(ob); 1003 } 1004 arr.push(ob); 1005 }); 1006 1007 ob = this.apply(this, arr); // Insantiate this View Model, passing getters args array to constructor 1008 for (prop in data) { // Copy over any other properties. that are not get/set properties 1009 if (prop !== $expando && !getterNames[prop]) { 1010 ob[prop] = data[prop]; 1011 } 1012 } 1013 } 1014 return ob; 1015 } 1016 1017 function merge(data) { 1018 data = data + "" === data 1019 ? JSON.parse(data) // Accept JSON string 1020 : data; // or object/array 1021 var i, j, l, m, prop, mod, found, assigned, ob, newModArr, 1022 model = this; 1023 1024 if ($isArray(model)) { 1025 assigned = {}; 1026 newModArr = []; 1027 l = data.length; 1028 m = model.length; 1029 for (i=0; i<l; i++) { 1030 ob = data[i]; 1031 found = false; 1032 for (j=0; j<m && !found; j++) { 1033 if (assigned[j]) { 1034 continue; 1035 } 1036 mod = model[j]; 1037 1038 if (id) { 1039 assigned[j] = found = id + "" === id 1040 ? (ob[id] && (getterNames[id] ? mod[id]() : mod[id]) === ob[id]) 1041 : id(mod, ob); 1042 } 1043 } 1044 if (found) { 1045 mod.merge(ob); 1046 newModArr.push(mod); 1047 } else { 1048 newModArr.push(vm.map(ob)); 1049 } 1050 } 1051 if ($observable) { 1052 $observable(model).refresh(newModArr, true); 1053 } else { 1054 model.splice.apply(model, [0, model.length].concat(newModArr)); 1055 } 1056 return; 1057 } 1058 iterate(data, function(ob, viewModel, getter) { 1059 if (viewModel) { 1060 model[getter]().merge(ob); // Update typed property 1061 } else { 1062 model[getter](ob); // Update non-typed property 1063 } 1064 }); 1065 for (prop in data) { 1066 if (prop !== $expando && !getterNames[prop]) { 1067 model[prop] = data[prop]; 1068 } 1069 } 1070 } 1071 1072 function unmap() { 1073 var ob, prop, i, l, getterType, arr, value, 1074 model = this; 1075 1076 if ($isArray(model)) { 1077 return unmapArray(model); 1078 } 1079 ob = {}; 1080 l = getters.length; 1081 for (i=0; i<l; i++) { 1082 prop = getters[i]; 1083 getterType = undefined; 1084 if (prop + "" !== prop) { 1085 getterType = prop; 1086 prop = getterType.getter; 1087 } 1088 value = model[prop](); 1089 ob[prop] = getterType && value && viewModels[getterType.type] 1090 ? $isArray(value) 1091 ? unmapArray(value) 1092 : value.unmap() 1093 : value; 1094 } 1095 for (prop in model) { 1096 if (prop !== "_is" && !getterNames[prop] && prop !== $expando && (prop.charAt(0) !== "_" || !getterNames[prop.slice(1)]) && !$.isFunction(model[prop])) { 1097 ob[prop] = model[prop]; 1098 } 1099 } 1100 return ob; 1101 } 1102 1103 GetNew.prototype = proto; 1104 1105 for (i=0; i<l; i++) { 1106 (function(getter) { 1107 getter = getter.getter || getter; 1108 getterNames[getter] = i+1; 1109 var privField = "_" + getter; 1110 1111 args += (args ? "," : "") + getter; 1112 body += "this." + privField + " = " + getter + ";\n"; 1113 proto[getter] = proto[getter] || function(val) { 1114 if (!arguments.length) { 1115 return this[privField]; // If there is no argument, use as a getter 1116 } 1117 if ($observable) { 1118 $observable(this).setProperty(getter, val); 1119 } else { 1120 this[privField] = val; 1121 } 1122 }; 1123 1124 if ($observable) { 1125 proto[getter].set = proto[getter].set || function(val) { 1126 this[privField] = val; // Setter called by observable property change 1127 }; 1128 } 1129 })(getters[i]); 1130 } 1131 1132 constructor = new Function(args, body.slice(0, -1)); 1133 constructor.prototype = proto; 1134 proto.constructor = constructor; 1135 1136 vm.map = map; 1137 vm.getters = getters; 1138 vm.extend = extend; 1139 vm.id = id; 1140 return vm; 1141} 1142 1143function tmplObject(markup, options) {
1144 // Template object constructor 1145 var htmlTag, 1146 wrapMap = $subSettingsAdvanced._wm || {}, // Only used in JsViews. Otherwise empty: {} 1147 tmpl = $extend( 1148 { 1149 tmpls: [], 1150 links: {}, // Compiled functions for link expressions 1151 bnds: [], 1152 _is: "template", 1153 render: renderContent 1154 }, 1155 options 1156 ); 1157 1158 tmpl.markup = markup; 1159 if (!options.htmlTag) { 1160 // Set tmpl.tag to the top-level HTML tag used in the template, if any... 1161 htmlTag = rFirstElem.exec(markup); 1162 tmpl.htmlTag = htmlTag ? htmlTag[1].toLowerCase() : ""; 1163 } 1164 htmlTag = wrapMap[tmpl.htmlTag]; 1165 if (htmlTag && htmlTag !== wrapMap.div) { 1166 // When using JsViews, we trim templates which are inserted into HTML contexts where text nodes are not rendered (i.e. not 'Phrasing Content'). 1167 // Currently not trimmed for <li> tag. (Not worth adding perf cost) 1168 tmpl.markup = $.trim(tmpl.markup); 1169 } 1170 1171 return tmpl; 1172} 1173 1174//============== 1175// registerStore 1176//============== 1177 1178function registerStore(storeName, storeSettings) { 1179 1180 function theStore(name, item, parentTmpl) { 1181 // The store is also the function used to add items to the store. e.g. $.templates, or $.views.tags 1182 1183 // For store of name 'thing', Call as: 1184 // $.views.things(items[, parentTmpl]), 1185 // or $.views.things(name, item[, parentTmpl]) 1186 1187 var onStore, compile, itemName, thisStore; 1188 if (name && typeof name === OBJECT && !name.nodeType && !name.markup && !name.getTgt && !(storeName === "viewModel" && name.getters || name.extend)) { 1189 // Call to $.views.things(items[, parentTmpl]), 1190 1191 // Adding items to the store 1192 // If name is a hash, then item is parentTmpl. Iterate over hash and call store for key. 1193 for (itemName in name) { 1194 theStore(itemName, name[itemName], item); 1195 } 1196 return item || $views; 1197 } 1198 // Adding a single unnamed item to the store 1199 if (item === undefined) { 1200 item = name; 1201 name = undefined; 1202 } 1203 if (name && "" + name !== name) { // name must be a string 1204 parentTmpl = item; 1205 item = name; 1206 name = undefined; 1207 } 1208 thisStore = parentTmpl 1209 ? storeName === "viewModel" 1210 ? parentTmpl 1211 : (parentTmpl[storeNames] = parentTmpl[storeNames] || {}) 1212 : theStore; 1213 compile = storeSettings.compile; 1214 if (item === null) { 1215 // If item is null, delete this entry 1216 if (name) { 1217 delete thisStore[name]; 1218 } 1219 } else { 1220 item = compile ? compile.call(thisStore, name, item, parentTmpl, 0) : item; 1221 if (name) { 1222 thisStore[name] = item; 1223 } 1224 } 1225 if (compile && item) { 1226 item._is = storeName;
vendor: 5,508 bytes, lines 1226-1391
1226 // Only do this for compiled objects (tags, templates...) 1227 } 1228 if (item && (onStore = $sub.onStore[storeName])) { 1229 // e.g. JsViews integration 1230 onStore(name, item, compile); 1231 } 1232 return item; 1233 } 1234 1235 var storeNames = storeName + "s"; 1236 1237 $views[storeNames] = theStore; 1238} 1239 1240function addSetting(st) { 1241 $viewsSettings[st] = function(value) { 1242 return arguments.length 1243 ? ($subSettings[st] = value, $viewsSettings) 1244 : $subSettings[st]; 1245 }; 1246} 1247 1248//========= 1249// dataMap 1250//========= 1251 1252function dataMap(mapDef) { 1253 function Map(source, options) { 1254 this.tgt = mapDef.getTgt(source, options); 1255 } 1256 1257 if ($isFunction(mapDef)) { 1258 // Simple map declared as function 1259 mapDef = { 1260 getTgt: mapDef 1261 }; 1262 } 1263 1264 if (mapDef.baseMap) { 1265 mapDef = $extend($extend({}, mapDef.baseMap), mapDef); 1266 } 1267 1268 mapDef.map = function(source, options) { 1269 return new Map(source, options); 1270 }; 1271 return mapDef; 1272} 1273 1274//============== 1275// renderContent 1276//============== 1277 1278function renderContent(data, context, noIteration, parentView, key, onRender) { 1279 var i, l, tag, tmpl, tagCtx, isTopRenderCall, prevData, prevIndex, 1280 view = parentView, 1281 result = ""; 1282 1283 if (context === true) { 1284 noIteration = context; // passing boolean as second param - noIteration 1285 context = undefined; 1286 } else if (typeof context !== OBJECT) { 1287 context = undefined; // context must be a boolean (noIteration) or a plain object 1288 } 1289 1290 if (tag = this.tag) { 1291 // This is a call from renderTag or tagCtx.render(...) 1292 tagCtx = this; 1293 view = view || tagCtx.view; 1294 tmpl = view.getTmpl(tag.template || tagCtx.tmpl); 1295 if (!arguments.length) { 1296 data = view; 1297 } 1298 } else { 1299 // This is a template.render(...) call 1300 tmpl = this; 1301 } 1302 1303 if (tmpl) { 1304 if (!view && data && data._is === "view") { 1305 view = data; // When passing in a view to render or link (and not passing in a parent view) use the passed-in view as parentView 1306 } 1307 1308 if (view) { 1309 if (data === view) { 1310 // Inherit the data from the parent view. 1311 // This may be the contents of an {{if}} block 1312 data = view.data; 1313 } 1314 } 1315 1316 isTopRenderCall = !view; 1317 isRenderCall = isRenderCall || isTopRenderCall; 1318 if (!view) { 1319 (context = context || {}).root = data; // Provide ~root as shortcut to top-level data. 1320 } 1321 if (!isRenderCall || $subSettingsAdvanced.useViews || tmpl.useViews || view && view !== topView) { 1322 result = renderWithViews(tmpl, data, context, noIteration, view, key, onRender, tag); 1323 } else { 1324 if (view) { // In a block 1325 prevData = view.data; 1326 prevIndex = view.index; 1327 view.index = indexStr; 1328 } else { 1329 view = topView; 1330 view.data = data; 1331 view.ctx = context; 1332 } 1333 if ($isArray(data) && !noIteration) { 1334 // Create a view for the array, whose child views correspond to each data item. (Note: if key and parentView are passed in 1335 // along with parent view, treat as insert -e.g. from view.addViews - so parentView is already the view item for array) 1336 for (i = 0, l = data.length; i < l; i++) { 1337 view.index = i; 1338 view.data = data[i]; 1339 result += tmpl.fn(data[i], view, $sub); 1340 } 1341 } else { 1342 view.data = data; 1343 result += tmpl.fn(data, view, $sub); 1344 } 1345 view.data = prevData; 1346 view.index = prevIndex; 1347 } 1348 if (isTopRenderCall) { 1349 isRenderCall = undefined; 1350 } 1351 } 1352 return result; 1353} 1354 1355function renderWithViews(tmpl, data, context, noIteration, view, key, onRender, tag) { 1356 function setItemVar(item) { 1357 // When itemVar is specified, set modified ctx with user-named ~item 1358 newCtx = $extend({}, context); 1359 newCtx[itemVar] = item; 1360 } 1361 1362 // Render template against data as a tree of subviews (nested rendered template instances), or as a string (top-level template). 1363 // If the data is the parent view, treat as noIteration, re-render with the same data context. 1364 var i, l, newView, childView, itemResult, swapContent, contentTmpl, outerOnRender, tmplName, itemVar, newCtx, tagCtx, 1365 result = ""; 1366 1367 if (tag) { 1368 // This is a call from renderTag or tagCtx.render(...) 1369 tmplName = tag.tagName; 1370 tagCtx = tag.tagCtx; 1371 context = context ? extendCtx(context, tag.ctx) : tag.ctx; 1372 1373 if (tmpl === view.content) { // {{xxx tmpl=#content}} 1374 contentTmpl = tmpl !== view.ctx._wrp // We are rendering the #content 1375 ? view.ctx._wrp // #content was the tagCtx.props.tmpl wrapper of the block content - so within this view, #content will now be the view.ctx._wrp block content 1376 : undefined; // #content was the view.ctx._wrp block content - so within this view, there is no longer any #content to wrap. 1377 } else if (tmpl !== tagCtx.content) { 1378 if (tmpl === tag.template) { // Rendering {{tag}} tag.template, replacing block content. 1379 contentTmpl = tagCtx.tmpl; // Set #content to block content (or wrapped block content if tagCtx.props.tmpl is set) 1380 context._wrp = tagCtx.content; // Pass wrapped block content to nested views 1381 } else { // Rendering tagCtx.props.tmpl wrapper 1382 contentTmpl = tagCtx.content || view.content; // Set #content to wrapped block content 1383 } 1384 } else { 1385 contentTmpl = view.content; // Nested views inherit same wrapped #content property 1386 } 1387 1388 if (tagCtx.props.link === false) { 1389 // link=false setting on block tag 1390 // We will override inherited value of link by the explicit setting link=false taken from props 1391 // The child views of an unlinked view are also unlinked. So setting child back to true will not have any effect.
1392 context = context || {}; 1393 context.link = false; 1394 } 1395 1396 if (itemVar = tagCtx.props.itemVar) { 1397 if (itemVar.charAt(0) !== "~") { 1398 syntaxError("Use itemVar='~myItem'"); 1399 } 1400 itemVar = itemVar.slice(1); 1401 } 1402 } 1403 1404 if (view) { 1405 onRender = onRender || view._.onRender; 1406 context = extendCtx(context, view.ctx); 1407 } 1408 1409 if (key === true) { 1410 swapContent = true; 1411 key = 0; 1412 } 1413 1414 // If link===false, do not call onRender, so no data-linking marker nodes 1415 if (onRender && (context && context.link === false || tag && tag._.noVws)) { 1416 onRender = undefined; 1417 } 1418 outerOnRender = onRender; 1419 if (onRender === true) { 1420 // Used by view.refresh(). Don't create a new wrapper view. 1421 outerOnRender = undefined; 1422 onRender = view._.onRender; 1423 } 1424 // Set additional context on views created here, (as modified context inherited from the parent, and to be inherited by child views) 1425 context = tmpl.helpers 1426 ? extendCtx(tmpl.helpers, context) 1427 : context; 1428 1429 newCtx = context; 1430 if ($isArray(data) && !noIteration) { 1431 // Create a view for the array, whose child views correspond to each data item. (Note: if key and view are passed in 1432 // along with parent view, treat as insert -e.g. from view.addViews - so view is already the view item for array) 1433 newView = swapContent 1434 ? view 1435 : (key !== undefined && view) 1436 || new View(context, "array", view, data, tmpl, key, onRender); 1437 if (view && view._.useKey) { 1438 // Parent is not an 'array view' 1439 newView._.bnd = !tag || tag._.bnd && tag; // For array views that are data bound for collection change events, set the 1440 // view._.bnd property to true for top-level link() or data-link="{for}", or to the tag instance for a data-bound tag, e.g. {^{for ...}} 1441 } 1442 if (itemVar) { 1443 newView.it = itemVar; 1444 } 1445 itemVar = newView.it; 1446 for (i = 0, l = data.length; i < l; i++) { 1447 // Create a view for each data item. 1448 if (itemVar) { 1449 setItemVar(data[i]); // use modified ctx with user-named ~item 1450 } 1451 childView = new View(newCtx, "item", newView, data[i], tmpl, (key || 0) + i, onRender, contentTmpl); 1452 1453 itemResult = tmpl.fn(data[i], childView, $sub); 1454 result += newView._.onRender ? newView._.onRender(itemResult, childView) : itemResult; 1455 } 1456 } else { 1457 // Create a view for singleton data object. The type of the view will be the tag name, e.g. "if" or "myTag" except for 1458 // "item", "array" and "data" views. A "data" view is from programmatic render(object) against a 'singleton'. 1459 if (itemVar) { 1460 setItemVar(data); 1461 } 1462 newView = swapContent ? view : new View(newCtx, tmplName || "data", view, data, tmpl, key, onRender, contentTmpl); 1463 if (tag && !tag.flow) { 1464 newView.tag = tag; 1465 } 1466 result += tmpl.fn(data, newView, $sub); 1467 } 1468 return outerOnRender ? outerOnRender(result, newView) : result; 1469} 1470 1471//=========================== 1472// Build and compile template 1473//=========================== 1474 1475// Generate a reusable function that will serve to render a template against data 1476// (Compile AST then build template function) 1477 1478function onRenderError(e, view, fallback) { 1479 var message = fallback !== undefined 1480 ? $isFunction(fallback) 1481 ? fallback.call(view.data, e, view) 1482 : fallback || "" 1483 : "{Error: " + e.message + "}"; 1484 1485 if ($subSettings.onError && (fallback = $subSettings.onError.call(view.data, e, fallback && message, view)) !== undefined) { 1486 message = fallback; // There is a settings.debugMode(handler) onError override. Call it, and use return value (if any) to replace message 1487 } 1488 1489 return view && !view.linkCtx ? $converters.html(message) : message; 1490} 1491 1492function error(message) { 1493 throw new $sub.Err(message); 1494} 1495 1496function syntaxError(message) { 1497 error("Syntax error\n" + message); 1498} 1499 1500function tmplFn(markup, tmpl, isLinkExpr, convertBack, hasElse) { 1501 // Compile markup to AST (abtract syntax tree) then build the template function code from the AST nodes 1502 // Used for compiling templates, and also by JsViews to build functions for data link expressions 1503 1504 //==== nested functions ==== 1505 function pushprecedingContent(shift) { 1506 shift -= loc; 1507 if (shift) { 1508 content.push(markup.substr(loc, shift).replace(rNewLine, "\\n")); 1509 } 1510 } 1511 1512 function blockTagCheck(tagName, block) { 1513 if (tagName) { 1514 tagName += '}}'; 1515 // '{{include}} block has {{/for}} with no open {{for}}' 1516 syntaxError(( 1517 block 1518 ? '{{' + block + '}} block has {{/' + tagName + ' without {{' + tagName
vendor: 7,344 bytes, lines 1519-1744
1519 : 'Unmatched or missing {{/' + tagName) + ', in template:\n' + markup); 1520 } 1521 } 1522 1523 function parseTag(all, bind, tagName, converter, colon, html, codeTag, params, slash, bind2, closeBlock, index) { 1524/* 1525 1526 bind tagName cvt cln html code params slash bind2 closeBlk comment 1527/(?:{(\^)?{(?:(\w+(?=[\/\s}]))|(\w+)?(:)|(>)|(\*))\s*((?:[^}]|}(?!}))*?)(\/)?|{(\^)?{(?:(?:\/(\w+))\s*|!--[\s\S]*?--))}}/g 1528 1529(?: 1530 {(\^)?{ bind 1531 (?: 1532 (\w+ tagName 1533 (?=[\/\s}]) 1534 ) 1535 | 1536 (\w+)?(:) converter colon 1537 | 1538 (>) html 1539 | 1540 (\*) codeTag 1541 ) 1542 \s* 1543 ( params 1544 (?:[^}]|}(?!}))*? 1545 ) 1546 (\/)? slash 1547 | 1548 {(\^)?{ bind2 1549 (?: 1550 (?:\/(\w+))\s* closeBlock 1551 | 1552 !--[\s\S]*?-- comment 1553 ) 1554) 1555}}/g 1556 1557*/ 1558 if (codeTag && bind || slash && !tagName || params && params.slice(-1) === ":" || bind2) { 1559 syntaxError(all); 1560 } 1561 1562 // Build abstract syntax tree (AST): [tagName, converter, params, content, hash, bindings, contentMarkup] 1563 if (html) { 1564 colon = ":"; 1565 converter = HTML; 1566 } 1567 slash = slash || isLinkExpr && !hasElse; 1568 1569 var pathBindings = (bind || isLinkExpr) && [[]], 1570 props = "", 1571 args = "", 1572 ctxProps = "", 1573 paramsArgs = "", 1574 paramsProps = "", 1575 paramsCtxProps = "", 1576 onError = "", 1577 useTrigger = "", 1578 // Block tag if not self-closing and not {{:}} or {{>}} (special case) and not a data-link expression 1579 block = !slash && !colon; 1580 1581 //==== nested helper function ==== 1582 tagName = tagName || (params = params || "#data", colon); // {{:}} is equivalent to {{:#data}} 1583 pushprecedingContent(index); 1584 loc = index + all.length; // location marker - parsed up to here 1585 if (codeTag) { 1586 if (allowCode) { 1587 content.push(["*", "\n" + params.replace(/^:/, "ret+= ").replace(rUnescapeQuotes, "$1") + ";\n"]); 1588 } 1589 } else if (tagName) { 1590 if (tagName === "else") { 1591 if (rTestElseIf.test(params)) { 1592 syntaxError('for "{{else if expr}}" use "{{else expr}}"'); 1593 } 1594 pathBindings = current[7] && [[]]; 1595 current[8] = markup.substring(current[8], index); // contentMarkup for block tag 1596 current = stack.pop(); 1597 content = current[2]; 1598 block = true; 1599 } 1600 if (params) { 1601 // remove newlines from the params string, to avoid compiled code errors for unterminated strings 1602 parseParams(params.replace(rNewLine, " "), pathBindings, tmpl) 1603 .replace(rBuildHash, function(all, onerror, isCtx, key, keyToken, keyValue, arg, param) { 1604 key = "'" + keyToken + "':"; 1605 if (arg) { 1606 args += keyValue + ","; 1607 paramsArgs += "'" + param + "',"; 1608 } else if (isCtx) { 1609 ctxProps += key + 'j._cp(' + keyValue + ',"' + param + '",view),'; 1610 // Compiled code for evaluating tagCtx on a tag will have: ctx:{'foo':j._cp(compiledExpr, "expr", view)} 1611 paramsCtxProps += key + "'" + param + "',"; 1612 } else if (onerror) { 1613 onError += keyValue; 1614 } else { 1615 if (keyToken === "trigger") { 1616 useTrigger += keyValue; 1617 } 1618 props += key + keyValue + ","; 1619 paramsProps += key + "'" + param + "',"; 1620 hasHandlers = hasHandlers || rHasHandlers.test(keyToken); 1621 } 1622 return ""; 1623 }).slice(0, -1); 1624 } 1625 1626 if (pathBindings && pathBindings[0]) { 1627 pathBindings.pop(); // Remove the bindings that was prepared for next arg. (There is always an extra one ready). 1628 } 1629 1630 newNode = [ 1631 tagName, 1632 converter || !!convertBack || hasHandlers || "", 1633 block && [], 1634 parsedParam(paramsArgs || (tagName === ":" ? "'#data'," : ""), paramsProps, paramsCtxProps), // {{:}} equivalent to {{:#data}} 1635 parsedParam(args || (tagName === ":" ? "data," : ""), props, ctxProps), 1636 onError, 1637 useTrigger, 1638 pathBindings || 0 1639 ]; 1640 content.push(newNode); 1641 if (block) { 1642 stack.push(current); 1643 current = newNode; 1644 current[8] = loc; // Store current location of open tag, to be able to add contentMarkup when we reach closing tag 1645 } 1646 } else if (closeBlock) { 1647 blockTagCheck(closeBlock !== current[0] && current[0] !== "else" && closeBlock, current[0]); 1648 current[8] = markup.substring(current[8], index); // contentMarkup for block tag 1649 current = stack.pop(); 1650 } 1651 blockTagCheck(!current && closeBlock); 1652 content = current[2]; 1653 } 1654 //==== /end of nested functions ==== 1655 1656 var i, result, newNode, hasHandlers, bindings, 1657 allowCode = $subSettings.allowCode || tmpl && tmpl.allowCode 1658 || $viewsSettings.allowCode === true, // include direct setting of settings.allowCode true for backward compat only 1659 astTop = [], 1660 loc = 0, 1661 stack = [], 1662 content = astTop, 1663 current = [,,astTop]; 1664 1665 if (allowCode && tmpl._is) { 1666 tmpl.allowCode = allowCode; 1667 } 1668 1669//TODO result = tmplFnsCache[markup]; // Only cache if template is not named and markup length < ..., 1670//and there are no bindings or subtemplates?? Consider standard optimization for data-link="a.b.c" 1671// if (result) { 1672// tmpl.fn = result; 1673// } else { 1674 1675// result = markup; 1676 if (isLinkExpr) { 1677 if (convertBack !== undefined) { 1678 markup = markup.slice(0, -convertBack.length - 2) + delimCloseChar0; 1679 } 1680 markup = delimOpenChar0 + markup + delimCloseChar1; 1681 } 1682 1683 blockTagCheck(stack[0] && stack[0][2].pop()[0]); 1684 // Build the AST (abstract syntax tree) under astTop 1685 markup.replace(rTag, parseTag); 1686 1687 pushprecedingContent(markup.length); 1688 1689 if (loc = astTop[astTop.length - 1]) { 1690 blockTagCheck("" + loc !== loc && (+loc[8] === loc[8]) && loc[0]); 1691 } 1692// result = tmplFnsCache[markup] = buildCode(astTop, tmpl); 1693// } 1694 1695 if (isLinkExpr) { 1696 result = buildCode(astTop, markup, isLinkExpr); 1697 bindings = []; 1698 i = astTop.length; 1699 while (i--) { 1700 bindings.unshift(astTop[i][7]); // With data-link expressions, pathBindings array for tagCtx[i] is astTop[i][7] 1701 } 1702 setPaths(result, bindings); 1703 } else { 1704 result = buildCode(astTop, tmpl); 1705 } 1706 return result; 1707} 1708 1709function setPaths(fn, pathsArr) { 1710 var key, paths, 1711 i = 0, 1712 l = pathsArr.length; 1713 fn.deps = []; 1714 for (; i < l; i++) { 1715 paths = pathsArr[i]; 1716 for (key in paths) { 1717 if (key !== "_jsvto" && paths.hasOwnProperty(key) && paths[key].length) { 1718 fn.deps = fn.deps.concat(paths[key]); // deps is the concatenation of the paths arrays for the different bindings 1719 } 1720 } 1721 } 1722 fn.paths = paths; // The array of paths arrays for the different bindings 1723} 1724 1725function parsedParam(args, props, ctx) { 1726 return [args.slice(0, -1), props.slice(0, -1), ctx.slice(0, -1)]; 1727} 1728 1729function paramStructure(parts, type) { 1730 return '\n\t' 1731 + (type 1732 ? type + ':{' 1733 : '') 1734 + 'args:[' + parts[0] + ']' 1735 + (parts[1] || !type 1736 ? ',\n\tprops:{' + parts[1] + '}' 1737 : "") 1738 + (parts[2] ? ',\n\tctx:{' + parts[2] + '}' : ""); 1739} 1740 1741function parseParams(params, pathBindings, tmpl) { 1742 1743 function parseTokens(all, lftPrn0, lftPrn, bound, path, operator, err, eq, path2, prn, comma, lftPrn2, apos, quot, rtPrn, rtPrnDot, prn2, space, index, full) { 1744 // /(\()(?=\s*\()|(?:([([])\s*)?(?:(\^?)(!*?[#~]?[\w$.^]+)?\s*((\+\+|--)|\+|-|&&|\|\||===|!==|==|!=|<=|>=|[<>%*:?\/]|(=))\s*|(!*?[#~]?[\w$.^]+)([([])?)|(,\s*)|(\(?)\\?(?:(')|("))|(?:\s*(([)\]])(?
1744=\s*[.^]|\s*$|[^([])|[)\]])([([]?))|(\s+)/g, 1745 // lftPrn0 lftPrn bound path operator err eq path2 prn comma lftPrn2 apos quot rtPrn rtPrnDot prn2 space 1746 // (left paren? followed by (path? followed by operator) or (path followed by paren?)) or comma or apos or quot or right paren or space 1747 function parsePath(allPath, not, object, helper, view, viewProperty, pathTokens, leafToken) { 1748 //rPath = /^(!*?)(?:null|true|false|\d[\d.]*|([\w$]+|\.|~([\w$]+)|#(view|([\w$]+))?)([\w$.^]*?)(?:[.[^]([\w$]+)\]?)?)$/g, 1749 // not object helper view viewProperty pathTokens leafToken 1750 var subPath = object === "."; 1751 if (object) { 1752 path = path.slice(not.length); 1753 if (/^\.?constructor$/.test(leafToken||path)) { 1754 syntaxError(allPath); 1755 } 1756 if (!subPath) { 1757 allPath = (helper 1758 ? 'view.hlp("' + helper + '")' 1759 : view 1760 ? "view" 1761 : "data") 1762 + (leafToken 1763 ? (viewProperty 1764 ? "." + viewProperty 1765 : helper 1766 ? "" 1767 : (view ? "" : "." + object) 1768 ) + (pathTokens || "") 1769 : (leafToken = helper ? "" : view ? viewProperty || "" : object, "")); 1770 1771 allPath = allPath + (leafToken ? "." + leafToken : ""); 1772 1773 allPath = not + (allPath.slice(0, 9) === "view.data" 1774 ? allPath.slice(5) // convert #view.data... to data... 1775 : allPath); 1776 } 1777 if (bindings) { 1778 binds = named === "linkTo" ? (bindto = pathBindings._jsvto = pathBindings._jsvto || []) : bndCtx.bd; 1779 if (theOb = subPath && binds[binds.length-1]) { 1780 if (theOb._jsv) { 1781 while (theOb.sb) { 1782 theOb = theOb.sb; 1783 } 1784 if (theOb.bnd) { 1785 path = "^" + path.slice(1); 1786 } 1787 theOb.sb = path; 1788 theOb.bnd = theOb.bnd || path.charAt(0) === "^"; 1789 } 1790 } else { 1791 binds.push(path); 1792 } 1793 pathStart[parenDepth] = index + (subPath ? 1 : 0); 1794 } 1795 } 1796 return allPath; 1797 } 1798 1799 //bound = bindings && bound; 1800 if (bound && !eq) { 1801 path = bound + path; // e.g. some.fn(...)^some.path - so here path is "^some.path" 1802 } 1803 operator = operator || ""; 1804 lftPrn = lftPrn || lftPrn0 || lftPrn2; 1805 path = path || path2; 1806 // Could do this - but not worth perf cost?? :- 1807 // if (!path.lastIndexOf("#data.", 0)) { path = path.slice(6); } // If path starts with "#data.", remove that. 1808 prn = prn || prn2 || ""; 1809 1810 var expr, exprFn, binds, theOb, newOb, 1811 rtSq = ")"; 1812 1813 if (prn === "[") { 1814 prn ="[j._sq("; 1815 rtSq = ")]"; 1816 } 1817 1818 if (err && !aposed && !quoted) { 1819 syntaxError(params); 1820 } else { 1821 if (bindings && rtPrnDot && !aposed && !quoted) { 1822 // This is a binding to a path in which an object is returned by a helper/data function/expression, e.g. foo()^x.y or (a?b:c)^x.y 1823 // We create a compiled function to get the object instance (which will be called when the dependent data of the subexpression changes, to return the new object, and trigger re-binding of the subsequent path) 1824 if (!named || boundName || bindto) { 1825 expr = pathStart[parenDepth - 1]; 1826 if (full.length - 1 > index - (expr || 0)) { // We need to compile a subexpression 1827 expr = full.slice(expr, index + all.length); 1828 if (exprFn !== true) { // If not reentrant call during compilation 1829 binds = bindto || bndStack[parenDepth-1].bd; 1830 // Insert exprOb object, to be used during binding to return the computed object 1831 theOb = binds[binds.length-1]; 1832 if (theOb && theOb.prm) { 1833 while (theOb.sb && theOb.sb.prm) { 1834 theOb = theOb.sb; 1835 } 1836 newOb = theOb.sb = {path: theOb.sb, bnd: theOb.bnd}; 1837 } else { 1838 binds.push(newOb = {path: binds.pop()}); // Insert exprOb object, to be used during binding to return the computed object 1839 } // (e.g. "some.object()" in "some.object().a.b" - to be used as context for binding the following tokens "a.b") 1840 } 1841 rtPrnDot = delimOpenChar1 + ":" + expr // The parameter or function subexpression 1842 + " onerror=''" // set onerror='' in order to wrap generated code with a try catch - returning '' as object instance if there is an error/missing parent 1843 + delimCloseChar0; 1844 exprFn = tmplLinks[rtPrnDot]; 1845 if (!exprFn) { 1846 tmplLinks[rtPrnDot] = true; // Flag that this exprFn (for rtPrnDot) is being compiled 1847 tmplLinks[rtPrnDot] = exprFn = tmplFn(rtPrnDot, tmpl, true); // Compile the expression (or use cached copy already in tmpl.links) 1848 } 1849 if (exprFn !== true && newOb) { 1850 // If not reentrant call during compilation 1851 newOb._jsv = exprFn; 1852 newOb.prm = bndCtx.bd; 1853 newOb.bnd = newOb.bnd || newOb.path && newOb.path.indexOf("^") >= 0; 1854 } 1855 } 1856 } 1857 } 1858 return (aposed 1859 // within single-quoted string
1860 ? (aposed = !apos, (aposed ? all : lftPrn2 + '"')) 1861 : quoted 1862 // within double-quoted string 1863 ? (quoted = !quot, (quoted ? all : lftPrn2 + '"')) 1864 : 1865 ( 1866 (lftPrn 1867 ? (pathStart[parenDepth] = index++, bndCtx = bndStack[++parenDepth] = {bd: []}, lftPrn) 1868 : "") 1869 + (space 1870 ? (parenDepth 1871 ? "" 1872 // New arg or prop - so insert backspace \b (\x08) as separator for named params, used subsequently by rBuildHash, and prepare new bindings array 1873 : (paramIndex = full.slice(paramIndex, index), named 1874 ? (named = boundName = bindto = false, "\b") 1875 : "\b,") + paramIndex + (paramIndex = index + all.length, bindings && pathBindings.push(bndCtx.bd = []), "\b") 1876 ) 1877 : eq 1878 // named param. Remove bindings for arg and create instead bindings array for prop 1879 ? (parenDepth && syntaxError(params), bindings && pathBindings.pop(), named = path, boundName = bound, paramIndex = index + all.length, bound && (bindings = bndCtx.bd = pathBindings[named] = []), path + ':') 1880 : path 1881 // path 1882 ? (path.split("^").join(".").replace(rPath, parsePath) 1883 + (prn 1884 // some.fncall( 1885 ? (bndCtx = bndStack[++parenDepth] = {bd: []}, fnCall[parenDepth] = rtSq, prn) 1886 : operator) 1887 ) 1888 : operator 1889 // operator 1890 ? operator 1891 : rtPrn 1892 // function 1893 ? ((rtPrn = fnCall[parenDepth] || rtPrn, fnCall[parenDepth] = false, bndCtx = bndStack[--parenDepth], rtPrn) 1894 + (prn // rtPrn and prn, e.g )( in (a)() or a()(), or )[ in a()[] 1895 ? (bndCtx = bndStack[++parenDepth], fnCall[parenDepth] = rtSq, prn) 1896 : "") 1897 ) 1898 : comma 1899 ? (fnCall[parenDepth] || syntaxError(params), ",") // We don't allow top-level literal arrays or objects 1900 : lftPrn0 1901 ? "" 1902 : (aposed = apos, quoted = quot, '"') 1903 )) 1904 ); 1905 } 1906 } 1907 1908 var named, bindto, boundName, 1909 quoted, // boolean for string content in double quotes 1910 aposed, // or in single quotes 1911 bindings = pathBindings && pathBindings[0], // bindings array for the first arg 1912 bndCtx = {bd: bindings}, 1913 bndStack = {0: bndCtx}, 1914 paramIndex = 0, // list, 1915 tmplLinks = (tmpl ? tmpl.links : bindings && (bindings.links = bindings.links || {})) || topView.tmpl.links, 1916 // The following are used for tracking path parsing including nested paths, such as "a.b(c^d + (e))^f", and chained computed paths such as 1917 // "a.b().c^d().e.f().g" - which has four chained paths, "a.b()", "^c.d()", ".e.f()" and ".g" 1918 parenDepth = 0, 1919 fnCall = {}, // We are in a function call 1920 pathStart = {}, // tracks the start of the current path such as c^d() in the above example 1921 result = (params + (tmpl ? " " : "")).replace(rParams, parseTokens); 1922 1923 return !parenDepth && result || syntaxError(params); // Syntax error if unbalanced parens in params expression 1924} 1925 1926function buildCode(ast, tmpl, isLinkExpr) { 1927 // Build the template function code from the AST nodes, and set as property on the passed-in template object 1928 // Used for compiling templates, and also by JsViews to build functions for data link expressions 1929 var i, node, tagName, converter, tagCtx, hasTag, hasEncoder, getsVal, hasCnvt, useCnvt, tmplBindings, pathBindings, params, boundOnErrStart, boundOnErrEnd, 1930 tagRender, nestedTmpls, tmplName, nestedTmpl, tagAndElses, content, markup, nextIsElse, oldCode, isElse, isGetVal, tagCtxFn, onError, tagStart, trigger, 1931 tmplBindingKey = 0, 1932 useViews = $subSettingsAdvanced.useViews || tmpl.useViews || tmpl.tags || tmpl.templates || tmpl.helpers || tmpl.converters, 1933 code = "", 1934 tmplOptions = {}, 1935 l = ast.length; 1936 1937 if ("" + tmpl === tmpl) { 1938 tmplName = isLinkExpr ? 'data-link="' + tmpl.replace(rNewLine, " ").slice(1, -1) + '"' : tmpl; 1939 tmpl = 0; 1940 } else { 1941 tmplName = tmpl.tmplName || "unnamed"; 1942 if (tmpl.allowCode) { 1943 tmplOptions.allowCode = true; 1944 } 1945 if (tmpl.debug) { 1946 tmplOptions.debug = true; 1947 } 1948 tmplBindings = tmpl.bnds; 1949 nestedTmpls = tmpl.tmpls; 1950 } 1951 for (i = 0; i < l; i++) { 1952 // AST nodes: [0: tagName, 1: converter, 2: content, 3: params, 4: code, 5: onError, 6: trigger, 7:pathBindings, 8: contentMarkup] 1953 node = ast[i]; 1954 1955 // Add newline for each callout to t() c() etc. and each markup string 1956 if ("" + node === node) { 1957 // a markup string to be inserted 1958 code += '\n+"' + node + '"'; 1959 } else { 1960 // a compiled tag expression to be inserted 1961 tagName = node[0]; 1962 if (tagName === "*") { 1963 // Code tag: {{* }} 1964 code += ";\n" + node[1] + "\nret=ret"; 1965 } else { 1966 converter = node[1]; 1967 content = !isLinkExpr && node[2]; 1968 tagCtx = paramStructure(node[3], 'params') + '},' + paramStructure(params = node[4]); 1969 onError = node[5]; 1970 trigger = node[6]; 1971 markup = node[8] && node[8].replace(rUnescapeQuotes, "$1"); 1972 if (isElse = tagName === "else") { 1973 if (pathBindings) { 1974 pathBindings.push(node[7]); 1975 } 1976 } else { 1977 tmplBindingKey = 0;
vendor: 4,177 bytes, lines 1978-2052
1978// if (tmplBindings && (pathBindings = node[7])) { // Array of paths, or false if not data-bound 1979// pathBindings = [pathBindings]; 1980// tmplBindingKey = tmplBindings.push(1); // Add placeholder in tmplBindings for compiled function 1981// } 1982 if (tmplBindings && (pathBindings = node[7])) { // Array of paths, or false if not data-bound 1983 pathBindings = [pathBindings]; 1984 tmplBindingKey = tmplBindings.push(1); // Add placeholder in tmplBindings for compiled function 1985 tmplBindingKey = tmplBindings.length; 1986 } 1987 } 1988 useViews = useViews || params[1] || params[2] || pathBindings || /view.(?!index)/.test(params[0]); 1989 // useViews is for perf optimization. For render() we only use views if necessary - for the more advanced scenarios. 1990 // We use views if there are props, contextual properties or args with #... (other than #index) - but you can force 1991 // using the full view infrastructure, (and pay a perf price) by opting in: Set useViews: true on the template, manually... 1992 if (isGetVal = tagName === ":") { 1993 if (converter) { 1994 tagName = converter === HTML ? ">" : converter + tagName; 1995 } 1996 } else { 1997 if (content) { // TODO optimize - if content.length === 0 or if there is a tmpl="..." specified - set content to null / don't run this compilation code - since content won't get used!! 1998 // Create template object for nested template 1999 nestedTmpl = tmplObject(markup, tmplOptions); 2000 nestedTmpl.tmplName = tmplName + "/" + tagName; 2001 // Compile to AST and then to compiled function 2002 nestedTmpl.useViews = nestedTmpl.useViews || useViews; 2003 buildCode(content, nestedTmpl); 2004 useViews = nestedTmpl.useViews; 2005 nestedTmpls.push(nestedTmpl); 2006 } 2007 2008 if (!isElse) { 2009 // This is not an else tag. 2010 tagAndElses = tagName; 2011 useViews = useViews || tagName && (!$tags[tagName] || !$tags[tagName].flow); 2012 // Switch to a new code string for this bound tag (and its elses, if it has any) - for returning the tagCtxs array 2013 oldCode = code; 2014 code = ""; 2015 } 2016 nextIsElse = ast[i + 1]; 2017 nextIsElse = nextIsElse && nextIsElse[0] === "else"; 2018 } 2019 tagStart = onError ? ";\ntry{\nret+=" : "\n+"; 2020 boundOnErrStart = ""; 2021 boundOnErrEnd = ""; 2022 2023 if (isGetVal && (pathBindings || trigger || converter && converter !== HTML)) { 2024 // For convertVal we need a compiled function to return the new tagCtx(s) 2025 tagCtxFn = new Function("data,view,j,u", " // " + tmplName + " " + tmplBindingKey + " " + tagName 2026 + "\nreturn {" + tagCtx + "};"); 2027 tagCtxFn._er = onError; 2028 tagCtxFn._tag = tagName; 2029 2030 if (isLinkExpr) { 2031 return tagCtxFn; 2032 } 2033 2034 setPaths(tagCtxFn, pathBindings); 2035 tagRender = 'c("' + converter + '",view,'; 2036 useCnvt = true; 2037 boundOnErrStart = tagRender + tmplBindingKey + ","; 2038 boundOnErrEnd = ")"; 2039 } 2040 code += (isGetVal 2041 ? (isLinkExpr ? (onError ? "try{\n" : "") + "return " : tagStart) + (useCnvt // Call _cnvt if there is a converter: {{cnvt: ... }} or {^{cnvt: ... }} 2042 ? (useCnvt = undefined, useViews = hasCnvt = true, tagRender + (pathBindings 2043 ? ((tmplBindings[tmplBindingKey - 1] = tagCtxFn), tmplBindingKey) // Store the compiled tagCtxFn in tmpl.bnds, and pass the key to convertVal() 2044 : "{" + tagCtx + "}") + ")") 2045 : tagName === ">" 2046 ? (hasEncoder = true, "h(" + params[0] + ")") 2047 : (getsVal = true, "((v=" + params[0] + ')!=null?v:' + (isLinkExpr ? 'null)' : '"")')) 2048 // Non strict equality so data-link="title{:expr}" with expr=null/undefined removes title attribute 2049 ) 2050 : (hasTag = true, "\n{view:view,tmpl:" // Add this tagCtx to the compiled code for the tagCtxs to be passed to renderTag() 2051 + (content ? nestedTmpls.length : "0") + "," // For block tags, pass in the key (nestedTmpls.length) to the nested content template 2052 + tagCtx + "},"));
2053 2054 if (tagAndElses && !nextIsElse) { 2055 // This is a data-link expression or an inline tag without any elses, or the last {{else}} of an inline tag 2056 // We complete the code for returning the tagCtxs array 2057 code = "[" + code.slice(0, -1) + "]"; 2058 tagRender = 't("' + tagAndElses + '",view,this,'; 2059 if (isLinkExpr || pathBindings) { 2060 // This is a bound tag (data-link expression or inline bound tag {^{tag ...}}) so we store a compiled tagCtxs function in tmp.bnds 2061 code = new Function("data,view,j,u", " // " + tmplName + " " + tmplBindingKey + " " + tagAndElses + "\nreturn " + code + ";"); 2062 code._er = onError; 2063 code._tag = tagAndElses; 2064 if (pathBindings) { 2065 setPaths(tmplBindings[tmplBindingKey - 1] = code, pathBindings); 2066 } 2067 if (isLinkExpr) { 2068 return code; // For a data-link expression we return the compiled tagCtxs function 2069 } 2070 boundOnErrStart = tagRender + tmplBindingKey + ",undefined,"; 2071 boundOnErrEnd = ")"; 2072 } 2073 2074 // This is the last {{else}} for an inline tag. 2075 // For a bound tag, pass the tagCtxs fn lookup key to renderTag. 2076 // For an unbound tag, include the code directly for evaluating tagCtxs array 2077 code = oldCode + tagStart + tagRender + (tmplBindingKey || code) + ")"; 2078 pathBindings = 0; 2079 tagAndElses = 0; 2080 } 2081 if (onError) { 2082 useViews = true; 2083 code += ';\n}catch(e){ret' + (isLinkExpr ? "urn " : "+=") + boundOnErrStart + 'j._err(e,view,' + onError + ')' + boundOnErrEnd + ';}' + (isLinkExpr ? "" : 'ret=ret'); 2084 } 2085 } 2086 } 2087 } 2088 // Include only the var references that are needed in the code 2089 code = "// " + tmplName 2090 2091 + "\nvar v" 2092 + (hasTag ? ",t=j._tag" : "") // has tag 2093 + (hasCnvt ? ",c=j._cnvt" : "") // converter 2094 + (hasEncoder ? ",h=j._html" : "") // html converter 2095 + (isLinkExpr ? ";\n" : ',ret=""\n') 2096 + (tmplOptions.debug ? "debugger;" : "") 2097 + code 2098 + (isLinkExpr ? "\n" : ";\nreturn ret;"); 2099 2100 if ($subSettings.debugMode !== false) { 2101 code = "try {\n" + code + "\n}catch(e){\nreturn j._err(e, view);\n}"; 2102 } 2103 2104 try { 2105 code = new Function("data,view,j,u", code); 2106 } catch (e) { 2107 syntaxError("Compiled template code:\n\n" + code + '\n: "' + e.message + '"'); 2108 } 2109 if (tmpl) { 2110 tmpl.fn = code; 2111 tmpl.useViews = !!useViews; 2112 } 2113 return code; 2114} 2115 2116//========== 2117// Utilities 2118//========== 2119 2120// Merge objects, in particular contexts which inherit from parent contexts 2121function extendCtx(context, parentContext) { 2122 // Return copy of parentContext, unless context is defined and is different, in which case return a new merged context 2123 // If neither context nor parentContext are defined, return undefined 2124 return context && context !== parentContext 2125 ? (parentContext 2126 ? $extend($extend({}, parentContext), context) 2127 : context) 2128 : parentContext && $extend({}, parentContext); 2129} 2130 2131// Get character entity for HTML and Attribute encoding 2132function getCharEntity(ch) { 2133 return charEntities[ch] || (charEntities[ch] = "&#" + ch.charCodeAt(0) + ";"); 2134} 2135 2136function getTargetProps(source) { 2137 // this pointer is theMap - which has tagCtx.props too 2138 // arguments: tagCtx.args. 2139 var key, prop, 2140 props = []; 2141 2142 if (typeof source === OBJECT) { 2143 for (key in source) { 2144 prop = source[key]; 2145 if (key !== $expando && source.hasOwnProperty(key) && !$isFunction(prop)) { 2146 props.push({key: key, prop: prop}); 2147 } 2148 } 2149 } 2150 return props; 2151} 2152 2153function $fnRender(data, context, noIteration) { 2154 var tmplElem = this.jquery && (this[0] || error('Unknown template: "' + this.selector + '"')), 2155 tmpl = tmplElem.getAttribute(tmplAttr); 2156 2157 return renderContent.call(tmpl ? $.data(tmplElem)[jsvTmpl] : $templates(tmplElem), data, context, noIteration); 2158} 2159 2160//========================== Register converters ========================== 2161 2162function htmlEncode(text) { 2163 // HTML encode: Replace < > & ' and " by corresponding entities. 2164 return text != undefined ? rIsHtml.test(text) && ("" + text).replace(rHtmlEncode, getCharEntity) || text : ""; 2165} 2166 2167//========================== Initialize ========================== 2168 2169$sub = $views.sub; 2170$viewsSettings = $views.settings; 2171 2172if (!(jsr || $ && $.render)) { 2173 // JsRender not already loaded, or loaded without jQuery, and we are now moving from jsrender namespace to jQuery namepace 2174 for (jsvStoreName in jsvStores) { 2175 registerStore(jsvStoreName, jsvStores[jsvStoreName]); 2176 } 2177 2178 $converters = $views.converters; 2179 $helpers = $views.helpers; 2180 $tags = $views.tags; 2181 2182 $sub._tg.prototype = { 2183 baseApply: baseApply, 2184 cvtArgs: convertArgs 2185 }; 2186 2187 topView = $sub.topView = new View(); 2188 2189 //BROWSER-SPECIFIC CODE 2190 if ($) { 2191 2192 //////////////////////////////////////////////////////////////////////////////////////////////// 2193 // jQuery (= $) is loaded 2194 2195 $.fn.render = $fnRender; 2196 $expando = $.expando; 2197 if ($.observable) { 2198 $extend($sub, $.views.sub); // jquery.observable.js was loaded before jsrender.js 2199 $views.map = $.views.map; 2200 } 2201 2202 } else { 2203 //////////////////////////////////////////////////////////////////////////////////////////////// 2204 // jQuery is not loaded. 2205 2206 $ = {}; 2207 2208 if (setGlobals) { 2209 global.jsrender = $; // We are loading jsrender.js from a script element, not AMD or CommonJS, so set global 2210 } 2211 2212 // Error warning if jsrender.js is used as template engine on Node.js (e.g. Express or Hapi...) 2213 // Use jsrender-node.js instead... 2214 $.renderFile = $.__express = $.compile = function() { throw "Node.js: use npm jsrender, or jsrender-node.js"; }; 2215 2216 //END BROWSER-SPECIFIC CODE 2217 $.isFunction = function(ob) { 2218 return typeof ob === "function"; 2219 }; 2220 2221 $.isArray = Array.isArray || function(obj) { 2222 return ({}.toString).call(obj) === "[object Array]"; 2223 }; 2224 2225 $sub._jq = function(jq) { // private method to move from JsRender APIs from jsrender namespace to jQuery namespace 2226 if (jq !== $) { 2227 $extend(jq, $); // map over from jsrender namespace to jQuery namespace 2228 $ = jq; 2229 $.fn.render = $fnRender; 2230 delete $.jsrender; 2231 $expando = $.expando; 2232 } 2233 }; 2234 2235 $.jsrender = versionNumber; 2236 } 2237 $subSettings = $sub.settings; 2238 $subSettings.allowCode = false; 2239 $isFunction = $.isFunction; 2240 $.render = $render; 2241 $.views = $views; 2242 $.templates = $templates = $views.templates; 2243 2244 for (setting in $subSettings) { 2245 addSetting(setting); 2246 } 2247 2248 ($viewsSettings.debugMode = function(debugMode) { 2249 return debugMode === undefined 2250 ? $subSettings.debugMode 2251 : ( 2252 $subSettings.debugMode = debugMode, 2253 $subSettings.onError = debugMode + "" === debugMode 2254 ? new Function("", "return '" + debugMode + "';" ) 2255 : $isFunction(debugMode) 2256 ? debugMode 2257 : undefined, 2258 $viewsSettings); 2259 })(false); // jshint ignore:line 2260 2261 $subSettingsAdvanced = $subSettings.advanced = { 2262 useViews: false, 2263 _jsv: false // For global access to JsViews store 2264 }; 2265 2266 //========================== Register tags ========================== 2267 2268 $tags({ 2269 "if": { 2270 render: function(val) { 2271 // This function is called once for {{if}} and once for each {{else}}. 2272 // We will use the tag.rendering object for carrying rendering state across the calls. 2273 // If not done (a previous block has not been rendered), look at expression for this block and render the block if expression is truthy 2274 // Otherwise return "" 2275 var self = this, 2276 tagCtx = self.tagCtx, 2277 ret = (self.rendering.done || !val && (arguments.length || !tagCtx.index)) 2278 ? "" 2279 : (self.rendering.done = true, self.selected = tagCtx.index, 2280 // Test is satisfied, so render content on current context. We call tagCtx.render() rather than return undefined 2281 // (which would also render the tmpl/content on the current context but would iterate if it is an array) 2282 tagCtx.render(tagCtx.view, true)); // no arg, so renders against parentView.data 2283 return ret; 2284 }, 2285 flow: true 2286 }, 2287 "for": { 2288 render: function(val) { 2289 // This function is called once for {{for}} and once for each {{else}}. 2290 // We will use the tag.rendering object for carrying rendering state across the calls. 2291 var finalElse = !arguments.length, 2292 value, 2293 self = this, 2294 tagCtx = self.tagCtx, 2295 result = "", 2296 done = 0; 2297 2298 if (!self.rendering.done) { 2299 value = finalElse ? tagCtx.view.data : val; // For the final else, defaults to current data without iteration. 2300 if (value !== undefined) { 2301 result += tagCtx.render(value, finalElse); // Iterates except on final else, if data is an array. (Use {{include}} to compose templates without array iteration) 2302 done += $isArray(value) ? value.length : 1; 2303 } 2304 if (self.rendering.done = done) { 2305 self.selected = tagCtx.index; 2306 } 2307 // If nothing was rendered we will look at the next {{else}}. Otherwise, we are done. 2308 } 2309 return result; 2310 }, 2311 flow: true 2312 }, 2313 props: { 2314 baseTag: "for", 2315 dataMap: dataMap(getTargetProps), 2316 flow: true 2317 }, 2318 include: { 2319 flow: true 2320 }, 2321 "*": { 2322 // {{* code... }} - Ignored if template.allowCode and $.views.settings.allowCode are false. Otherwise include code in compiled template 2323 render: retVal, 2324 flow: true 2325 }, 2326 ":*": { 2327 // {{:* returnedExpression }} - Ignored if template.allowCode and $.views.settings.allowCode are false. Otherwise include code in compiled template 2328 render: retVal, 2329 flow: true 2330 }, 2331 dbg: $helpers.dbg = $converters.dbg = dbgBreak // Register {{dbg/}}, {{dbg:...}} and ~dbg() to throw and catch, as breakpoints for debugging. 2332 }); 2333 2334 $converters({ 2335 html: htmlEncode, 2336 attr: htmlEncode, // Includes > encoding since rConvertMarkers in JsViews does not skip > characters in attribute strings 2337 url: function(text) { 2338 // URL encoding helper. 2339 return text != undefined ? encodeURI("" + text) : text === null ? text : ""; // null returns null, e.g. to remove attribute. undefined returns "" 2340 } 2341 }); 2342} 2343//========================== Define default delimiters ========================== 2344$subSettings = $sub.settings; 2345$isArray = ($||jsr).isArray; 2346$viewsSettings.delimiters("{{", "}}", "^"); 2347 2348if (jsrToJq) { // Moving from jsrender namespace to jQuery namepace - copy over the stored items (templates, converters, helpers...) 2349 jsr.views.sub._jq($); 2350} 2351 2352//<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< JsObservable >>>>>>>>>>>>>>>>>>>>>>>>>>>>>> 2353/* JsObservable: 2354 * See http://www.jsviews.com/#jsobservable and http://github.com/borismoore/jsviews 2355 * Copyright 2016, Boris Moore 2356 * Released under the MIT License. 2357 */ 2358 2359//========================== Top-level vars ========================== 2360 2361$views = $.views; 2362$sub = $views.sub; 2363$isFunction = $.isFunction; 2364$isArray = $.isArray; 2365$expando = $.expando; 2366if (!$.observe) { 2367 2368 var $eventSpecial = $.event.special, 2369 slice = [].slice, 2370 splice = [].splice, 2371 concat = [].concat, 2372 PARSEINT = parseInt, 2373 rNotWhite = /\S+/g, 2374 propertyChangeStr = $sub.propChng = $sub.propChng || "propertyChange",// These two settings can be overridden on settings after loading 2375 arrayChangeStr = $sub.arrChng = $sub.arrChng || "arrayChange", // jsRender, and prior to loading jquery.observable.js and/or JsViews 2376 cbBindingsStore = {}, 2377 observeStr = propertyChangeStr + ".observe", 2378 observeObjKey = 1, 2379 observeCbKey = 1, 2380 observeInnerCbKey = 1, 2381 $hasData = $.hasData, 2382 $data = $.data, 2383 remove = {}, // flag for removeProperty 2384 2385 //========================== Top-level functions ========================== 2386 2387 getCbKey = function(cb) { 2388 return cb._cId = cb._cId || (".obs" + observeCbKey++); 2389 }, 2390 2391 ObjectObservable = function(ns, data) { 2392 this._data = data; 2393 this._ns = ns; 2394 return this; 2395 }, 2396 2397 ArrayObservable = function(ns, data) { 2398 this._data = data; 2399 this._ns = ns; 2400 return this; 2401 }, 2402 2403 wrapArray = function(data) { 2404 return $isArray(data) 2405 ? [data] 2406 : data; 2407 }, 2408 2409 resolvePathObjects = function(paths, root, callback) { 2410 paths = paths 2411 ? $isArray(paths) 2412 ? paths 2413 : [paths] 2414 : []; 2415 2416 var i, path, 2417 object = root, 2418 nextObj = object, 2419 l = paths && paths.length, 2420 out = []; 2421 2422 for (i = 0; i < l; i++) { 2423 path = paths[i]; 2424 if ($isFunction(path)) { 2425 out = out.concat(resolvePathObjects(path.call(root, root, callback), root)); 2426 continue; 2427 } else if ("" + path !== path) { 2428 root = nextObj = path; 2429 if (nextObj !== object) { 2430 out.push(object = nextObj); 2431 } 2432 continue; 2433 } 2434 if (nextObj !== object) { 2435 out.push(object = nextObj); 2436 } 2437 out.push(path);
2438 } 2439 return out; 2440 }, 2441 2442 removeCbBindings = function(cbBindings, cbBindingsId) { 2443 // If the cbBindings collection is empty we will remove it from the cbBindingsStore 2444 for (var cb in cbBindings) { 2445 return; 2446 } 2447 delete cbBindingsStore[cbBindingsId]; // This binding collection is empty, so remove from store 2448 }, 2449 2450 onObservableChange = function(ev, eventArgs) { 2451 function isOb(val) { 2452 return typeof val === OBJECT && (paths[0] || allowArray && $isArray(val)); 2453 } 2454 2455 if (!(ev.data && ev.data.off)) { 2456 // 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) 2457 var allPath, filter, parentObs, 2458 oldValue = eventArgs.oldValue, 2459 value = eventArgs.value, 2460 ctx = ev.data, 2461 observeAll = ctx.observeAll, 2462 cb = ctx.cb, 2463 allowArray = !cb.noArray, 2464 paths = ctx.paths, 2465 ns = ctx.ns; 2466 2467 if (ev.type === arrayChangeStr) { 2468 (cb.array || cb).call(ctx, ev, eventArgs); // If there is an arrayHandler expando on the regular handler, use it, otherwise use the regular handler for arrayChange events also - for example: $.observe(array, handler) 2469 // or observeAll() with an array in the graph. Note that on data-link bindings we ensure always to have an array handler - $.noop if none is specified e.g. on the data-linked tag. 2470 } else if (ctx.prop === eventArgs.path || ctx.prop === "*") { 2471 if (observeAll) { 2472 allPath = observeAll._path + "." + eventArgs.path; 2473 filter = observeAll.filter; 2474 parentObs = [ev.target].concat(observeAll.parents()); 2475 2476 if (isOb(oldValue)) { 2477 observe_apply(allowArray, ns, [oldValue], paths, cb, true, filter, [parentObs], allPath); // unobserve 2478 } 2479 if (isOb(value)) { 2480 observe_apply(allowArray, ns, [value], paths, cb, undefined, filter, [parentObs], allPath); 2481 } 2482 } else { 2483 if (isOb(oldValue)) { // oldValue is an object, so unobserve 2484 observe_apply(allowArray, ns, [oldValue], paths, cb, true); // unobserve 2485 } 2486 if (isOb(value)) { // value is an object, so observe 2487 observe_apply(allowArray, ns, [value], paths, cb); 2488 } 2489 } 2490 ctx.cb(ev, eventArgs); 2491 } 2492 } 2493 }, 2494 2495 observe_apply = function() { 2496 // $.observe(), but allowing you to include arrays within the arguments - which you want flattened. 2497 var args = concat.apply([], arguments); // Flatten the arguments 2498 return $observe.apply(args.shift(), args); 2499 }, 2500 2501 $observeAll = function(cb, filter, unobserve) { 2502 observeAll(this._ns, this._data, cb, filter, [], "root", unobserve); 2503 }, 2504 2505 $unobserveAll = function(cb, filter) { 2506 $observeAll.call(this, cb, filter, true); 2507 }, 2508 2509 observeAll = function(namespace, object, cb, filter, parentObs, allPath, unobserve, objMap) { 2510 function observeArrayItems(arr, unobs) { 2511 l = arr.length; 2512 newAllPath = allPath + "[]"; 2513 while (l--) { 2514 filterAndObserveAll(arr, l, unobs, 1); 2515 } 2516 } 2517 2518 function filterAndObserveAll(obj, prop, unobs, nestedArray) { 2519 var newObject, newParentObs; 2520 if (prop !== $expando) { 2521 if (newObject = $observable._fltr(newAllPath, obj[prop], nextParentObs, filter)) { 2522 newParentObs = nextParentObs.slice(); 2523 if (nestedArray && updatedTgt && newParentObs[0] !== updatedTgt) { 2524 newParentObs.unshift(updatedTgt); // For array change events when observing an array which is not the root, need to add updated array to parentObs 2525 } 2526 observeAll(namespace, newObject, cb, filter || (nestedArray ? undefined : 0), newParentObs, newAllPath, unobs, objMap); 2527 // If nested array, need to observe the array too - so set filter to undefined 2528 } 2529 } 2530 } 2531 2532 function wrappedCb(ev, eventArgs) { 2533 // This object is changing. 2534 allPath = ev.data.observeAll._path; 2535 updatedTgt = ev.target; 2536 switch (eventArgs.change) { // observeAll/unobserveAll on added or removed objects 2537 case "insert": 2538 observeArrayItems(eventArgs.items); 2539 break; 2540 case "remove": 2541 observeArrayItems(eventArgs.items, true); // unobserveAll on removed items 2542 break; 2543 case "set":
2544 newAllPath = allPath + "." + eventArgs.path; 2545 filterAndObserveAll(eventArgs, "oldValue", true); // unobserve old value 2546 filterAndObserveAll(eventArgs, "value"); // observe new value 2547 } 2548 updatedTgt = undefined; 2549 cb.apply(this, arguments); // Observe this object (invoke the callback) 2550 } 2551 2552 var l, isObject, newAllPath, nextParentObs, updatedTgt, obId, 2553 notRemoving = !objMap || objMap.un || !unobserve; // true unless it is an observeAll call (not unobserveAll) and we are removing a listener (not adding one) 2554 2555 if (object && typeof object === OBJECT) { 2556 nextParentObs = [object].concat(parentObs); // The parentObs chain for the next depth of observeAll 2557 isObject = $isArray(object) ? "" : "*"; 2558 if (objMap && notRemoving && $hasData(object) && objMap[obId = $data(object).obId]) { 2559 objMap[obId]++; 2560 return; // This object has already being observed/unobserved by this observeAll/unobserveAll call (must be a cyclic object graph) so skip, to avoid 2561 // stack overflow/multiple instances of listener. See jsviews/pull/305 2562 // NOTE - WE DO NOT support ObserveAll on data with cyclic graphs which include DUPLICATE REFERENCES TO ARRAY PROPERTIES - such as data.children = data.descendants = [] 2563 } 2564 if (!objMap) { 2565 objMap = {un: unobserve}; // Map object to register observed objects for this observeAll 2566 } 2567 2568 if (cb) { 2569 // Observe this object or array - and also listen for changes to object graph, to add or remove observers from the modified object graph 2570 if (isObject || filter !== 0) { 2571 // If an object, observe the object. If an array, only add arrayChange binding if has filter or if filter is undefined (!== 0) - which 2572 // is the case for top-level calls or for nested array (array item of an array - e.g. member of 2-dimensional array). 2573 // For array properties lower in the tree, with no filter, filter is set to 0 in filterAndObserveAll, so no arrayChange binding here, 2574 // since they get arrayChange binding added during regular $.observe(array ...) binding. 2575 wrappedCb._cId = getCbKey(cb); // Identify wrapped callback with unwrapped callback, so unobserveAll will 2576 // remove previous observeAll wrapped callback, if inner callback was the same; 2577 if (notRemoving) { 2578 $observe(namespace, object, isObject, wrappedCb, unobserve, filter, nextParentObs, allPath); 2579 obId = $data(object).obId; 2580 objMap[obId] = (objMap[obId] || 0) + 1; // Register on map of objects observed/unobserved by this observeAll/unobserveAll call 2581 //- or remove from map if we are removing this object from observeAll call. (Avoid dups, for cyclic graphs) 2582 } else { 2583 if (--objMap[$data(object).obId]) { 2584 // Register on map of objects observed/unobserved by this observeAll/unobserveAll call 2585 //- or remove from map if we are removing this object from observeAll call. (Avoid dups, for cyclic graphs) 2586 return; 2587 } 2588 $observe(namespace, object, isObject, wrappedCb, unobserve, filter, nextParentObs, allPath); 2589 } 2590 } 2591 } else { 2592 // No callback. Just unobserve if unobserve === true. 2593 if (objMap) { 2594 objMap[$data(object).obId] = 1; // Register on map of objects unobserved by this unobserveAll call. (Avoid dups, for cyclic graphs) 2595 } 2596 $observe(namespace, object, isObject, undefined, unobserve, filter, nextParentObs, allPath); 2597 } 2598 2599 if (isObject) { 2600 // Continue stepping through object graph, observing object and arrays 2601 // To override filtering, pass in filter function, or replace $.observable._fltr 2602 for (l in object) { 2603 newAllPath = allPath + "." + l; 2604 filterAndObserveAll(object, l, unobserve); 2605 } 2606 } else { // Observe items in Array 2607 observeArrayItems(object, unobserve); 2608 } 2609 } 2610 }, 2611 2612 shallowFilter = function(allPath /*, object, parentObs*/) { 2613 return allPath.indexOf(".") < 0 && allPath.indexOf("[") < 0; 2614 }, 2615 2616 $unobserve = function() { 2617 [].push.call(arguments, true); // Add true as additional final argument 2618 return $observe.apply(this, arguments); 2619 }; 2620 2621 $observe = function() { 2622 // $.observe([namespace, ]root, [1 or more objects, path or path Array params...], callback[, contextCallback][, unobserve]) 2623
2624 function innerObserve() { 2625 2626 function observeOnOff(namespace, pathStr, isArrayBinding, off) { 2627 var j, evData, 2628 obIdExpando = $hasData(object), 2629 boundObOrArr = wrapArray(object), 2630 prntObs = parentObs, 2631 allPth = allPath; 2632 2633 namespace = initialNs ? namespace + "." + initialNs : namespace; 2634 2635 if (!unobserve && (off || isArrayBinding)) { 2636 events = obIdExpando && $._data(object); 2637 events = events && events.events; 2638 events = events && events[isArrayBinding ? arrayChangeStr : propertyChangeStr]; 2639 el = events && events.length; 2640 while (el--) { // Skip duplicates 2641 data = events[el] && events[el].data; 2642 if (data && (off && data.ns !== initialNs 2643 // When observing, don't unbind dups unless they have the same namespace 2644 || !off && data.ns === initialNs && data.cb && data.cb._cId === callback._cId)) 2645 // When observing and doing array binding, don't bind dups if they have the same namespace (Dups can happen e.g. with {^{for people ~foo=people}}) 2646 { 2647 return; 2648 } 2649 } 2650 } 2651 if (unobserve || off) { 2652 $(boundObOrArr).off(namespace, onObservableChange); 2653 } else { 2654 evData = isArrayBinding ? {} 2655 : { 2656 fullPath: path, 2657 paths: pathStr ? [pathStr] : [], 2658 prop: prop 2659 }; 2660 evData.ns = initialNs; 2661 evData.cb = callback; 2662 2663 if (allPath) { 2664 // This is an observeAll call 2665 evData.observeAll = { 2666 _path: allPth, 2667 path: function() { // Step through path and parentObs parent chain, replacing '[]' by '[n]' based on current index of objects in parent arrays. 2668 j = prntObs.length; 2669 return allPth.replace(/[[.]/g, function(all) { 2670 j--; 2671 return all === "[" 2672 ? "[" + $.inArray(prntObs[j - 1], prntObs[j]) 2673 : "."; 2674 });
2675 }, 2676 parents: function() { 2677 return prntObs; // The chain of parents between the modified object and the root object used in the observeAll() call 2678 }, 2679 filter: filter 2680 }; 2681 } 2682 $(boundObOrArr).on(namespace, null, evData, onObservableChange); 2683 if (cbBindings) { 2684 // Add object to cbBindings 2685 cbBindings[$data(object).obId || $data(object, "obId", observeObjKey++)] = object; 2686 } 2687 } 2688 } 2689 2690 function getInnerCb(exprOb) { 2691 // Returns the innerCb used for updating a computed in a compiled expression (setting the new instance as exprOb.ob, unobserving the previous object, 2692 // and observing the new one), then calling the outerCB - i.e. the handler for the whole compiled expression. 2693 // Initialized exprOb.ob to the current object. 2694 // Uses the contextCb callback to execute the compiled exprOb template in the context of the view/data etc. to get the returned value, typically an object or array. 2695 // If it is an array, registers array binding 2696 var origRt = root; 2697 // Note: For jsviews/issues/292 ctxCb will need var ctxCb = contextCb || function(exprOb, origRt) {return exprOb._jsv(origRt);}; 2698 2699 exprOb.ob = contextCb(exprOb, origRt); // Initialize object 2700 2701 return exprOb.cb = function(ev, eventArgs) { 2702 var obj = exprOb.ob, // The old object 2703 sub = exprOb.sb, 2704 newObj = contextCb(exprOb, origRt); 2705 2706 if (newObj !== obj) { 2707 if (typeof obj === OBJECT) { 2708 bindArray(obj, true); 2709 if (sub || allowArray && $isArray(obj)) { 2710 innerObserve([obj], sub, callback, contextCb, true); // unobserve on the old object 2711 } 2712 } 2713 exprOb.ob = newObj; 2714 // Put the updated object instance onto the exprOb in the paths array, so subsequent string paths are relative to this object 2715 if (typeof newObj === OBJECT) { 2716 bindArray(newObj); 2717 if (sub || allowArray && $isArray(newObj)) { 2718 // Register array binding 2719 innerObserve([newObj], sub, callback, contextCb); 2720 } 2721 } 2722 } 2723 // Call the outerCb - to execute the compiled expression that this computed is part of 2724 callback(ev, eventArgs); 2725 }; 2726 } 2727 2728 function bindArray(arr, unbind, isArray, relPath) { 2729 if (allowArray) { 2730 // This is a call to observe that does not come from observeAndBind (tag binding), so we allow arrayChange binding 2731 var prevObj = object, 2732 prevAllPath = allPath; 2733 2734 object = arr; 2735 if (relPath) { 2736 object = arr[relPath]; 2737 allPath += "." + relPath; 2738 } 2739 if (filter && object) { 2740 object = $observable._fltr(allPath, object, relPath ? [arr].concat(parentObs) : parentObs, filter); 2741 } 2742 if (object && (isArray || $isArray(object))) { 2743 observeOnOff(arrayChangeStr + ".observe" + (callback ? getCbKey(callback) : ""), undefined, true, unbind); 2744 } 2745 object = prevObj; 2746 allPath = prevAllPath; 2747 } 2748 } 2749 2750 var i, p, skip, parts, prop, path, dep, unobserve, callback, cbId, inId, el, data, events, contextCb, items, cbBindings, 2751 depth, innerCb, parentObs, allPath, filter, initNsArr, initNsArrLen, 2752 ns = observeStr, 2753 paths = this != 1 // Using != for IE<10 bug- see jsviews/issues/237 2754 ? concat.apply([], arguments) // Flatten the arguments - this is a 'recursive call' with params using the 'wrapped array' 2755 // style - such as innerObserve([object], path.path, [origRoot], path.prm, innerCb, ...); 2756 : slice.call(arguments), // Don't flatten - this is the first 'top-level call, to innerObserve.apply(1, paths) 2757 lastArg = paths.pop() || false, 2758 root = paths.shift(), 2759 object = root, 2760 l = paths.length; 2761 2762 if (lastArg + "" === lastArg) { // If last arg is a string then this observe call is part of an observeAll call, 2763 allPath = lastArg; // and the last three args are the parentObs array, the filter, and the allPath string. 2764 parentObs = paths.pop(); 2765 filter = paths.pop(); 2766 lastArg = !!paths.pop(); // unobserve 2767 l -= 3; 2768 } 2769 if (lastArg === !!lastArg) { 2770 unobserve = lastArg; 2771 lastArg = paths[l-1]; 2772 lastArg = l && lastArg + "" !== lastArg && (!lastArg || $isFunction(lastArg)) ? (l--, paths.pop()) : undefined;
2773 if (unobserve && !l && $isFunction(root)) { 2774 lastArg = root; 2775 root = undefined; 2776 } 2777 } 2778 callback = lastArg; 2779 if (l && $isFunction(paths[l - 1])) { 2780 contextCb = callback; 2781 callback = paths.pop(); 2782 l--; 2783 } 2784 2785 if (unobserve && callback && !callback._cId) { 2786 return; 2787 } 2788 2789 // Use a unique namespace (e.g. obs7) associated with each observe() callback to allow unobserve to remove handlers 2790 ns += callback 2791 ? ((inId = callback._inId || ""), unobserve) 2792 ? callback._cId + inId 2793 : (cbId = getCbKey(callback)) + inId 2794 : ""; 2795 2796 if (cbId && !unobserve) { 2797 cbBindings = cbBindingsStore[cbId] = cbBindingsStore[cbId] || {}; 2798 } 2799 2800 initNsArr = initialNs && initialNs.match(rNotWhite) || [""]; 2801 initNsArrLen = initNsArr.length; 2802 2803 while (initNsArrLen--) { 2804 initialNs = initNsArr[initNsArrLen]; 2805 if (root && (path = paths[0], !path || path + "" !== path)) { 2806 if ($isArray(root)) { 2807 bindArray(root, unobserve, true); 2808 } else if (unobserve) { 2809 // remove onObservableChange handlers that wrap that callback 2810 observeOnOff(ns, ""); 2811 } 2812 } 2813 if (unobserve && !l && !root) { // unobserve() - unobserves all 2814 for (p in cbBindingsStore) { 2815 p = cbBindingsStore[p]; 2816 for (data in p) { 2817 object = p[data]; 2818 if ($isArray(object)) { 2819 bindArray(object, unobserve, unobserve); 2820 } else { 2821 // remove onObservableChange handlers that wrap that callback 2822 observeOnOff(ns, ""); 2823 } 2824 } 2825 } 2826 } 2827 depth = 0; 2828 for (i = 0; i < l; i++) { 2829 path = paths[i]; 2830 if (path === "") { 2831 continue; 2832 } 2833 object = root; 2834 if ("" + path === path) { 2835 // Consider support for computed paths: jsviews/issues/292 2836 //if (/[\(\[\+]/.test(path)) { 2837 // var b={links:{}}, t = $sub.tmplFn("{:"+path+"}", b, true), items = t.paths[0]; 2838 // l += items.length - 1; 2839 // splice.apply(paths, [i--, 1].concat(items)); 2840 // continue; 2841 //} 2842 parts = path.split("^"); 2843 if (parts[1]) { 2844 // We bind the leaf, plus additional nodes based on depth. 2845 // "a.b.c^d.e" is depth 2, so listens to changes of e, plus changes of d and of c 2846 depth = parts[0].split(".").length; 2847 path = parts.join("."); 2848 depth = path.split(".").length - depth; 2849 // if more than one ^ in the path, the first one determines depth 2850 } 2851 if (contextCb && (items = contextCb(path, root, depth))) { 2852 // If the array of objects and paths returned by contextCb is non empty, insert them 2853 // into the sequence, replacing the current item (path). Otherwise simply remove current item (path) 2854 l += items.length - 1; 2855 splice.apply(paths, [i--, 1].concat(items)); 2856 continue; 2857 } 2858 parts = path.split("."); 2859 } else { 2860 if (!$isFunction(path)) { 2861 if (path && path._jsv) { 2862 // This is a compiled function for binding to an object returned by a helper/data function. 2863 // Set current object on exprOb.ob, and get innerCb for updating the object 2864 innerCb = unobserve ? path.cb : getInnerCb(path); 2865 innerCb.noArray = !allowArray; 2866 // innerCb._ctx = callback._ctx; Could pass context (e.g. linkCtx) for use in a depends = function() {} call, so depends is different for different linkCtx's 2867 innerCb._cId = callback._cId; 2868 // Set the same cbBindingsStore key as for callback, so when callback is disposed, disposal of innerCb happens too. 2869 innerCb._inId = innerCb._inId || ".obIn" + observeInnerCbKey++; 2870 if (path.bnd || path.prm && path.prm.length || !path.sb) { 2871 // If the exprOb is bound e.g. foo()^sub.path, or has parameters e.g. foo(bar) or is a leaf object (so no sub path) e.g. foo() 2872 // then observe changes on the object, or its parameters and sub-path 2873 innerObserve([object], path.path, [origRoot], path.prm, innerCb, contextCb, unobserve); 2874 } 2875 if (path.sb) { // subPath 2876 innerObserve([path.ob], path.sb, callback, contextCb, unobserve); 2877 } 2878 path = origRoot; 2879 object = undefined; 2880 } else { 2881 object = path; // For top-level calls, objects in the paths array become the origRoot for subsequent paths. 2882 } 2883 } 2884 parts = [root = path]; 2885 } 2886 while (object && (prop = parts.shift()) !== undefined) { 2887 if (typeof object === OBJECT) { 2888 if ("" + prop === prop) { 2889 if (prop === "") { 2890 continue; 2891 } 2892 if ((parts.length < depth + 1) && !object.nodeType) { 2893 // Add observer for each token in path starting at depth, and on to the leaf 2894 if (!unobserve && (events = $hasData(object) && $._data(object))) { 2895 events = events.events; 2896 events = events && events[propertyChangeStr]; 2897 el = events && events.length; 2898 skip = 0; 2899 while (el--) { // Skip duplicates 2900 data = events[el].data; 2901 if (data 2902 && data.ns === initialNs 2903 && data.cb._cId === callback._cId 2904 && data.cb._inId === callback._inId 2905 && (data.prop === prop || data.prop === "*" || data.prop === "**")) { 2906 if (p = parts.join(".")) { 2907 data.paths.push(p); // We will skip this binding, but if it is not a leaf binding, 2908 // need to keep bindings for rest of path, ready for if the object gets swapped. 2909 } 2910 skip++; 2911 } 2912 } 2913 if (skip) { 2914 // Duplicate binding(s) found, so move on 2915 object = object[prop]; 2916 continue; 2917 } 2918 } 2919 if (prop === "*" || prop === "**") { // "*" => all properties. "**" => all properties and sub-properties (i.e. deep observeAll behavior) 2920 if (!unobserve && events && events.length) { 2921 // Remove existing bindings, since they will be duplicates with "*" or "**" 2922 observeOnOff(ns, "", false, true); 2923 } 2924 if (prop === "*") { 2925 observeOnOff(ns, ""); // observe the object for any property change 2926 for (p in object) { 2927 // observing "*": So (in addition to listening to prop change, above) listen to arraychange on props of type array 2928 if (p !== $expando) { 2929 bindArray(object, unobserve, undefined, p); 2930 } 2931 } 2932 } else { 2933 $.observable(initialNs, object)[(unobserve ? "un" : "") + "observeAll"](callback); // observe or unobserve the object for any property change 2934 } 2935 break; 2936 } else if (prop) { 2937 observeOnOff(ns + ".p_" + prop, parts.join("^")); // By using "^" rather than "." we ensure that deep binding will be used on newly inserted object graphs 2938 } 2939 } 2940 if (allPath) { 2941 allPath += "." + prop; 2942 } 2943 prop = object[prop]; 2944 } 2945 if ($isFunction(prop)) { 2946 if (dep = prop.depends) { 2947 // This is a computed observable. We will observe any declared dependencies 2948 innerObserve([object], resolvePathObjects(dep, object, callback), callback, contextCb, unobserve); 2949 } 2950 break; 2951 } 2952 object = prop;
2953 if (unobserve && object === root && (i>l-2 || paths[i+1] + "" !== paths[i+1])) { 2954 // unobserve all handlers of object, if not followed by string path. 2955 // e.g.$.unobserve(object1, object2, "path", object3) will unobserve all from object1 and object3, and just "path" listener from object2 2956 observeOnOff(ns, ""); 2957 } 2958 } 2959 } 2960 bindArray(object, unobserve); 2961 } 2962 } 2963 if (cbId) { 2964 removeCbBindings(cbBindings, cbId); 2965 } 2966 2967 // Return the cbBindings to the top-level caller, along with the cbId 2968 return {cbId: cbId, bnd: cbBindings}; 2969 } 2970 2971 var initialNs, 2972 allowArray = this != false, // If this === false, this is a call from observeAndBind - doing binding of datalink expressions. We don't bind 2973 // arrayChange events in this scenario. Instead, {^{for}} and similar do specific arrayChange binding to the tagCtx.args[0] value, in onAfterLink. 2974 // Note deliberately using this != false, rather than this !== false because of IE<10 bug- see jsviews/issues/237 2975 paths = slice.call(arguments), 2976 origRoot = paths[0]; 2977 2978 if (origRoot + "" === origRoot && allowArray) { 2979 initialNs = origRoot; // The first arg is a namespace, since it is a string, and this call is not from observeAndBind 2980 paths.shift(); 2981 origRoot = paths[0]; 2982 } 2983 2984 return innerObserve.apply(1, paths); 2985 }; 2986 2987 $observable = function(ns, data) { 2988 if (arguments.length === 1) { 2989 data = ns; 2990 ns = ""; 2991 } 2992 return $isArray(data) 2993 ? new ArrayObservable(ns, data) 2994 : new ObjectObservable(ns, data); 2995 }; 2996 2997 //========================== Initialize ========================== 2998 2999 $sub.getDeps = function() { 3000 var args = arguments; 3001 return function() { 3002 var arg, dep, 3003 deps = [], 3004 l = args.length; 3005 while (l--) { 3006 arg = args[l--]; 3007 dep = args[l]; 3008 if (dep) { 3009 deps = deps.concat($isFunction(dep) ? dep(arg, arg) : dep); 3010 } 3011 } 3012 return deps; 3013 }; 3014 }; 3015 3016 $.observable = $observable; 3017 $observable._fltr = function(allPath, object, parentObs, filter) { 3018 if (filter && $isFunction(filter) 3019 ? filter(allPath, object, parentObs) 3020 : true // TODO Consider supporting filter being a string or strings to do RegEx filtering based on key and/or allPath 3021 ) { 3022 object = $isFunction(object) 3023 ? object.set && object.call(parentObs[0]) // It is a getter/setter 3024 : object; 3025 return typeof object === OBJECT && object; 3026 } 3027 }; 3028 3029 $observable.Object = ObjectObservable; 3030 $observable.Array = ArrayObservable; 3031 $.observe = $observable.observe = $observe; 3032 $.unobserve = $observable.unobserve = $unobserve; 3033 $observable._apply = observe_apply; 3034 3035 ObjectObservable.prototype = { 3036 _data: null, 3037 3038 observeAll: $observeAll, 3039 unobserveAll: $unobserveAll, 3040 3041 data: function() { 3042 return this._data; 3043 }, 3044 3045 setProperty: function(path, value, nonStrict) { 3046 path = path || ""; 3047 var key, pair, parts, 3048 multi = path + "" !== path, 3049 self = this, 3050 object = self._data; 3051 3052 if (object) { 3053 if (multi) { 3054 nonStrict = value; 3055 if ($isArray(path)) { 3056 // This is the array format generated by serializeArray. However, this has the problem that it coerces types to string, 3057 // and does not provide simple support of convertTo and convertFrom functions. 3058 key = path.length; 3059 while (key--) { 3060 pair = path[key]; 3061 self.setProperty(pair.name, pair.value, nonStrict === undefined || nonStrict); //If nonStrict not specified, default to true; 3062 } 3063 } else { 3064 // Object representation where property name is path and property value is value. 3065 for (key in path) { 3066 self.setProperty(key, path[key], nonStrict); 3067 } 3068 } 3069 } else if (path !== $expando) { 3070 // Simple single property case. 3071 parts = path.split(/[.^]/); 3072 while (object && parts.length > 1) { 3073 object = object[parts.shift()]; 3074 } 3075 if (object) { 3076 self._setProperty(object, parts[0], value, nonStrict); 3077 } 3078 } 3079 } 3080 return self; 3081 }, 3082 3083 removeProperty: function(path) { 3084 this.setProperty(path, remove); 3085 return this; 3086 }, 3087 3088 _setProperty: function(leaf, path, value, nonStrict) { 3089 var setter, getter, removeProp, 3090 property = path ? leaf[path] : leaf; 3091 3092 if ($isFunction(property)) { 3093 if (property.set) { 3094 // Case of property setter/getter - with convention that property is getter and property.set is setter 3095 leaf = leaf._wrp // Case of JsViews 2-way data-linking to a helper function as getter, with a setter. 3096 // The view will be the this pointer for getter and setter. Note: this is the one scenario where path is "".
3097 || leaf; 3098 getter = property; 3099 setter = getter.set === true ? getter : getter.set; 3100 property = getter.call(leaf); // get - only treated as getter if also a setter. Otherwise it is simply a property of type function. See unit tests 'Can observe properties of type function'. 3101 } 3102 } 3103 3104 if (property !== value || nonStrict && property != value) { // Optional non-strict equality, since serializeArray, and form-based editors can map numbers to strings, etc. 3105 // Date objects don't support != comparison. Treat as special case. 3106 if (!(property instanceof Date) || property > value || property < value) { 3107 if (setter) { 3108 setter.call(leaf, value); //set 3109 value = getter.call(leaf); //get updated value 3110 } else if (removeProp = value === remove) { 3111 if (property !== undefined) { 3112 delete leaf[path]; 3113 value = undefined; 3114 } else { 3115 path = undefined; // If value was already undefined, don't trigger handler for removeProp 3116 } 3117 } else if (path) { 3118 leaf[path] = value; 3119 } 3120 if (path) { 3121 this._trigger(leaf, {change: "set", path: path, value: value, oldValue: property, remove: removeProp}); 3122 } 3123 } 3124 } 3125 }, 3126 3127 _trigger: function(target, eventArgs) { 3128 $(target).triggerHandler(propertyChangeStr + (this._ns ? "." + /^\S+/.exec(this._ns)[0] : ""), eventArgs); // If white-space separated namespaces, use first one only 3129 } 3130 }; 3131 3132 ArrayObservable.prototype = { 3133 _data: null, 3134 3135 observeAll: $observeAll, 3136 unobserveAll: $unobserveAll, 3137 3138 data: function() { 3139 return this._data; 3140 }, 3141 3142 insert: function(index, data) { 3143 var _data = this._data; 3144 if (arguments.length === 1) { 3145 data = index; 3146 index = _data.length; 3147 } 3148 index = PARSEINT(index); 3149 if (index > -1) { 3150 data = $isArray(data) ? data : [data]; 3151 // data can be a single item (including a null/undefined value) or an array of items. 3152 // Note the provided items are inserted without being cloned, as direct references to the provided objects 3153 3154 if (data.length) { 3155 this._insert(index, data); 3156 } 3157 } 3158 return this; 3159 }, 3160 3161 _insert: function(index, data) { 3162 var _data = this._data, 3163 oldLength = _data.length; 3164 if (index > oldLength) { 3165 index = oldLength; 3166 } 3167 splice.apply(_data, [index, 0].concat(data)); 3168 this._trigger({change: "insert", index: index, items: data}, oldLength); 3169 }, 3170 3171 remove: function(index, numToRemove) { 3172 var items, 3173 _data = this._data; 3174 3175 if (index === undefined) { 3176 index = _data.length - 1; 3177 } 3178 3179 index = PARSEINT(index); 3180 numToRemove = numToRemove ? PARSEINT(numToRemove) : numToRemove === 0 ? 0 : 1; // if null or undefined: remove 1 3181 if (numToRemove > 0 && index > -1) { 3182 items = _data.slice(index, index + numToRemove); 3183 if (numToRemove = items.length) { 3184 this._remove(index, numToRemove, items); 3185 } 3186 } 3187 return this; 3188 }, 3189 3190 _remove: function(index, numToRemove, items) { 3191 var _data = this._data, 3192 oldLength = _data.length; 3193 3194 _data.splice(index, numToRemove); 3195 this._trigger({change: "remove", index: index, items: items}, oldLength); 3196 }, 3197 3198 move: function(oldIndex, newIndex, numToMove) { 3199 numToMove = numToMove ? PARSEINT(numToMove) : numToMove === 0 ? 0 : 1; // if null or undefined: move 1 3200 oldIndex = PARSEINT(oldIndex); 3201 newIndex = PARSEINT(newIndex); 3202 3203 if (numToMove > 0 && oldIndex > -1 && newIndex > -1 && oldIndex !== newIndex) { 3204 this._move(oldIndex, newIndex, numToMove); 3205 } 3206 return this; 3207 }, 3208 3209 _move: function(oldIndex, newIndex, numToMove) { 3210 var items, 3211 _data = this._data, 3212 oldLength = _data.length, 3213 excess = oldIndex + numToMove - oldLength; 3214 if (excess > 0) { 3215 numToMove -= excess; 3216 } 3217 if (numToMove) { 3218 items = _data.splice(oldIndex, numToMove); // remove 3219 if (newIndex > _data.length) { 3220 newIndex = _data.length; 3221 } 3222 splice.apply(_data, [newIndex, 0].concat(items)); //re-insert 3223 this._trigger({change: "move", oldIndex: oldIndex, index: newIndex, items: items}, oldLength); 3224 } 3225 }, 3226 3227 refresh: function(newItems, sort) { 3228 function insertAdded() { 3229 if (k) { 3230 self.insert(j-k, addedItems); // Not found in original array - so insert 3231 dataLength += k; 3232 i += k; 3233 k = 0; 3234 addedItems = []; 3235 } 3236 } 3237 3238 // For refresh operation we iteratively step through the target array and sort by move/add/remove operations on the source array until they match 3239 var i, j, k, newItem, num, 3240 self = this, 3241 addedItems = [], 3242 data = self._data, 3243 oldItems = data.slice(), 3244 oldLength = data.length, 3245 dataLength = oldLength, 3246 newLength = newItems.length; 3247 self._srt = true; // Flag for sorting during refresh 3248 for (j=k=0; j<newLength; j++) { 3249 if ((newItem = newItems[j]) === data[j-k]) { 3250 insertAdded(); 3251 } else { 3252 for (i=j-k; i<dataLength; i++) { 3253 if (newItem === data[i]) { 3254 break; 3255 } 3256 } 3257 if (i<dataLength) { 3258 insertAdded(); 3259 num = 0; 3260 while (num++ < newLength-i && newItems[j+num] === data[i+num]); 3261 self.move(i, j, num); // Found newItem in original array - so move it to new position 3262 j += num - 1; 3263 } else { 3264 k++; 3265 addedItems.push(newItem); // Not found in original array - so insert 3266 } 3267 } 3268 } 3269 insertAdded(); 3270 if (dataLength > j) { 3271 self.remove(j, dataLength - j); 3272 } 3273 self._srt = undefined; // We have finished sort operations during refresh 3274 self._trigger({change: "refresh", oldItems: oldItems}, oldLength); 3275 return self; 3276 }, 3277 3278 _trigger: function(eventArgs, oldLength) { 3279 var self = this, 3280 _data = self._data, 3281 length = _data.length, 3282 $_data = $([_data]); 3283 if (self._srt) { 3284 eventArgs.refresh = true; // We are sorting during refresh 3285 } else if (length !== oldLength) { // We have finished sort operations during refresh 3286 $_data.triggerHandler(propertyChangeStr, {change: "set", path: "length", value: length, oldValue: oldLength}); 3287 } 3288 $_data.triggerHandler(arrayChangeStr + (self._ns ? "." + /^\S+/.exec(self._ns)[0] : ""), eventArgs); // If white-space separated namespaces, use first one only 3289 } 3290 }; 3291 3292 $eventSpecial[propertyChangeStr] = $eventSpecial[arrayChangeStr] = { 3293 // Register a jQuery special 'remove' event, to access the data associated with handlers being removed by jQuery.off(). 3294 // We get data.cb._cId from the event handleObj and get the corresponding cbBindings hash from the cbBindingsStore, 3295 // then remove this object from that bindings hash - if the object does not have any other handlers associated with the same callback. 3296 remove: function (handleObj) { 3297 var cbBindings, found, events, l, data, 3298 evData = handleObj.data; 3299 if ((evData) && (evData.off = true, evData = evData.cb)) { //Set off = true as marker for disposed event 3300 // Get the cb._cId from handleObj.data.cb._cId 3301 if (cbBindings = cbBindingsStore[evData._cId]) { 3302 // There were bindings for this callback. If this was the last one, we'll remove it. 3303 events = $._data(this).events[handleObj.type]; 3304 l = events.length; 3305 while (l-- && !found) {
3306 found = (data = events[l].data) && data.cb && data.cb._cId === evData._cId; 3307 // Found another one with same callback (though may be a different innerCallback) 3308 } 3309 if (!found) { 3310 // This was the last handler for this callback and object, so remove the binding entry 3311 delete cbBindings[$data(this).obId]; 3312 removeCbBindings(cbBindings, evData._cId); 3313 } 3314 } 3315 } 3316 } 3317 }; 3318 3319 $views.map = function(mapDef) { 3320 function Map(source, options, target, unbound) { 3321 var changing, 3322 map = this; 3323 if (this.src) { 3324 this.unmap(); // We are re-mapping a new source 3325 } 3326 if (typeof source === OBJECT) { 3327 map.src = source; 3328 map.tgt = target || map.tgt || []; 3329 map.options = options || map.options; 3330 map.update(); 3331 if (!unbound) { 3332 if (mapDef.obsSrc) { 3333 $observable(map.src).observeAll(map.obs = function(ev, eventArgs) { 3334 if (!changing) { 3335 changing = true; 3336 mapDef.obsSrc(map, ev, eventArgs); 3337 changing = undefined; 3338 } 3339 }, map.srcFlt); 3340 } 3341 if (mapDef.obsTgt ) { 3342 $observable(map.tgt).observeAll(map.obt = function(ev, eventArgs) { 3343 if (!changing) { 3344 changing = true; 3345 mapDef.obsTgt(map, ev, eventArgs); 3346 changing = undefined; 3347 } 3348 }, map.tgtFlt); 3349 } 3350 } 3351 } 3352 } 3353 3354 if ($isFunction(mapDef)) { 3355 // Simple map declared as function 3356 mapDef = { 3357 getTgt: mapDef 3358 }; 3359 } 3360 3361 if (mapDef.baseMap) { 3362 mapDef = $.extend({}, mapDef.baseMap, mapDef); 3363 } 3364 3365 mapDef.map = function(source, options, target, unbound) { 3366 return new Map(source, options, target, unbound); 3367 }; 3368 3369 (Map.prototype = { 3370 srcFlt: mapDef.srcFlt || shallowFilter, // default to shallowFilter 3371 tgtFlt: mapDef.tgtFlt || shallowFilter, 3372 update: function(options) { 3373 var map = this; 3374 $observable(map.tgt).refresh(mapDef.getTgt(map.src, map.options = options || map.options)); 3375 }, 3376 unmap: function() { 3377 var map = this; 3378 if (map.src) { 3379 if (map.obs) { 3380 $observable(map.src).unobserveAll(map.obs, map.srcFlt); 3381 } 3382 if (map.obt) { 3383 $observable(map.tgt).unobserveAll(map.obt, map.tgtFlt); 3384 } 3385 map.src = undefined; 3386 } 3387 }, 3388 map: Map, 3389 _def: mapDef 3390 }).constructor = Map; 3391 3392 return mapDef; 3393 }; 3394 3395 $sub.advSet = function() { // refresh advanced settings 3396 global._jsv = $subSettings.advanced._jsv 3397 ? { // create global _jsv, for accessing views, etc 3398 cbBindings: cbBindingsStore 3399 } 3400 : undefined; // In IE8 cannot do delete global._jsv 3401 }; 3402} 3403 3404//<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< JsViews >>>>>>>>>>>>>>>>>>>>>>>>>>>>>> 3405/* JsViews: 3406 * Interactive data-driven views using templates and data-linking. 3407 * See http://www.jsviews.com/#jsviews and http://github.com/BorisMoore/jsviews 3408 * Copyright 2016, Boris Moore 3409 * Released under the MIT License. 3410 */ 3411 3412//========================== Top-level vars ========================== 3413 3414$viewsSettings = $views.settings; 3415$subSettings = $sub.settings; 3416$subSettingsAdvanced = $subSettings.advanced; 3417$converters = $views.converters; 3418$.templates = $templates = $views.templates; 3419$tags = $views.tags; 3420rFirstElem = /<(?!script)(\w+)[>\s]/; 3421 3422if ($.link) { return $; } // JsViews is already loaded 3423 3424$subSettings.trigger = true; 3425var activeBody, rTagDatalink, $view, $viewsLinkAttr, linkViewsSel, wrapMap, viewStore, oldAdvSet, 3426 jsvAttrStr = "data-jsv", 3427 elementChangeStr = "change.jsv", 3428 onBeforeChangeStr = "onBeforeChange", 3429 onAfterChangeStr = "onAfterChange", 3430 onAfterCreateStr = "onAfterCreate", 3431 CHECKED = "checked", 3432 CHECKBOX = "checkbox", 3433 RADIO = "radio", 3434 RADIOINPUT = "input[type=radio]", 3435 NONE = "none", 3436 SCRIPT = "SCRIPT", 3437 TRUE = "true", 3438 closeScript = '"></script>', 3439 openScript = '<script type="jsv', 3440 deferAttr = jsvAttrStr + "-df", 3441 bindElsSel = "script,[" + jsvAttrStr + "]", 3442 fnSetters = { 3443 value: "val", 3444 input: "val", 3445 html: HTML, 3446 text: "text" 3447 }, 3448 valueBinding = {from: "value", to: "value"}, 3449 isCleanCall = 0, 3450 oldCleanData = $.cleanData, 3451 oldJsvDelimiters = $viewsSettings.delimiters, 3452 safeFragment = document.createDocumentFragment(), 3453 qsa = document.querySelector, 3454 3455 // elContent maps tagNames which have only element content, so may not support script nodes. 3456 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}, 3457 badParent = {tr: "table"}, 3458 voidElems = {br: 1, img: 1, input: 1, hr: 1, area: 1, base: 1, col: 1, link: 1, meta: 1, 3459 command: 1, embed: 1, keygen: 1, param: 1, source: 1, track: 1, wbr: 1}, 3460 displayStyles = {}, 3461 bindingStore = {}, 3462 bindingKey = 1, 3463 rViewPath = /^#(view\.?)?/, 3464 rConvertMarkers = /((\/>)|<\/(\w+)>|)(\s*)([#\/]\d+(?:_|(\^)))`(\s*)(<\w+(?=[\s\/>]))?|\s*(?:(<\w+(?=[\s\/>]))|<\/(\w+)>(\s*)|(\/>)\s*|(>)|$)/g, 3465 rOpenViewMarkers = /(#)()(\d+)(_)/g, 3466 rOpenMarkers = /(#)()(\d+)([_^])/g, 3467 rViewMarkers = /(?:(#)|(\/))(\d+)(_)/g, 3468 rOpenTagMarkers = /(#)()(\d+)(\^)/g, 3469 rMarkerTokens = /(?:(#)|(\/))(\d+)([_^])([-+@\d]+)?/g, 3470 rSplitBindings = /&(\d+)\+?/g, 3471 getComputedStyle = global.getComputedStyle; 3472 3473$observable = $.observable; 3474 3475if (!$observable) { 3476 // JsObservable is not loaded. 3477 throw requiresStr + "JsObservable"; // jquery.observable.js must be loaded before JsViews 3478} 3479 3480$observe = $observable.observe; 3481 3482//========================== Top-level functions ========================== 3483 3484//=============== 3485// Event handlers 3486//=============== 3487 3488function elemChangeHandler(ev, params, sourceValue) { 3489 var setter, cancel, fromAttr, linkCtx, cvtBack, cnvtName, target, $source, view, binding, oldLinkCtx, onBeforeChange, onAfterChange, tag, to, eventArgs, exprOb, 3490 source = ev.target, 3491 bindings = source._jsvBnd; 3492 3493 // _jsvBnd is a string with the syntax: "&bindingId1&bindingId2" 3494 if (bindings) { 3495 while (binding = rSplitBindings.exec(bindings)) { 3496 if (binding = bindingStore[binding[1]]) { 3497 if (to = binding.to) { 3498 // The binding has a 'to' field, which is of the form [[targetObject, toPath], cvtBack] 3499 linkCtx = binding.linkCtx; 3500 view = linkCtx.view; 3501 tag = linkCtx.tag || view.tag; 3502 $source = $(source); 3503 onBeforeChange = view.hlp(onBeforeChangeStr); // TODO Can we optimize this and other instances of same? 3504 onAfterChange = view.hlp(onAfterChangeStr); // TODO Can we optimize this and other instances of same 3505 fromAttr = defaultAttr(source); 3506 setter = fnSetters[fromAttr]; 3507 if (sourceValue === undefined) { 3508 sourceValue = $isFunction(fromAttr) 3509 ? fromAttr(source) 3510 : setter 3511 ? $source[setter]() 3512 : $source.attr(fromAttr); 3513 } 3514 cnvtName = to[1]; 3515 to = to[0]; // [object, path] 3516 to = to + "" === to ? [linkCtx.data, to] : to; 3517 if (cnvtName) { 3518 if ($isFunction(cnvtName)) { 3519 cvtBack = cnvtName; 3520 } else { 3521 cvtBack = view.getRsc("converters", cnvtName); 3522 } 3523 } 3524 if (linkCtx.elem.nodeName === "SELECT") { 3525 linkCtx.elem._jsvSel = sourceValue = sourceValue || (linkCtx.elem.multiple ? [] : sourceValue); 3526 // data-link <select>
3526 to string or (multiselect) array of strings 3527 } 3528 if (cvtBack) { 3529 sourceValue = cvtBack.call(tag, sourceValue); 3530 } 3531 3532 // Set linkCtx on view, dynamically, just during this handler call 3533 oldLinkCtx = view.linkCtx; 3534 view.linkCtx = linkCtx; 3535 eventArgs = { 3536 change: "change", 3537 oldValue: linkCtx._val, 3538 value: sourceValue 3539 }; 3540 if ((!onBeforeChange || !(cancel = onBeforeChange.call(linkCtx, ev, eventArgs) === false)) && 3541 (!tag || !tag.onBeforeChange || !(cancel = tag.onBeforeChange(ev, eventArgs) === false)) && 3542 sourceValue !== undefined) { 3543 target = to[0]; // [object, path] 3544 if (sourceValue !== undefined && target) { 3545 if (target._jsv) { 3546 exprOb = target; 3547 target = linkCtx.data; 3548 while (exprOb && exprOb.sb) { 3549 target = linkCtx._ctxCb(exprOb, target); 3550 exprOb = exprOb.sb; 3551 } 3552 } 3553 if (tag) { 3554 tag._.chging = true; // marker to prevent tag change event triggering its own refresh 3555 } 3556 $observable(target).setProperty(to[1], sourceValue); // 2way binding change event - observably updating bound object 3557 if (onAfterChange) { 3558 onAfterChange.call(linkCtx, ev, eventArgs); 3559 } 3560 if (tag) { 3561 if (tag.onAfterChange) { 3562 tag.onAfterChange(ev, eventArgs); 3563 } 3564 tag._.chging = undefined; // clear the marker 3565 } 3566 linkCtx._val = sourceValue; 3567 } 3568 } 3569 view.linkCtx = oldLinkCtx; 3570 } 3571 } 3572 } 3573 } 3574} 3575 3576function propertyChangeHandler(ev, eventArgs, linkFn) { 3577 var attr, sourceValue, noUpdate, forceUpdate, hasError, onError, 3578 linkCtx = this, 3579 tag = linkCtx.tag, 3580 source = linkCtx.data, 3581 target = linkCtx.elem, 3582 cvt = linkCtx.convert, 3583 parentElem = target.parentNode, 3584 view = linkCtx.view, 3585 oldLinkCtx = view.linkCtx, 3586 onEvent = view.hlp(onBeforeChangeStr); 3587 3588 // Set linkCtx on view, dynamically, just during this handler call 3589 view.linkCtx = linkCtx; 3590 3591 if (parentElem && (!onEvent || !(eventArgs && onEvent.call(linkCtx, ev, eventArgs) === false)) 3592 // If data changed, the ev.data is set to be the path. Use that to filter the handler action...
3593 && !(eventArgs && ev.data.prop !== "*" && ev.data.prop !== eventArgs.path)) { 3594 3595 if (eventArgs) { 3596 linkCtx.eventArgs = eventArgs; 3597 } 3598 if (eventArgs || linkCtx._toLk) { 3599 // If eventArgs are defined, this is a data update 3600 // Otherwise this is the initial data-link rendering call. Bind on this the first time it get called 3601 linkCtx._toLk = 0; // Remove flag to skip unneccessary rebinding next time 3602 if (linkFn._er) { 3603 // data-link="exprUsingTagOrCvt with onerror=..." - e.g. {tag ... {cvt:... {:... convert='cvt' 3604 try { 3605 sourceValue = linkFn(source, view); 3606 } catch (e) { 3607 hasError = linkFn._er; 3608 onError = onRenderError(e,view,(new Function("data,view", "return " + hasError + ";"))(source, view)); 3609 sourceValue = [{props: {}, args: [onError]}]; 3610 } 3611 } else { 3612 sourceValue = linkFn(source, view, $sub); 3613 } 3614 // Compiled link expression for linkTag: return value for data-link="{:xxx}" with no cvt or cvtBk, otherwise tagCtx or tagCtxs 3615 3616 attr = getTargetVal(sourceValue, linkCtx, tag = linkCtx.tag, 3617 linkCtx.attr || defaultAttr(target, true, cvt !== undefined) 3618 ); 3619 3620 if (tag) { 3621 // Existing tag instance 3622 forceUpdate = hasError || tag._er; 3623 // If the new tagCtxs hasError or the previous tagCtxs had error, then force update 3624 sourceValue = sourceValue[0] ? sourceValue : [sourceValue]; 3625 noUpdate = !forceUpdate && eventArgs && tag.onUpdate && tag.onUpdate(ev, eventArgs, sourceValue) === false; 3626 3627 mergeCtxs(tag, sourceValue, forceUpdate); 3628 3629 if (noUpdate || attr === NONE) { 3630 // onUpdate returned false, or attr === "none", or this is an update coming from the tag's own change event 3631 // - so don't refresh the tag: we just use the new tagCtxs merged from the sourceValue, 3632 // (which may optionally have been modifed in onUpdate()...) and then bind, and we are done 3633 callAfterLink(tag, ev, eventArgs); 3634 observeAndBind(linkCtx, source, target); 3635 view.linkCtx = oldLinkCtx; 3636 return; 3637 } 3638 if (tag._.chging) { 3639 return; 3640 } 3641 3642 if (tag.onUnbind) { 3643 tag.onUnbind(tag.tagCtx, linkCtx, tag.ctx, ev, eventArgs); 3644 } 3645 3646 sourceValue = tag.tagName === ":" // Call convertVal if it is a {{cvt:...}} - otherwise call renderTag 3647 ? $sub._cnvt(tag.cvt, view, sourceValue[0]) 3648 : $sub._tag(tag, view, view.tmpl, sourceValue, true, onError); 3649 } else if (linkFn._tag) { 3650 // For {{: ...}} without a convert or convertBack, we already have the sourceValue, and we are done 3651 // For {{: ...}} with either cvt or cvtBack we call convertVal to get the sourceValue and instantiate the tag 3652 // If cvt is undefined then this is a tag, and we call renderTag to get the rendered content and instantiate the tag 3653 cvt = cvt === "" ? TRUE : cvt; // If there is a cvtBack but no cvt, set cvt to "true" 3654 sourceValue = cvt // Call convertVal if it is a {{cvt:...}} - otherwise call renderTag 3655 ? $sub._cnvt(cvt, view, sourceValue[0] || sourceValue) // convertVal 3656 : $sub._tag(linkFn._tag, view, view.tmpl, sourceValue, true, onError); // renderTag 3657 3658 addLinkMethods(tag = linkCtx.tag, true); // In both convertVal and renderTag we have instantiated a tag 3659 attr = linkCtx.attr || attr; // linkCtx.attr may have been set to tag.attr during tag instantiation in renderTag 3660 } 3661 3662 if (updateContent(sourceValue, linkCtx, attr, tag) 3663 && eventArgs 3664 && (onEvent = view.hlp(onAfterChangeStr))) { 3665 onEvent.call(linkCtx, ev, eventArgs); 3666 } 3667 linkCtx._noUpd = 0; // For data-link="^{...}" remove _noUpd flag so updates on subsequent calls 3668 3669 if (tag) { 3670 tag._er = hasError; 3671 callAfterLink(tag, ev, eventArgs); 3672 } 3673 } 3674 3675 observeAndBind(linkCtx, source, target); 3676 3677 // Remove dynamically added linkCtx from view 3678 view.linkCtx = oldLinkCtx; 3679 } 3680} 3681 3682function getTargetVal(sourceValue, linkCtx, tag, attr) { 3683 var currentValue, setter, css, $target, 3684 target = tag && tag.parentElem || linkCtx.elem; 3685 3686 if (sourceValue !== undefined) { 3687 $target = $(target); 3688 attr = tag && tag.attr || attr; 3689 if ($isFunction(sourceValue)) { 3690 error(linkCtx.expr + ": missing parens"); 3691 } 3692 3693 if (css = /^css-/.test(attr) && attr.slice(4)) { 3694 currentValue = $.style(target, css); 3695 if (+sourceValue === sourceValue) { 3696 // Optimization for perf on integer values - e.g. css-width{:width+'px'} 3697 currentValue = parseInt(currentValue); 3698 } 3699 } else if (attr !== "link") { // attr === "link" is for tag controls which do data binding but have no rendered output or target 3700 if (attr === "value") { 3701 if (target.type === CHECKBOX) { 3702 currentValue = $target.prop(attr = CHECKED); 3703 } 3704 } else if (attr === RADIO) { 3705 if (target.value === ("" + sourceValue)) { 3706 currentValue = $target.prop(CHECKED); 3707 } else { 3708 return attr; 3709 } 3710 } 3711 3712 if (currentValue === undefined) { 3713 setter = fnSetters[attr]; 3714 currentValue = setter ? $target[setter]() : $target.attr(attr); 3715 } 3716 } 3717 linkCtx._val = currentValue; 3718 } 3719 return attr; 3720} 3721 3722function setDefer(elem, value) { 3723 elem._df = value; // Use both an expando and an attribute to track defered tokens. Attribute is needed for querySelectorAll for getViewInfos (childTags) 3724 elem[(value ? "set" : "remove") + "Attribute"](deferAttr, ""); 3725} 3726 3727function updateContent(sourceValue, linkCtx, attr, tag) { 3728 // When called for a tag, either in tag.refresh() or propertyChangeHandler(), returns a promise (and supports async) 3729 // When called (in propertyChangeHandler) for target HTML returns true 3730 // When called (in propertyChangeHandler) for other targets returns boolean for "changed" 3731 var setter, prevNode, nextNode, promise, nodesToRemove, useProp, tokens, id, openIndex, closeIndex, testElem, nodeName, cStyle, jsvSel, 3732 renders = attr !== NONE && sourceValue !== undefined && !linkCtx._noUpd, // For data-link="^{...}", don't update the first time (no initial render) - e.g. to leave server rendered values. 3733 source = linkCtx.data, 3734 target = tag && tag.parentElem || linkCtx.elem, 3735 targetParent = target.parentNode, 3736 $target = $(target), 3737 view = linkCtx.view, 3738 targetVal = linkCtx._val, 3739 oldLinkCtx = view.linkCtx, 3740 // If not a tag we can use the ._val obtained from getTargetVal() 3741 // and only update when the new value (sourceValue) has changed from the previous one 3742 change = tag; 3743 3744 if (tag) { 3745 // Initialize the tag with element references 3746 tag._.unlinked = true; // Set to unlinked, so initialization is triggered after re-rendering, e.g. for setting linkedElem, and calling onBind 3747 tag.parentElem = tag.parentElem || (linkCtx.expr || tag._elCnt) ? target : targetParent; 3748 prevNode = tag._prv; 3749 nextNode = tag._nxt; 3750 } 3751 if (!renders) { 3752 return; 3753 } 3754 3755 if (attr === "visible") { 3756 attr = "css-display"; 3757 } 3758 if (/^css-/.test(attr)) { 3759 if (linkCtx.attr === "visible") { 3760 // Get the current display style 3761 cStyle = (target.currentStyle || getComputedStyle.call(global, target, "")).display; 3762 3763 if (sourceValue) { 3764 // We are showing the element. 3765 // Get the cached 'visible' display value from the -jsvd expando 3766 sourceValue = target._jsvd 3767 // Or, if not yet cached, get the current display value 3768 || cStyle; 3769 if (sourceValue === NONE && !(sourceValue = displayStyles[nodeName = target.nodeName])) { 3770 // Currently display value is 'none', and the 'visible' style has not been cached. 3771 // We create an element to find the correct 'visible' display style for this nodeName 3772 testElem = document.createElement(nodeName); 3773 document.body.appendChild(testElem); 3774 3775 // Get the default style for this HTML tag to use as 'visible' style 3776 sourceValue 3777 // and cache it as a hash against nodeName 3778 = displayStyles[nodeName] 3779 = (testElem.currentStyle || getComputedStyle.call(global, testElem, "")).display; 3780 document.body.removeChild(testElem); 3781 } 3782 } else { 3783 // We are hiding the element. 3784 // Cache the current display value as 'visible' style, on _jsvd expando, for when we show the element again 3785 target._jsvd = cStyle; 3786 sourceValue = NONE; // Hide the element 3787 } 3788 } 3789 if (change = change || targetVal !== sourceValue) { 3790 $.style(target, attr.slice(4), sourceValue); 3791 } 3792 } else if (attr !== "link") { // attr === "link" is for tag controls which do data binding but have no rendered output or target 3793 if (/^data-/.test(attr)) { 3794 $.data(target, attr.slice(5), sourceValue);
3794 // Support for binding to data attributes: data-foo{:expr}: data-foo attribute will be 3795 // expr.toString(), but $.data(element, "foo") and $(element).data("foo") will actually return value of expr, even if of type object 3796 } 3797 if (attr === CHECKED) { 3798 useProp = true; 3799 sourceValue = sourceValue && sourceValue !== "false"; 3800 // The string value "false" can occur with data-link="checked{attr:expr}" - as a result of attr, and hence using convertVal() 3801 // We will set the "checked" property 3802 // We will compare this with the current value 3803 } else if (attr === RADIO) { 3804 // This is a special binding attribute for radio buttons, which corresponds to the default 'to' binding. 3805 // This allows binding both to value (for each input) and to the default checked radio button (for each input in named group, 3806 // e.g. binding to parent data). 3807 // Place value binding first: <input type="radio" data-link="value{:name} {:#get('data').data.currency:} " .../> 3808 // or (allowing any order for the binding expressions): 3809 // <input type="radio" value="{{:name}}" data-link="{:#get('data').data.currency:} value^{:name}" .../> 3810 3811 if (target.value === ("" + sourceValue)) { 3812 // If the data value corresponds to the value attribute of this radio button input, set the checked property to true 3813 sourceValue = useProp = true; 3814 attr = CHECKED; 3815 } else { 3816 // Otherwise, go straight to observeAndBind, without updating. 3817 // (The browser will remove the 'checked' attribute, when another radio button in the group is checked). 3818 observeAndBind(linkCtx, source, target); 3819 return; 3820 } 3821 } else if (attr === "selected" || attr === "disabled" || attr === "multiple" || attr === "readonly") { 3822 sourceValue = (sourceValue && sourceValue !== "false") ? attr : null; 3823 // Use attr, not prop, so when the options (for example) are changed dynamically, but include the previously selected value, 3824 // they will still be selected after the change 3825 } else if (attr === "value" && target.nodeName === "SELECT") { 3826 target._jsvSel = $isArray(sourceValue) 3827 ? sourceValue 3828 : "" + sourceValue; // If not array, coerce to string 3829 } 3830 3831 if (setter = fnSetters[attr]) { 3832 if (attr === HTML) { 3833 // Set linkCtx on view, dynamically, just during this handler call 3834 view.linkCtx = linkCtx; 3835 if (tag && tag._.inline) { 3836 nodesToRemove = tag.nodes(true); 3837 if (tag._elCnt) { 3838 if (prevNode && prevNode !== nextNode) { 3839 // This prevNode will be removed from the DOM, so transfer the view tokens on prevNode to nextNode of this 'viewToRefresh' 3840 transferViewTokens(prevNode, nextNode, target, tag._tgId, "^", true); 3841 } else if (tokens = target._df) { // This occurs when there is no nextNode, and so the target._df may include tokens referencing 3842 // view and tag bindings contained within the open and close tokens of the updated tag control. They need to be processed (disposed) 3843 id = tag._tgId + "^"; 3844 openIndex = tokens.indexOf("#" + id) + 1; 3845 closeIndex = tokens.indexOf("/" + id); 3846 3847 if (openIndex && closeIndex > 0) { 3848 openIndex += id.length; 3849 if (closeIndex > openIndex) { 3850 setDefer(target, tokens.slice(0, openIndex) + tokens.slice(closeIndex)); 3851 disposeTokens(tokens.slice(openIndex, closeIndex)); 3852 } 3853 } 3854 } 3855 prevNode = prevNode 3856 ? prevNode.previousSibling 3857 : nextNode 3858 ? nextNode.previousSibling 3859 : target.lastChild; 3860 } 3861 // Remove HTML nodes 3862 $(nodesToRemove).remove(); // Note if !tag._elCnt removing the nodesToRemove will process and dispose view and tag bindings contained within the updated tag control 3863 3864 // Insert and link new content 3865 promise = view.link(view.data, target, prevNode, nextNode, sourceValue, tag && {tag: tag._tgId, lazyLink: tag.tagCtx.props.lazyLink}); 3866 } else { 3867 // data-linked value targeting innerHTML: data-link="html{:expr}" or contenteditable="true" 3868 renders = renders && targetVal !== sourceValue; 3869 if (renders) { 3870 $target.empty(); 3871 } 3872 if (renders) { 3873 promise = view.link(source, target, prevNode, nextNode, sourceValue, tag && {tag: tag._tgId}); 3874 } 3875 } 3876 // Remove dynamically added linkCtx and ctx from view 3877 view.linkCtx = oldLinkCtx; 3878 } else { 3879 if (change = change || targetVal !== sourceValue) { 3880 if (attr === "text" && target.children && !target.children[0]) { 3881 // This code is faster then $target.text() 3882 if (target.textContent !== undefined) { 3883 target.textContent = sourceValue; 3884 } else {
3885 target.innerText = sourceValue === null ? "" : sourceValue; 3886 } 3887 } else { 3888 $target[setter](sourceValue); 3889 } 3890 } 3891 if ((jsvSel = targetParent._jsvSel) 3892 // Setting value of <option> element 3893 && (attr === "value" || !$target.attr("value"))) { // Setting value attribute, or setting textContent if attribute is null 3894 // Set/unselect selection based on value set on parent <select>. Works for multiselect too 3895 target.selected = $.inArray("" + sourceValue, $isArray(jsvSel) ? jsvSel : [jsvSel]) > -1; 3896 } 3897 } 3898 } else if (change = change || targetVal !== sourceValue) { 3899 // Setting an attribute to undefined should remove the attribute 3900 $target[useProp ? "prop" : "attr"](attr, sourceValue === undefined && !useProp ? null : sourceValue); 3901 } 3902 linkCtx._val = sourceValue; 3903 } 3904 return promise || change; 3905} 3906 3907function arrayChangeHandler(ev, eventArgs) { 3908 var self = this, 3909 onBeforeChange = self.hlp(onBeforeChangeStr), 3910 onAfterChange = self.hlp(onAfterChangeStr); 3911 if (!onBeforeChange || onBeforeChange.call(this, ev, eventArgs) !== false) { 3912 if (eventArgs) { 3913 // This is an observable action (not a trigger/handler call from pushValues, or similar, for which eventArgs will be null) 3914 var action = eventArgs.change, 3915 index = eventArgs.index, 3916 items = eventArgs.items; 3917 self._.srt = eventArgs.refresh; // true if part of a 'sort' on refresh 3918 switch (action) { 3919 case "insert": 3920 self.addViews(index, items); 3921 break; 3922 case "remove": 3923 self.removeViews(index, items.length); 3924 break; 3925 case "move": 3926 self.removeViews(eventArgs.oldIndex, items.length, undefined, true); // remove 3927 self.addViews(index, items); // re-insert 3928 break; 3929 case "refresh": 3930 self._.srt = undefined; 3931 self.fixIndex(0); 3932 // Other cases: (e.g.undefined, for setProperty on observable object) etc. do nothing 3933 } 3934 } 3935 if (onAfterChange) { 3936 onAfterChange.call(this, ev, eventArgs); 3937 } 3938 } 3939} 3940 3941//============================= 3942// Utilities for event handlers 3943//============================= 3944 3945function setArrayChangeLink(view) { 3946 // Add/remove arrayChange handler on view 3947 var handler, arrayBinding, 3948 type = view.type, // undefined if view is being removed 3949 data = view.data, 3950 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) 3951 3952 if (!view._.useKey && bound) { 3953 // This is an array view. (view._.useKey not defined => data is array), and is data-bound to collection change events 3954 3955 if (arrayBinding = view._.bndArr) { 3956 // First remove the current handler if there is one 3957 $([arrayBinding[1]]).off(arrayChangeStr, arrayBinding[0]); 3958 view._.bndArr = undefined; 3959 } 3960 if (bound !== !!bound) { 3961 // bound is not a boolean, so it is the data-linked tag that 'owns' this array binding - e.g. {^{for...}} 3962 if (type) { 3963 bound._.arrVws[view._.id] = view; 3964 } else { 3965 delete bound._.arrVws[view._.id]; // if view.type is undefined, view is being removed 3966 } 3967 } else if (type && data) { 3968 // If this view is not being removed, but the data array has been replaced, then bind to the new data array 3969 handler = function(ev) { 3970 if (!(ev.data && ev.data.off)) { 3971 // 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) 3972 // If view.type is undefined, do nothing. (Corresponds to case where there is another handler on the same data whose 3973 // effect was to remove this view, and which happened to precede this event in the trigger sequence. So although this 3974 // event has been removed now, it is still called since already on the trigger sequence) 3975 arrayChangeHandler.apply(view, arguments); 3976 } 3977 }; 3978 $([data]).on(arrayChangeStr, handler); 3979 view._.bndArr = [handler, data]; 3980 } 3981 } 3982} 3983 3984function defaultAttr(elem, to, linkGetVal) { 3985 // to: true - default attribute for setting data value on HTML element; false: default attribute for getting value from HTML element 3986 // Merge in the default attribute bindings for this target element 3987 var nodeName = elem.nodeName.toLowerCase(), 3988 attr = 3989 $subSettingsAdvanced._fe[nodeName] // get form element binding settings for input textarea select or optgroup 3990 || elem.contentEditable === TRUE && {to: HTML, from: HTML}; // Or if contentEditable set to "true" set attr to "html" 3991 return attr 3992 ? (to 3993 ? ((nodeName === "input" && elem.type === RADIO) // For radio buttons, bind from value, but bind to 'radio' - special value. 3994 ? RADIO 3995 : attr.to) 3996 : attr.from) 3997 : to 3998 ? linkGetVal ? "text" : HTML // Default innerText for data-link="a.b.c" or data-link="{:a.b.c}" (with or without converters)- otherwise innerHTML 3999 : ""; // Default is not to bind from 4000} 4001 4002//============================== 4003// Rendering and DOM insertion 4004//============================== 4005 4006function renderAndLink(view, index, tmpl, views, data, context, refresh) { 4007 var html, linkToNode, prevView, nodesToRemove, bindId, 4008 parentNode = view.parentElem, 4009 prevNode = view._prv, 4010 nextNode = view._nxt, 4011 elCnt = view._elCnt; 4012 4013 if (prevNode && prevNode.parentNode !== parentNode) { 4014 error("Missing parentNode"); 4015 // Abandon, since node has already been removed, or wrapper element has been inserted between prevNode and parentNode 4016 } 4017 4018 if (refresh) { 4019 nodesToRemove = view.nodes(); 4020 if (elCnt && prevNode && prevNode !== nextNode) { 4021 // This prevNode will be removed from the DOM, so transfer the view tokens on prevNode to nextNode of this 'viewToRefresh' 4022 transferViewTokens(prevNode, nextNode, parentNode, view._.id, "_", true); 4023 } 4024 // Remove child views 4025 view.removeViews(undefined, undefined, true); 4026 linkToNode = nextNode; 4027 4028 if (elCnt) { 4029 prevNode = prevNode 4030 ? prevNode.previousSibling 4031 : nextNode 4032 ? nextNode.previousSibling 4033 : parentNode.lastChild; 4034 } 4035 4036 // Remove HTML nodes 4037 $(nodesToRemove).remove(); 4038 4039 for (bindId in view._.bnds) { 4040 // The view bindings may have already been removed above in: $(nodesToRemove).remove(); 4041 // If not, remove them here: 4042 removeViewBinding(bindId); 4043 } 4044 } else {
4045 // addViews. Only called if view is of type "array" 4046 if (index) { 4047 // index is a number, so indexed view in view array 4048 prevView = views[index - 1]; 4049 if (!prevView) { 4050 return false; // If subview for provided index does not exist, do nothing 4051 } 4052 prevNode = prevView._nxt; 4053 } 4054 if (elCnt) { 4055 linkToNode = prevNode; 4056 prevNode = linkToNode 4057 ? linkToNode.previousSibling // There is a linkToNode, so insert after previousSibling, or at the beginning 4058 : parentNode.lastChild; // If no prevView and no prevNode, index is 0 and the container is empty, 4059 // so prevNode = linkToNode = null. But if prevView._nxt is null then we set prevNode to parentNode.lastChild 4060 // (which must be before the prevView) so we insert after that node - and only link the inserted nodes 4061 } else { 4062 linkToNode = prevNode.nextSibling; 4063 } 4064 } 4065 html = tmpl.render(data, context, view._.useKey && refresh, view, refresh || index, true); 4066 // Pass in view._.useKey as test for noIteration (which corresponds to when self._.useKey > 0 and self.data is an array) 4067 4068 // Link the new HTML nodes to the data 4069 view.link(data, parentNode, prevNode, linkToNode, html, prevView); 4070//}, 0); 4071} 4072 4073//===================== 4074// addBindingMarkers 4075//===================== 4076 4077function addBindingMarkers(value, view, tag) { 4078 // Insert binding markers into the rendered template output, which will get converted to appropriate 4079 // data-jsv attributes (element-only content) or script marker nodes (phrasing or flow content), in convertMarkers, 4080 // within view.link, prior to inserting into the DOM. Linking will then bind based on these markers in the DOM. 4081 // Added view markers: #m_...VIEW.../m_ 4082 // Added tag markers: #m^...TAG..../m^ 4083 var id, end; 4084 if (tag) { 4085 // This is a binding marker for a data-linked tag {^{...}} 4086 end = "^`"; 4087 addLinkMethods(tag, true); // This is {^{>...}} or {^{tag ...}}, {{cvt:...} or {^{:...}}, and tag was defined in convertVal or renderTag 4088 id = tag._tgId; 4089 if (!id) { 4090 bindingStore[id = bindingKey++] = tag; // Store the tag temporarily, ready for databinding. 4091 // During linking, in addDataBinding, the tag will be attached to the linkCtx, 4092 // and then in observeAndBind, bindingStore[bindId] will be replaced by binding info. 4093 tag._tgId = "" + id; 4094 } 4095 } else { 4096 // This is a binding marker for a view 4097 // Add the view to the store of current linked views 4098 end = "_`"; 4099 addLinkMethods(viewStore[id = view._.id] = view); 4100 } 4101 // Example: "#23^TheValue/23^" 4102 return "#" + id + end 4103 + (value != undefined ? value : "") // For {^{:name}} this gives the equivalent semantics to compiled 4104 // (v=data.name)!=null?v:""; used in {{:name}} or data-link="name" 4105 + "/" + id + end; 4106} 4107 4108//============================== 4109// Data-linking and data binding 4110//============================== 4111 4112//--------------- 4113// observeAndBind 4114//--------------- 4115 4116function observeAndBind(linkCtx, source, target) { //TODO? linkFnArgs) {; 4117 var binding, l, linkedElem, exprFnDeps, exprOb, 4118 tag = linkCtx.tag, 4119 cvtBk = linkCtx.convertBack, 4120 depends = [], 4121 bindId = linkCtx._bndId || "" + bindingKey++, 4122 handler = linkCtx._hdl; 4123 4124 linkCtx._bndId = undefined; 4125 4126 if (tag) { 4127 // Use the 'depends' paths set on linkCtx.tag - which may have been set on declaration 4128 // or in events: init, render, onAfterLink etc. 4129 depends = tag.depends || depends; 4130 depends = $isFunction(depends) ? tag.depends(tag) : depends; 4131 linkedElem = tag.linkedElem; 4132 } 4133 if (!linkCtx._depends || ("" + linkCtx._depends !== "" + depends)) { 4134 // Only bind the first time, or if the new depends (toString) has changed from when last bound 4135 if (linkCtx._depends) { 4136 // Unobserve previous binding 4137 $observable._apply(false, [source], linkCtx._depends, handler, true); 4138 } 4139 4140 exprFnDeps = linkCtx.fn.deps.slice(); // Make a copy of the dependency paths for the compiled linkCtx expression - to pass to observe(). In getInnerCb(), 4141 // (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. 4142 l = exprFnDeps.length; 4143 while (l--) { 4144 exprOb = exprFnDeps[l]; 4145 if (exprOb._jsv) { 4146 // This path is an 'exprOb', corresponding to a computed, returning an object. We replace the exprOb by 4147 // a view-binding-specific exprOb instance. The current object will be stored as exprOb.ob. 4148 exprFnDeps[l] = $extend({}, exprOb); 4149 } 4150 } 4151 4152 binding = $observable._apply( 4153 false, 4154 [source], 4155 exprFnDeps, // flatten the paths - to gather all the dependencies across args and bound params 4156 depends, 4157 handler, 4158 linkCtx._ctxCb);
4159 // The binding returned by $observe has a bnd array with the source objects of the individual bindings. 4160 4161 binding.elem = target; // The target of all the individual bindings 4162 binding.linkCtx = linkCtx; 4163 binding._tgId = bindId; 4164 4165 // Add to the _jsvBnd on the target the view id and binding id - for unbinding when the target element is removed 4166 target._jsvBnd = target._jsvBnd || ""; 4167 target._jsvBnd += "&" + bindId; 4168 linkCtx._depends = depends; 4169 // Store the binding key on the view, for disposal when the view is removed 4170 linkCtx.view._.bnds[bindId] = bindId; 4171 // Store the binding. 4172 bindingStore[bindId] = binding; // Note: If this corresponds to a data-linked tag, we are replacing the 4173 // temporarily stored tag by the stored binding. The tag will now be at binding.linkCtx.tag 4174 4175 if (linkedElem) { 4176 binding.to = [[], cvtBk]; 4177 } 4178 if (linkedElem || cvtBk !== undefined) { 4179 bindTo(binding, tag, linkedElem && linkedElem[0] || target, cvtBk); 4180 } 4181 if (tag) { 4182 if (!tag.flow && !tag._.inline) { 4183 target.setAttribute(jsvAttrStr, (target.getAttribute(jsvAttrStr)||"") + "#" + bindId + "^/" + bindId + "^"); 4184 tag._tgId = "" + bindId; 4185 } 4186 } 4187 } 4188 if (linkedElem && linkedElem[0]) { 4189 l = linkedElem.length; 4190 while (l--) { 4191 linkedElem[l]._jsvBnd = linkedElem[l]._jsvBnd || (target._jsvBnd + "+"); 4192 // Add a "+" for cloned binding - so removing elems with cloned bindings will not remove the 'parent' binding from the bindingStore. 4193 linkedElem[l]._jsvLkEl = tag; 4194 } 4195 } 4196} 4197 4198//------- 4199// $.link 4200//------- 4201 4202function tmplLink(to, from, context, noIteration, parentView, prevNode, nextNode) { 4203 return $link(this, to, from, context, noIteration, parentView, prevNode, nextNode); 4204} 4205 4206function $link(tmplOrLinkExpr, to, from, context, noIteration, parentView, prevNode, nextNode) { 4207 // When linking from a template, prevNode and nextNode parameters are ignored 4208 if (context === true) { 4209 noIteration = context; // passing boolean as third param - noIteration 4210 context = undefined; 4211 } else if (typeof context !== "object") { 4212 context = undefined; // context must be a boolean (noIteration) or a plain object 4213 } else { 4214 context = $extend({}, context); 4215 } 4216 if (tmplOrLinkExpr && to) { 4217 to = to.jquery ? to : $(to); // to is a jquery object or an element or selector 4218 4219 if (!activeBody) { 4220 activeBody = document.body; 4221 $(activeBody) 4222 .on(elementChangeStr, elemChangeHandler) 4223 .on('blur', '[contenteditable]', elemChangeHandler); 4224 } 4225 4226 var i, k, html, vwInfos, view, placeholderParent, targetEl, refresh, topLevelCall, 4227 onRender = addBindingMarkers, 4228 replaceMode = context && context.target === "replace", 4229 l = to.length; 4230 4231 while (l--) { 4232 targetEl = to[l]; 4233 4234 parentView = parentView || $view(targetEl); 4235 4236 if (topLevelCall = parentView === topView) { 4237 topView.data = (topView.ctx = context || {}).root = from; 4238 } 4239 if ("" + tmplOrLinkExpr === tmplOrLinkExpr) { 4240 // tmplOrLinkExpr is a string: treat as data-link expression. 4241 addDataBinding(tmplOrLinkExpr, targetEl, parentView, undefined, true, from, context); 4242 } else { 4243 if (tmplOrLinkExpr.markup !== undefined) { 4244 // This is a call to template.link() 4245 if (replaceMode) { 4246 placeholderParent = targetEl.parentNode; 4247 } 4248 4249 html = tmplOrLinkExpr.render(from, context, noIteration, parentView, undefined, onRender); 4250 // TODO Consider finding a way to bind data (link) within template without html being different for each view, the HTML can 4251 // be evaluated once outside the while (l--), and pushed into a document fragment, then cloned and inserted at each target. 4252 4253 if (placeholderParent) { 4254 // This is target="replace" mode 4255 prevNode = targetEl.previousSibling; 4256 nextNode = targetEl.nextSibling; 4257 $.cleanData([targetEl], true); 4258 placeholderParent.removeChild(targetEl); 4259 4260 targetEl = placeholderParent; 4261 } else { 4262 prevNode = nextNode = undefined; // When linking from a template, prevNode and nextNode parameters are ignored 4263 $(targetEl).empty(); 4264 } 4265 } else if (tmplOrLinkExpr === true && parentView === topView) { 4266 // $.link(true, selector, data, ctx) - where selector points to elem in top-level content. (If not top-level content, no-op) 4267 refresh = {lnk: 1}; 4268 } else { 4269 break; 4270 } 4271 4272// TODO Consider deferred linking API feature on per-template basis - {@{ instead of {^{ which allows the user to see the rendered content 4273// before that content is linked, with better perceived perf. Have view.link return a deferred, and pass that to onAfterLink...
4274// or something along those lines. 4275// setTimeout(function() { 4276 4277 if (targetEl._df && !nextNode) { 4278 // We are inserting new content and the target element has some deferred binding annotations,and there is no nextNode. 4279 // Those views may be stale views (that will be recreated in this new linking action) so we will first remove them 4280 // (if not already removed). 4281 vwInfos = viewInfos(targetEl._df, true, rOpenViewMarkers); 4282 4283 for (i = 0, k = vwInfos.length; i < k; i++) { 4284 view = vwInfos[i]; 4285 if ((view = viewStore[view.id]) && view.data !== undefined) { 4286 // If this is the _prv (prevNode) for a view, remove the view 4287 // - unless view.data is undefined, in which case it is already being removed 4288 view.parent.removeViews(view._.key, undefined, true); 4289 } 4290 } 4291 setDefer(targetEl); // remove defer tokens 4292 } 4293 4294 // Link the content of the element, since this is a call to template.link(), or to $(el).link(true, ...), 4295 parentView.link(from, targetEl, prevNode, nextNode, html, refresh, context); 4296//}, 0); 4297 } 4298 if (topLevelCall) { 4299 topView.data = topView.ctx = undefined; 4300 } 4301 } 4302 } 4303 return to; // Allow chaining, to attach event handlers, etc. 4304} 4305 4306//---------- 4307// view.link 4308//---------- 4309 4310function viewLink(outerData, parentNode, prevNode, nextNode, html, refresh, context, validateOnly) { 4311 // Optionally insert HTML into DOM using documentFragments (and wrapping HTML appropriately). 4312 // Data-link existing contents of parentNode, or the inserted HTML, if provided 4313 4314 // Depending on the content model for the HTML elements, the standard data-linking markers inserted in the HTML by addBindingMarkers during 4315 // template rendering will be converted either to script marker nodes or, for element-only content sections, to data-jsv element annotations. 4316 4317 // Data-linking will then add _prv and _nxt to views, where: 4318 // _prv: References the previous node (script element of type "jsv123"), or (for elCnt=true), the first element node in the view (or if none, set _prv = _nxt) 4319 // _nxt: References the last node (script element of type "jsv/123"), or (for elCnt=true), the next element node after the view. 4320 4321 //==== nested functions ==== 4322 function convertMarkers(all, preceding, selfClose, closeTag, spaceBefore, id, boundId, spaceAfter, tag1, tag2, closeTag2, spaceAfterClose, selfClose2, endOpenTag) { 4323 // rConvertMarkers = /(^|(\/>)|<\/(\w+)>|)(\s*)([#\/]\d+(?:_|(\^)))`(\s*)(<\w+(?=[\s\/>]))?|\s*(?:(<\w+(?=[\s\/>]))|<\/(\w+)>(\s*)|(\/>)\s*|(>))/g, 4324 // prec, slfCl, clsTag, spBefore, id, bndId spAfter,tag1, tag2, clTag2,sac slfCl2, endOpenTag 4325 // Convert the markers that were included by addBindingMarkers in template output, to appropriate DOM annotations: 4326 // data-jsv attributes (for element-only content) or script marker nodes (within phrasing or flow content). 4327 4328// TODO consider detecting 'quoted' contexts (attribute strings) so that attribute encoding does not need to encode > 4329// Currently rAttrEncode = /[><"'&]/g includes '>' encoding in order to avoid erroneous parsing of <span title="<a/>"></span>"> 4330 var errorMsg, bndId, 4331 endOfElCnt = ""; 4332 if (endOpenTag) { 4333 inTag = 0; 4334 return all; 4335 } 4336 tag = tag1 || tag2 || ""; 4337 closeTag = closeTag || closeTag2; 4338 selfClose = selfClose || selfClose2; 4339 if (isVoid && !selfClose && ( !all || closeTag || tag || id && !inTag)) { // !all = end of string 4340 isVoid = undefined; 4341 parentTag = tagStack.shift(); // preceding tag was a void element, with no closing slash, such as <br>. 4342 } 4343 closeTag = closeTag || selfClose; 4344 if (closeTag) { 4345 inTag = 0; 4346 isVoid = undefined; 4347 // TODO: smart insertion of <tbody> - to be completed for robust insertion of deferred bindings etc. 4348 //if (closeTag === "table" && parentTag === "tbody") { 4349 // preceding = "</tbody>" + preceding; 4350 // parentTag = "table"; 4351 // tagStack.shift(); 4352 //} 4353 if (validate) { 4354 if (selfClose || selfClose2) { 4355 if (!voidElems[parentTag] && !/;svg;|;math;/.test(";" + tagStack.join(";") + ";")) { 4356 // Only self-closing elements must be legitimate void elements, such as <br/>
4356, per HTML schema, 4357 // or under svg or math foreign namespace elements. 4358 errorMsg = "'<" + parentTag + ".../"; 4359 } 4360 } else if (voidElems[closeTag]) { 4361 errorMsg = "'</" + closeTag; // closing tag such as </input> 4362 } else if (!tagStack.length || closeTag !== parentTag) { 4363 errorMsg = "Mismatch: '</" + closeTag; 4364 } 4365 if (errorMsg) { 4366 syntaxError(errorMsg + ">' in:\n" + html); 4367 } 4368 } 4369 prevElCnt = elCnt; 4370 parentTag = tagStack.shift(); 4371 elCnt = elContent[parentTag]; 4372 closeTag2 = closeTag2 ? ("</" + closeTag2 + ">") : ""; 4373 if (prevElCnt) { 4374 // If there are ids (markers since the last tag), move them to the defer string 4375 defer += ids; 4376 ids = ""; 4377 if (!elCnt) { 4378 endOfElCnt = closeTag2 + openScript + "@" + defer + closeScript + (spaceAfterClose || ""); 4379 defer = deferStack.shift(); 4380 } else { 4381 defer += "-"; // Will be used for stepping back through deferred tokens 4382 } 4383 } 4384 } 4385 if (elCnt) { 4386 // elContent maps tagNames which have only element content, so may not support script nodes. 4387 // We are in element-only content, can remove white space, and use data-jsv attributes on elements as markers 4388 // Example: <tr data-jsv="/2_#6_"> - close marker for view 2 and open marker for view 6 4389 4390 if (id) { 4391 // append marker for this id, to ids string 4392 ids += id; 4393 } else { 4394 preceding = (closeTag2 || selfClose2 || ""); 4395 } 4396 if (tag) { 4397 // TODO: smart insertion of <tbody> - to be completed for robust insertion of deferred bindings etc. 4398 //if (tag === "<tr" && parentTag === "table") { 4399 // tagStack.unshift(parentTag); 4400 // parentTag = "tbody"; 4401 // preceding += "<" + parentTag + ">"; 4402 // if (defer) { 4403 // defer += "+"; // Will be used for stepping back through deferred tokens 4404 // } 4405 // // TODO: move this to design-time validation check 4406 // // error('"' + parentTag + '" has incorrect parent tag'); 4407 //} 4408 preceding += tag; 4409 if (ids) { 4410 preceding += ' ' + jsvAttrStr + '="' + ids + '"'; 4411 ids = ""; 4412 } 4413 } 4414 } else { 4415 // We are in phrasing or flow content, so use script marker nodes 4416 // Example: <script type="jsv3/"></script> - data-linked tag, close marker 4417 // We validate with inTag so no script markers are inserted in attribute context e.g. for: 4418 // "<table {{if ...}}...{{/if}}... >" or "<table {{if ...}}...> ...{{/if}}..." 4419 preceding = id 4420 ? (preceding + endOfElCnt + spaceBefore + (inTag ? "" : openScript + id + closeScript)+ spaceAfter + tag) 4421 : endOfElCnt || all; 4422 } 4423 4424 if (validate && boundId) { 4425 if (inTag) { 4426 // JsViews data-linking tags are not allowed within element markup. 4427 // See jsviews/issues/303 4428 syntaxError('{^{ within elem markup (' + inTag + ' ). Use data-link="..."'); 4429 } 4430 if (id.charAt(0) === "#") { 4431 tagStack.unshift(id.slice(1)); 4432 } else if (id.slice(1) !== (bndId = tagStack.shift())) { 4433 // See jsviews/issues/213 4434 syntaxError('Closing tag for {^{...}} under different elem: <' + bndId + '>'); 4435 } 4436 } 4437 if (tag) { 4438 inTag = tag; 4439 // If there are ids (markers since the last tag), move them to the defer string 4440 tagStack.unshift(parentTag); 4441 parentTag = tag.slice(1); 4442 if (validate && tagStack[0] && tagStack[0] === badParent[parentTag]) { 4443 // Missing <tbody> 4444 // TODO: replace this by smart insertion of <tbody> tags 4445 error('Parent of <tr> must be <tbody>'); 4446 } 4447 isVoid = voidElems[parentTag]; 4448 if ((elCnt = elContent[parentTag]) && !prevElCnt) { 4449 deferStack.unshift(defer); 4450 defer = ""; 4451 } 4452 prevElCnt = elCnt; 4453//TODO Consider providing validation which throws if you place <span> as child of <tr>, etc. - since if not caught, 4454//this can cause errors subsequently which are difficult to debug. 4455// if (elContent[tagStack[0]]>2 && !elCnt) { 4456// error(parentTag + " in " + tagStack[0]); 4457// } 4458 if (defer && elCnt) { 4459 defer += "+"; // Will be used for stepping back through deferred tokens 4460 } 4461 } 4462 return preceding; 4463 } 4464 4465 function processViewInfos(vwInfos, targetParent) { 4466 // If targetParent, we are processing viewInfos (which may include navigation through '+-' paths) and hooking up to the right parentElem etc.
4467 // (and elem may also be defined - the next node) 4468 // If no targetParent, then we are processing viewInfos on newly inserted content 4469 var deferPath, deferChar, bindChar, parentElem, id, onAftCr, deep, 4470 addedBindEls = []; 4471 4472 // In elCnt context (element-only content model), prevNode is the first node after the open, nextNode is the first node after the close. 4473 // 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 4474 // '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 4475 // its placeholder is the 'previousSibling' of the prevNode, which is also the nextNode. 4476 if (vwInfos) { 4477 if (vwInfos._tkns.charAt(0) === "@") { 4478 // We are processing newly inserted content. This is a special script element that was created in convertMarkers() to process deferred bindings, 4479 // and inserted following the target parent element - because no element tags (outside elCnt) were encountered to carry those binding tokens. 4480 // We will step back from the preceding sibling of this element, looking at targetParent elements until we find the one that the current binding 4481 // token belongs to. Set elem to null (the special script element), and remove it from the DOM. 4482 targetParent = elem.previousSibling; 4483 elem.parentNode.removeChild(elem); 4484 elem = undefined; 4485 } 4486 len = vwInfos.length; 4487 while (len--) { 4488 vwInfo = vwInfos[len]; 4489//if (prevIds.indexOf(vwInfo.token) < 0) { // This token is a newly created view or tag binding 4490 bindChar = vwInfo.ch; 4491 if (deferPath = vwInfo.path) { 4492 // We have a 'deferred path' 4493 j = deferPath.length - 1; 4494 while (deferChar = deferPath.charAt(j--)) { 4495 // Use the "+" and"-" characters to navigate the path back to the original parent node where the deferred bindings ocurred 4496 if (deferChar === "+") { 4497 if (deferPath.charAt(j) === "-") { 4498 j--; 4499 targetParent = targetParent.previousSibling; 4500 } else { 4501 targetParent = targetParent.parentNode; 4502 } 4503 } else { 4504 targetParent = targetParent.lastChild; 4505 } 4506 // Note: Can use previousSibling and lastChild, not previousElementSibling and lastElementChild, 4507 // since we have removed white space within elCnt. Hence support IE < 9 4508 } 4509 } 4510 if (bindChar === "^") { 4511 if (tag = bindingStore[id = vwInfo.id]) { 4512 // The binding may have been deleted, for example in a different handler to an array collectionChange event 4513 // This is a tag binding 4514 deep = targetParent && (!elem || elem.parentNode !== targetParent); // We are stepping back looking for the right targetParent, 4515 // or we are linking existing content and this element is in elCnt, not an immediate child of the targetParent. 4516 if (!elem || deep) { 4517 tag.parentElem = targetParent; 4518 } 4519 if (vwInfo.elCnt && deep) { 4520 // With element only content, if there is no following element, or if the binding is deeper than the following element 4521 // then we need to set the open or close token as a deferred binding annotation on the parent 4522 setDefer(targetParent, (vwInfo.open ? "#" : "/") + id + bindChar + (targetParent._df || "")); 4523 } 4524 // This is an open or close marker for a data-linked tag {^{...}}. Add it to bindEls. 4525 addedBindEls.push([deep ? null : elem, vwInfo]); 4526 } 4527 } else if (view = viewStore[id = vwInfo.id]) { 4528 // The view may have been deleted, for example in a different handler to an array collectionChange event 4529 if (!view.parentElem) { 4530 // If view is not already extended for JsViews, extend and initialize the view object created in JsRender, as a JsViews view 4531 view.parentElem = targetParent || elem && elem.parentNode || parentNode; 4532 view._.onRender = addBindingMarkers; 4533 view._.onArrayChange = arrayChangeHandler; 4534 setArrayChangeLink(view); 4535 } 4536 parentElem = view.parentElem;
4537 if (vwInfo.open) { 4538 // This is an 'open view' node (preceding script marker node, 4539 // or if elCnt, the first element in the view, with a data-jsv annotation) for binding 4540 view._elCnt = vwInfo.elCnt; 4541 if (targetParent && !elem) { 4542 setDefer(targetParent, "#" + id + bindChar + (targetParent._df || "")); 4543 } else { 4544 // No targetParent, so there is a ._nxt elem (and this is processing tokens on the elem) 4545 if (!view._prv) { 4546 setDefer(parentElem, removeSubStr(parentElem._df, "#" + id + bindChar)); 4547 } 4548 view._prv = elem; 4549 } 4550 } else { 4551 // This is a 'close view' marker node for binding 4552 if (targetParent && (!elem || elem.parentNode !== targetParent)) { 4553 // There is no ._nxt so add token to _df. It is deferred. 4554 setDefer(targetParent, "/" + id + bindChar + (targetParent._df || "")); 4555 view._nxt = undefined; 4556 } else if (elem) { 4557 // This view did not have a ._nxt, but has one now, so token may be in _df, and must be removed. (No longer deferred) 4558 if (!view._nxt) { 4559 setDefer(parentElem, removeSubStr(parentElem._df, "/" + id + bindChar)); 4560 } 4561 view._nxt = elem; 4562 } 4563 linkCtx = view.linkCtx; 4564 if (onAftCr = view.ctx && view.ctx.onAfterCreate || onAfterCreate) { 4565 onAftCr.call(linkCtx, view); 4566 } 4567 } 4568//} 4569 } 4570 } 4571 len = addedBindEls.length; 4572 while (len--) { 4573 // These were added in reverse order to addedBindEls. We push them in BindEls in the correct order. 4574 bindEls.push(addedBindEls[len]); 4575 } 4576 } 4577 return !vwInfos || vwInfos.elCnt; 4578 } 4579 4580 function getViewInfos(vwInfos) { 4581 // Used by view.childTags() and tag.childTags() 4582 // Similar to processViewInfos in how it steps through bindings to find tags. Only finds data-linked tags. 4583 var level, parentTag, named; 4584 4585 if (vwInfos) { 4586 len = vwInfos.length; 4587 for (j = 0; j < len; j++) { 4588 vwInfo = vwInfos[j]; 4589 // This is an open marker for a data-linked tag {^{...}}, within the content of the tag whose id is get.id. Add it to bindEls. 4590 // Note - if bindingStore[vwInfo.id]._is === "tag" then getViewInfos is being called too soon - during first linking pass 4591 parentTag = tag = bindingStore[vwInfo.id].linkCtx.tag; 4592 named = tag.tagName === tagName; 4593 if (!tag.flow || named) { 4594 if (!deep) { 4595 level = 1; 4596 while (parentTag = parentTag.parent) { 4597 level++; 4598 } 4599 tagDepth = tagDepth || level; // The level of the first tag encountered. 4600 } 4601 if ((deep || level === tagDepth) && (!tagName || named)) { 4602 // Filter on top-level or tagName as appropriate 4603 tags.push(tag); 4604 } 4605 } 4606 } 4607 } 4608 } 4609 4610 function dataLink() { 4611 //================ Data-link and fixup of data-jsv annotations ================ 4612 var j, index, 4613 tokens = "", 4614 wrap = {}, 4615 selector = linkViewsSel + (get ? ",[" + deferAttr + "]" : ""); 4616 // If a childTags() call, get = ",[" + deferAttr + "]" - since we need to include elements that have a ._df expando for deferred tokens 4617 4618 elems = qsa ? parentNode.querySelectorAll(selector) : $(selector, parentNode).get(); 4619 l = elems.length; 4620 4621 // The prevNode will be in the returned query, since we called markPrevOrNextNode() on it. 4622 // But it may have contained nodes that satisfy the selector also. 4623 if (prevNode && prevNode.innerHTML) { 4624 // Find the last contained node of prevNode, to use as the prevNode - so we only link subsequent elems in the query 4625 prevNodes = qsa ? prevNode.querySelectorAll(selector) : $(selector, prevNode).get(); 4626 prevNode = prevNodes.length ? prevNodes[prevNodes.length - 1] : prevNode; 4627 } 4628 4629 tagDepth = 0; 4630 for (i = 0; i < l; i++) { 4631 elem = elems[i]; 4632 if (prevNode && !found) { 4633 // If prevNode is set, not false, skip linking. If this element is the prevNode, set to false so subsequent elements will link. 4634 found = (elem === prevNode); 4635 } else if (nextNode && elem === nextNode) { 4636 // If nextNode is set then break when we get to nextNode 4637 if (get) { 4638 tokens += markerNodeInfo(elem); 4639 } 4640 break; 4641 } else if (elem.parentNode) { 4642 // elem has not been removed from DOM 4643 if (get) { 4644 tokens += markerNodeInfo(elem); 4645 if (elem._df) { 4646 j = i + 1; 4647 while (j < l && elem.contains(elems[j])) { 4648 j++; 4649 } 4650 // Add defered tokens after any tokens on descendant elements of this one 4651 wrap[j-1] = elem._df; 4652 } 4653 if (wrap[i]) { 4654 tokens += wrap[i] || ""; 4655 } 4656 } else {
4657 if (isLink && (vwInfo = viewInfos(elem, undefined, rViewMarkers)) && (vwInfo = vwInfo[0])) { 4658 // If this is a link(trueOrString ...) call we will avoid re-binding to elems that are within template-rendered views 4659 skip = skip ? (vwInfo.id !== skip && skip) : vwInfo.open && vwInfo.id; 4660 } 4661 if (!skip && processInfos(viewInfos(elem)) 4662 // If a link() call, processViewInfos() adds bindings to bindEls, and returns true for non-script nodes, for adding data-link bindings 4663 // If a childTags() call, getViewInfos returns array of tag bindings. 4664 && elem.getAttribute($viewsLinkAttr)) { 4665 bindEls.push([elem]); // A data-linked element so add to bindEls too 4666 } 4667 } 4668 } 4669 } 4670 4671 if (get) { 4672 tokens += parentNode._df || ""; 4673 if (index = tokens.indexOf("#" + get.id) + 1) { 4674 // We are looking for view.childTags() or tag.childTags() - so start after the open token of the parent view or tag. 4675 tokens = tokens.slice(index + get.id.length); 4676 } 4677 index = tokens.indexOf("/" + get.id); 4678 if (index + 1) { 4679 // We are looking for view.childTags() or tag.childTags() - so don't look beyond the close token of the parent view or tag. 4680 tokens = tokens.slice(0, index); 4681 } 4682 // Call getViewInfos to add the found childTags to the tags array 4683 getViewInfos(viewInfos(tokens, undefined, rOpenTagMarkers)); 4684 } 4685 4686 if (html === undefined && parentNode.getAttribute($viewsLinkAttr)) { 4687 bindEls.push([parentNode]);
4687 // Support data-linking top-level element directly (not within a data-linked container) 4688 } 4689 4690 // Remove temporary marker script nodes they were added by markPrevOrNextNode 4691 unmarkPrevOrNextNode(prevNode, elCnt); 4692 unmarkPrevOrNextNode(nextNode, elCnt); 4693 4694 if (get) { 4695 if (lazyLink) { 4696 lazyLink.resolve(); 4697 } 4698 return; // We have added childTags to the tags array, so we are done 4699 } 4700 4701 if (elCnt && defer + ids) { 4702 // There are some views with elCnt, for which the open or close did not precede any HTML tag - so they have not been processed yet 4703 elem = nextNode; 4704 if (defer) { 4705 if (nextNode) { 4706 processViewInfos(viewInfos(defer + "+", true), nextNode); 4707 } else { 4708 processViewInfos(viewInfos(defer, true), parentNode); 4709 } 4710 } 4711 processViewInfos(viewInfos(ids, true), parentNode); 4712 // If there were any tokens on nextNode which have now been associated with inserted HTML tags, remove them from nextNode 4713 if (nextNode) { 4714 tokens = nextNode.getAttribute(jsvAttrStr); 4715 if (l = tokens.indexOf(prevIds) + 1) { 4716 tokens = tokens.slice(l + prevIds.length - 1); 4717 } 4718 nextNode.setAttribute(jsvAttrStr, ids + tokens); 4719 } 4720 } 4721 4722 //================ Bind the data-linked elements and tags ================ 4723 l = bindEls.length; 4724 for (i = 0; i < l; i++) { 4725 elem = bindEls[i]; 4726 linkInfo = elem[1]; 4727 elem = elem[0]; 4728 if (linkInfo) { 4729 if (tag = bindingStore[linkInfo.id]) { 4730 if (linkCtx = tag.linkCtx) { 4731 // The tag may have been stored temporarily on the bindingStore - or may have already been replaced by the actual binding 4732 tag = linkCtx.tag; 4733 tag.linkCtx = linkCtx; 4734 } 4735 if (linkInfo.open) { 4736 // This is an 'open linked tag' binding annotation for a data-linked tag {^{...}} 4737 if (elem) { 4738 tag.parentElem = elem.parentNode; 4739 tag._prv = elem; 4740 } 4741 tag._elCnt = linkInfo.elCnt; 4742 // We data-link depth-first ("on the way in"), which is better for perf - and allows setting parent tags etc. 4743 view = tag.tagCtx.view; 4744 addDataBinding(undefined, tag._prv, view, linkInfo.id); 4745 } else { 4746 tag._nxt = elem; 4747 if (tag._.unlinked) { 4748 // This is a 'close linked tag' binding annotation 4749 // Add data binding 4750 tagCtx = tag.tagCtx; 4751 view = tagCtx.view; 4752 callAfterLink(tag); 4753 } 4754 } 4755 } 4756 } else { 4757 // Add data binding for a data-linked element (with data-link attribute) 4758 addDataBinding(elem.getAttribute($viewsLinkAttr), elem, $view(elem), undefined, isLink, outerData, context); 4759 } 4760 } 4761 if (lazyLink) { 4762 lazyLink.resolve(); 4763 } 4764 } 4765 //==== /end of nested functions ==== 4766 4767 var inTag, linkCtx, tag, i, l, j, len, elems, elem, view, vwInfo, linkInfo, prevNodes, token, prevView, nextView,
4768 node, tags, deep, tagName, tagCtx, validate, tagDepth, depth, fragment, copiedNode, firstTag, parentTag, 4769 isVoid, wrapper, div, tokens, elCnt, prevElCnt, htmlTag, ids, prevIds, found, skip, lazyLink, isLink, get, 4770 self = this, 4771 thisId = self._.id + "_", 4772 defer = "", 4773 // The marker ids for which no tag was encountered (empty views or final closing markers) which we carry over to container tag 4774 bindEls = [], 4775 tagStack = [], 4776 deferStack = [], 4777 onAfterCreate = self.hlp(onAfterCreateStr), 4778 processInfos = processViewInfos; 4779 4780 if (refresh) { 4781 lazyLink = refresh.lazyLink && $.Deferred(); 4782 if (refresh.tmpl) { 4783 // refresh is the prevView, passed in from addViews() 4784 prevView = "/" + refresh._.id + "_"; 4785 } else { 4786 isLink = refresh.lnk; // Top-level linking 4787 if (refresh.tag) { 4788 thisId = refresh.tag + "^"; 4789 refresh = true; 4790 } 4791 if (get = refresh.get) { 4792 processInfos = getViewInfos; 4793 tags = get.tags; 4794 deep = get.deep; 4795 tagName = get.name; 4796 } 4797 } 4798 refresh = refresh === true; 4799 } 4800 4801 parentNode = parentNode 4802 ? ("" + parentNode === parentNode 4803 ? $(parentNode)[0] // It is a string, so treat as selector 4804 : parentNode.jquery 4805 ? parentNode[0] // A jQuery object - take first element. 4806 : parentNode) 4807 : (self.parentElem // view.link() 4808 || document.body); // link(null, data) to link the whole document 4809 4810 validate = !$subSettingsAdvanced.noValidate && parentNode.contentEditable !== TRUE; 4811 parentTag = parentNode.tagName.toLowerCase(); 4812 elCnt = !!elContent[parentTag]; 4813 4814 prevNode = prevNode && markPrevOrNextNode(prevNode, elCnt); 4815 nextNode = nextNode && markPrevOrNextNode(nextNode, elCnt) || null; 4816 4817 if (html != undefined) { 4818 //================ Insert html into DOM using documentFragments (and wrapping HTML appropriately). ================ 4819 // Also convert markers to DOM annotations, based on content model. 4820 // Corresponds to nextNode ? $(nextNode).before(html) : $(parentNode).html(html); 4821 // but allows insertion to wrap correctly even with inserted script nodes. jQuery version will fail e.g. under tbody or select. 4822 // This version should also be slightly faster 4823 div = document.createElement("div"); 4824 wrapper = div; 4825 prevIds = ids = ""; 4826 htmlTag = parentNode.namespaceURI === "http://www.w3.org/2000/svg" ? "svg_ns" : (firstTag = rFirstElem.exec(html)) && firstTag[1] || ""; 4827 if (elCnt) { 4828 // Now look for following view, and find its tokens, or if not found, get the parentNode._df tokens 4829 node = nextNode; 4830 while (node && !(nextView = viewInfos(node))) { 4831 node = node.nextSibling; 4832 } 4833 if (tokens = nextView ? nextView._tkns : parentNode._df) { 4834 token = prevView || ""; 4835 if (refresh || !prevView) { 4836 token += "#" + thisId; 4837 } 4838 j = tokens.indexOf(token); 4839 if (j + 1) { 4840 j += token.length; 4841 // Transfer the initial tokens to inserted nodes, by setting them as the ids variable, picked up in convertMarkers 4842 prevIds = ids = tokens.slice(0, j); 4843 tokens = tokens.slice(j); 4844 if (nextView) { 4845 node.setAttribute(jsvAttrStr, tokens); 4846 } else { 4847 setDefer(parentNode, tokens); 4848 } 4849 } 4850 } 4851 } 4852 4853 //================ Convert the markers to DOM annotations, based on content model. ================ 4854// oldElCnt = elCnt; 4855 isVoid = undefined; 4856 html = ("" + html).replace(rConvertMarkers, convertMarkers); 4857// if (!!oldElCnt !== !!elCnt) { 4858// error("Parse: " + html); // Parse error. Content not well-formed? 4859// } 4860 if (validate && tagStack.length) { 4861 syntaxError("Mismatched '<" + parentTag + "...>' in:\n" + html); // Unmatched tag 4862 } 4863 if (validateOnly) { 4864 return; 4865 } 4866 // Append wrapper element to doc fragment 4867 safeFragment.appendChild(div); 4868 4869 // Go to html and back, then peel off extra wrappers 4870 // Corresponds to jQuery $(nextNode).before(html) or $(parentNode).html(html); 4871 // but supports svg elements, and other features missing from jQuery version (and this version should also be slightly faster) 4872 htmlTag = wrapMap[htmlTag] || wrapMap.div; 4873 depth = htmlTag[0]; 4874 wrapper.innerHTML = htmlTag[1] + html + htmlTag[2]; 4875 while (depth--) { 4876 wrapper = wrapper.lastChild; 4877 } 4878 safeFragment.removeChild(div); 4879 fragment = document.createDocumentFragment(); 4880 while (copiedNode = wrapper.firstChild) { 4881 fragment.appendChild(copiedNode); 4882 } 4883 // Insert into the DOM 4884 parentNode.insertBefore(fragment, nextNode); 4885 } 4886 4887 if (lazyLink) { 4888 setTimeout(dataLink, 0); 4889 } else { 4890 dataLink(); 4891 } 4892 4893 return lazyLink && lazyLink.promise(); 4894} 4895 4896function addDataBinding(linkMarkup, node, currentView, boundTagId, isLink, data, context) { 4897 // Add data binding for data-linked elements or {^{...}} data-linked tags 4898 var tmpl, tokens, attr, convertBack, tagExpr, linkFn, linkCtx, tag, rTagIndex, hasElse, lastIndex, 4899 linkExpressions = []; 4900 4901 if (boundTagId) { 4902 // boundTagId is a string for {^{...}} data-linked tag. So only one linkTag in linkMarkup 4903 // data and context parameters are undefined 4904 tag = bindingStore[boundTagId];
4905 tag = tag.linkCtx ? tag.linkCtx.tag : tag; 4906 4907 linkCtx = tag.linkCtx || { 4908 type: "inline", 4909 data: currentView.data, // source 4910 elem: tag._elCnt ? tag.parentElem : node, // target 4911 view: currentView, 4912 ctx: currentView.ctx, 4913 attr: HTML, // Script marker nodes are associated with {^{ and always target HTML. 4914 fn: tag._.bnd, 4915 tag: tag, 4916 // Pass the boundTagId in the linkCtx, so that it can be picked up in observeAndBind 4917 _bndId: boundTagId 4918 }; 4919 bindDataLinkTarget(linkCtx, linkCtx.fn); 4920 } else if (linkMarkup && node) { 4921 // If isLink then this is a top-level linking: .link(expression, target, data, ....) or 4922 // .link(true, target, data, ....) scenario - and data and context are passed in separately from the view 4923 data = isLink ? data : currentView.data; 4924 4925 // Compiled linkFn expressions could be stored in the tmpl.links array of the template 4926 // TODO - consider also caching globally so that if {{:foo}} or data-link="foo" occurs in different places, 4927 // the compiled template for this is cached and only compiled once... 4928 //links = currentView.links || currentView.tmpl.links; 4929 4930 tmpl = currentView.tmpl; 4931 4932// if (!(linkTags = links[linkMarkup])) { 4933 // This is the first time this view template has been linked, so we compile the data-link expressions, and store them on the template. 4934 4935 linkMarkup = normalizeLinkTag(linkMarkup, defaultAttr(node)); 4936 lastIndex = rTagDatalink.lastIndex = 0; 4937 while (tokens = rTagDatalink.exec(linkMarkup)) { // TODO require } to be followed by whitespace or $, and remove the \}(!\}) option. 4938 linkExpressions.push(tokens); 4939 lastIndex = rTagDatalink.lastIndex; 4940 } 4941 if (lastIndex < linkMarkup.length) { 4942 syntaxError(linkMarkup); 4943 } 4944 while (tokens = linkExpressions.shift()) { 4945 // Iterate over the data-link expressions, for different target attrs, 4946 // e.g. <input data-link="{:firstName:} title{>~description(firstName, lastName)}" 4947 // tokens: [all, attr, bindOnly, tagExpr, tagName, converter, colon, html, comment, code, params] 4948 rTagIndex = rTagDatalink.lastIndex; 4949 attr = tokens[1]; 4950 tagExpr = tokens[3]; 4951 while (linkExpressions[0] && linkExpressions[0][4] === "else") { // If this is {someTag...} and is followed by an {else...} add to tagExpr 4952 tagExpr += delimCloseChar1 + delimOpenChar0 + linkExpressions.shift()[3]; 4953 hasElse = true; 4954 } 4955 if (hasElse) { // If an {else} has been added, need also to add closing {{/someTag}} 4956 tagExpr += delimCloseChar1 + delimOpenChar0 + delimOpenChar1 + "/" + tokens[4] + delimCloseChar0; 4957 } 4958 linkCtx = { 4959 type: isLink ? "top" : "link", 4960 data: data, // source 4961 elem: node, // target 4962 view: currentView, 4963 ctx: context, 4964 attr: attr, 4965 isLk: isLink, // top-level linking? 4966 _toLk : 1, // Flag to data-link on initial data-link call rendering call 4967 _noUpd : tokens[2] // Flag for data-link="^{...}" so on initial data-link call will bind, but not render) 4968 }; 4969 4970 convertBack = undefined; 4971 if (tokens[6]) { 4972 convertBack = tokens[10] || undefined; 4973 linkCtx.convert = tokens[5] || ""; 4974 if (!attr && convertBack !== undefined && defaultAttr(node)) { 4975 // Default target, so allow 2 way binding 4976 linkCtx.convertBack = convertBack = convertBack.slice(1); 4977 } 4978 } 4979 // Compile the linkFn expression which evaluates and binds a data-link expression 4980 // TODO - optimize for the case of simple data path with no conversion, helpers, etc.: 4981 // i.e. data-link="a.b.c". Avoid creating new instances of Function every time. Can use a default function for all of these... 4982 4983 linkCtx.expr = attr + tagExpr; 4984 linkFn = tmpl.links[tagExpr]; 4985 if (!linkFn) { 4986 tmpl.links[tagExpr] = linkFn = $sub.tmplFn(tagExpr, tmpl, true, convertBack, hasElse); 4987 } 4988 linkCtx.fn = linkFn; 4989 bindDataLinkTarget(linkCtx, linkFn); 4990 // We store rTagIndex in local scope, since this addDataBinding method can sometimes be called recursively, 4991 // and each is using the same rTagDatalink instance. 4992 rTagDatalink.lastIndex = rTagIndex; 4993 } 4994// } 4995 } 4996} 4997 4998function bindDataLinkTarget(linkCtx, linkFn) { 4999 // Add data link bindings for a link expression in data-link attribute markup 5000 function handler(ev, eventArgs) { 5001 propertyChangeHandler.call(linkCtx, ev, eventArgs, linkFn); 5002 // If the link expression uses a custom tag, the propertyChangeHandler call will call renderTag, which will set tagCtx on linkCtx 5003 } 5004 handler.noArray = true; 5005 if (linkCtx.isLk) { 5006 // Top-level linking: .link(expressionOrTrue, data, context) - so we need to create a view for the linking, with the data and ctx 5007 // which may be different than the current context of the target. Note that this view is not a standard data-linked view, so it will 5008 // be disposed only when its parent view is disposed. 5009 addLinkMethods(linkCtx.view = new $sub.View( 5010 $sub.extendCtx(linkCtx.ctx, linkCtx.view.ctx), 5011 "link", linkCtx.view, linkCtx.data, linkCtx.expr, undefined, addBindingMarkers)); 5012 } 5013 linkCtx._ctxCb = getContextCb(linkCtx.view); // _ctxCb is for filtering/appending to dependency paths: function(path, object) { return [(object|path)*]} 5014 linkCtx._hdl = handler; 5015 // handler._ctx = linkCtx; Could pass linkCtx for use in a depends = function() {}
5015 call, so depends is different for different linkCtx's 5016 handler(true); 5017} 5018 5019//===================== 5020// Data-linking helpers 5021//===================== 5022 5023function removeSubStr(str, substr) { 5024 var k; 5025 return str 5026 ? (k = str.indexOf(substr), 5027 (k + 1 5028 ? str.slice(0, k) + str.slice(k + substr.length) 5029 : str)) 5030 : ""; 5031} 5032 5033function markerNodeInfo(node) { 5034 return node && 5035 ("" + node === node 5036 ? node 5037 : node.tagName === SCRIPT 5038 ? node.type.slice(3) 5039 : node.nodeType === 1 && node.getAttribute(jsvAttrStr) || ""); 5040} 5041 5042function viewInfos(node, isVal, rBinding) { 5043 // Test whether node is a script marker node, and if so, return metadata 5044 function getInfos(all, open, close, id, ch, elPath) { 5045 infos.push({ 5046 elCnt: elCnt, 5047 id: id, 5048 ch: ch, 5049 open: open, 5050 close: close, 5051 path: elPath, 5052 token: all 5053 }); 5054 } 5055 var elCnt, tokens, 5056 infos = []; 5057 if (tokens = isVal ? node : markerNodeInfo(node)) { 5058 elCnt = infos.elCnt = node.tagName !== SCRIPT; 5059 elCnt = tokens.charAt(0) === "@" || elCnt; 5060 infos._tkns = tokens; 5061 // rMarkerTokens = /(?:(#)|(\/))(\d+)([_^])([-+@\d]+)?/g; 5062 tokens.replace(rBinding || rMarkerTokens, getInfos); 5063 return infos; 5064 } 5065} 5066 5067function unmarkPrevOrNextNode(node, elCnt) { 5068 if (node) { 5069 if (node.type === "jsv") { 5070 node.parentNode.removeChild(node); 5071 } else if (elCnt && node.getAttribute($viewsLinkAttr) === "") { 5072 node.removeAttribute($viewsLinkAttr); 5073 } 5074 } 5075} 5076 5077function markPrevOrNextNode(node, elCnt) { 5078 var marker = node; 5079 while (elCnt && marker && marker.nodeType !== 1) { 5080 marker = marker.previousSibling; 5081 } 5082 if (marker) { 5083 if (marker.nodeType !== 1) { 5084 // For text nodes, we will add a script node before 5085 marker = document.createElement(SCRIPT); 5086 marker.type = "jsv"; 5087 node.parentNode.insertBefore(marker, node); 5088 } else if (!markerNodeInfo(marker) && !marker.getAttribute($viewsLinkAttr)) { 5089 // For element nodes, we will add a data-link attribute (unless there is already one) 5090 // so that this node gets included in the node linking process. 5091 marker.setAttribute($viewsLinkAttr, ""); 5092 } 5093 } 5094 return marker; 5095} 5096 5097function normalizeLinkTag(linkMarkup, twoway) { 5098 linkMarkup = $.trim(linkMarkup).replace(rEscapeQuotes, "\\$&"); 5099 return linkMarkup.slice(-1) !== delimCloseChar0 5100 // If simplified syntax is used: data-link="expression", convert to data-link="{:expression}", 5101 // or for inputs, data-link="{:expression:}" for (default) two-way binding 5102 ? linkMarkup = delimOpenChar1 + ":" + linkMarkup + (twoway ? ":" : "") + delimCloseChar0 5103 : linkMarkup; 5104} 5105 5106//=========================== 5107// Methods for views and tags 5108//=========================== 5109 5110function callAfterLink(tag, ev, eventArgs) { 5111 var $linkedElem, linkedElem, radioButtons, val, l, linkedTag, oldTrig, newTrig, tagProps, propsExpr, linkedElemView, prop, propDef, 5112 tagCtx = tag.tagCtx, 5113 view = tagCtx.view, 5114 props = tagCtx.props, 5115 linkCtx = tag.linkCtx; 5116 5117 if (tag._.unlinked) { // First call to onAfterLink, or first call after onUpdate: updateContent. Initialize and call onBind and set properties 5118 if (tag.linkedElement !== undefined) { 5119 // linkedElement: - selector for identifying linked element in template/rendered content 5120 tag.linkedElem = tag._.inline ? tag.contents(true, tag.linkedElement || "*").first() : $(linkCtx.elem); 5121 } 5122 if (tag.onBind) { 5123 tag.onBind(tagCtx, linkCtx, tag.ctx, ev, eventArgs); 5124 } 5125 } 5126 5127 if (tag.onAfterLink) { 5128 tag.onAfterLink(tagCtx, linkCtx, tag.ctx, ev, eventArgs); 5129 } 5130 5131 tag._.unlinked = undefined; 5132 $linkedElem = tag.targetTag ? tag.targetTag.linkedElem : tag.linkedElem; 5133 if (linkedElem = $linkedElem && $linkedElem[0]) { 5134 if (!tag.noVal) { 5135 if (!tag._.chging) { 5136 val = tag.cvtArgs()[0]; 5137 5138 if (linkedElem !== linkCtx.elem) { 5139 l = $linkedElem.length; 5140 while (l--) { 5141 linkedElem = $linkedElem[l]; 5142 linkedTag = linkedElem._jsvLkEl; 5143 if (tag._.inline && (!linkedTag || linkedTag !== tag && linkedTag.targetTag !== tag)) { 5144 // For data-linked tags, identify the linkedElem with the tag, for "to" binding 5145 // (For data-linked elements, if not yet bound, we identify later when the linkCtx.elem is bound) 5146 linkedElem._jsvLkEl = tag; 5147 bindTo(bindingStore[tag._tgId], tag, linkedElem); 5148 linkedElem._jsvBnd = "&" + tag._tgId + "+"; // Add a "+" for cloned binding - so removing 5149 // elems with cloned bindings will not remove the 'parent' binding from the bindingStore. 5150 } 5151 } 5152 linkCtx._val = val; 5153 } 5154 if (val !== undefined) { 5155 if (linkedElem.value !== undefined) { 5156 if (linkedElem.type === CHECKBOX) {
5157 linkedElem[CHECKED] = val && val !== "false"; 5158 } else if (linkedElem.type === "text") { 5159 linkedElem.value = val; 5160 } 5161 } else if (linkedElem.contentEditable === TRUE) { 5162 linkedElem.innerHTML = val; 5163 } 5164 } 5165 } 5166 } 5167 if (tag.setSize) { 5168 if (props.height) { 5169 $linkedElem.height(props.height); 5170 } 5171 if (props.width) { 5172 $linkedElem.width(props.width); 5173 } 5174 } 5175 if (props.title !== undefined) { 5176 $linkedElem.attr("title", props.title); 5177 } 5178 if (props["class"]) { 5179 // This code supports dynamic binding to class - where it adds the class if absent, and removes/adds if a previous value is present 5180 if (eventArgs && $linkedElem.hasClass(eventArgs.oldValue)) { 5181 $linkedElem.removeClass(eventArgs.oldValue); 5182 } 5183 $linkedElem.addClass(props["class"]); 5184 } 5185 if (props.id) { 5186 $linkedElem[0].id = props.id; 5187 } 5188 if (props.name) { 5189 $linkedElem.attr("name", props.name); 5190 } 5191 } 5192} 5193 5194function asyncElemChangeHandler(ev) { 5195 setTimeout(function() { 5196 elemChangeHandler(ev); 5197 }, 0); 5198} 5199 5200function bindElChange($elem, trig, onoff) { 5201 if (trig) { 5202 trig = "" + trig === trig ? trig : "keydown"; // Set trigger to (true || truey non-string (e.g. 1) || 'keydown'): Get 'keydown' with async 5203 $elem[onoff](trig, trig === "keydown" ? asyncElemChangeHandler : elemChangeHandler); 5204 } 5205} 5206 5207function bindTo(binding, tag, linkedElem, cvtBk) { 5208 // Two-way binding. 5209 // 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. 5210 // So for a computed path with an object call: a.b.getObject().d.e, then we set to[0] to be [exprOb, "d.e"], and we bind to the path on the returned object, exprOb.ob, as target 5211 // 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 5212 5213 var bindto, pathIndex, path, lastPath, bindtoOb, $linkedElem, newTrig, oldTrig, to, l, totry, 5214 linkCtx = binding.linkCtx, 5215 source = linkCtx.data, 5216 paths = linkCtx.fn.paths; 5217 5218 tag = tag || linkedElem._jsvLkEl; 5219 5220 if (binding && paths) { 5221 oldTrig = linkedElem._jsvTr || false; 5222 if (tag) { 5223 cvtBk = tag.convertBack || cvtBk; 5224 newTrig = tag.tagCtx.props.trigger; 5225 } 5226 newTrig = newTrig === undefined ? $subSettings.trigger : newTrig; 5227 if (oldTrig !== newTrig) { 5228 $linkedElem = $(linkedElem); 5229 bindElChange($linkedElem, oldTrig, "off"); 5230 bindElChange( 5231 $linkedElem, 5232 linkedElem._jsvTr 5233 // Trigger is noop except for text box, textarea, contenteditable... 5234 = (linkedElem.tagName === "INPUT" && linkedElem.type !== CHECKBOX && linkedElem.type !== RADIO || linkedElem.type === "textarea" || linkedElem.contentEditable === TRUE) && newTrig, 5235 "on" 5236 ); 5237 } 5238 5239 paths = (bindto = paths._jsvto) || paths[0]; 5240 pathIndex = paths && paths.length; 5241 if (pathIndex && (!tag || tag.tagCtx.args.length)) { 5242 lastPath = paths[pathIndex - 1]; 5243 if (lastPath._jsv) { 5244 bindtoOb = lastPath; 5245 while (lastPath.sb && lastPath.sb._jsv) { 5246 path = lastPath = lastPath.sb; 5247 } 5248 path = lastPath.sb || path && path.path; 5249 lastPath = path ? path.slice(1) : bindtoOb.path; 5250 } 5251 if (path) { 5252 to = [ 5253 bindtoOb, // 'exprOb' for this expression and view-binding. So bindtoOb.ob is current object returned by expression. 5254 lastPath 5255 ]; 5256 } else { 5257 while ((totry = linkCtx._ctxCb(path = lastPath.split("^").join("."), source)) && (l = totry.length)) { 5258 // Recursively dereference any ~foo or #bar tokens in the path. (Recursive because ~foo may be a contextual param which has 5259 // its own dependencies on other ~foo #bar components) 5260 to = totry; 5261 if (to._cp) { // Two-way binding to a contextual parameter reference, ~foo (declared as ~foo=expr on a parent tag) 5262 to = [to[l-3], to[l-2]]; 5263 lastPath = to[1]; 5264 if (lastPath._jsv) { 5265 bindtoOb = lastPath; 5266 while (lastPath.sb && lastPath.sb._jsv) { 5267 path = lastPath = lastPath.sb; 5268 } 5269 path = lastPath.sb || path && path.path;
5270 lastPath = path ? path.slice(1) : bindtoOb.path; 5271 to = [ 5272 bindtoOb, // 'exprOb' for this expression and view-binding. So bindtoOb.ob is current object returned by expression. 5273 lastPath 5274 ]; 5275 } 5276 } else { // Two-way binding to a helper - e.g. ~address.street, or computed, e.g. ~fullName(), or view property e.g. #data.foo 5277 to = l>2 5278 ? [to[l-3], to[l-2]] // With path: [object, path] 5279 : [to[l-2]]; // No path, (e.g. [function] for computed with setter) 5280 } 5281 source = to[0]; 5282 lastPath = to[1]; 5283 } 5284 to = to || [source, path]; // Two way binding to an object (neither ~foo nor #bar) 5285 } 5286 } else { 5287 to = []; 5288 } 5289 binding.to = [ 5290 to, 5291 cvtBk 5292 ]; 5293 } 5294} 5295 5296function mergeCtxs(tag, newCtxs, replace) { // Merge updated tagCtxs into tag.tagCtxs 5297 var tagCtx, newTagCtx, 5298 view = tag.tagCtx.view, 5299 tagCtxs = tag.tagCtxs || [tag.tagCtx], 5300 l = tagCtxs.length, 5301 refresh = !newCtxs; 5302 5303 newCtxs = newCtxs || tag._.bnd.call(view.tmpl, (tag.linkCtx || view).data, view, $sub); 5304 5305 if (replace) { 5306 // Replace previous tagCtxs by new ones, rather than merging 5307 tagCtxs = tag.tagCtxs = newCtxs; 5308 tag.tagCtx = tagCtxs[0]; 5309 } else { 5310 while (l--) { 5311 tagCtx = tagCtxs[l]; 5312 newTagCtx = newCtxs[l]; 5313 $observable(tagCtx.props).setProperty(newTagCtx.props); 5314 $extend(tagCtx.ctx, newTagCtx.ctx); // We don't support propagating ctx variables, ~foo, observably, to nested views. So extend, not setProperty... 5315 tagCtx.args = newTagCtx.args; 5316 if (refresh) { 5317 tagCtx.tmpl = newTagCtx.tmpl; 5318 } 5319 } 5320 } 5321 $sub._ths(tag, tagCtxs[0]); // tagHandlersFromProps 5322 return tagCtxs; 5323} 5324 5325//========= 5326// Disposal 5327//========= 5328 5329function clean(elems) { 5330 // Remove data-link bindings, or contained views 5331 var l, elem, bindings, 5332 elemArray = [], 5333 len = elems.length, 5334 i = len; 5335 while (i--) { 5336 // Copy into an array, so that deletion of nodes from DOM will not cause our 'i' counter to get shifted 5337 // (Note: This seems as fast or faster than elemArray = [].slice.call(elems); ...) 5338 elemArray.push(elems[i]); 5339 } 5340 i = len; 5341 while (i--) { 5342 elem = elemArray[i]; 5343 if (elem.parentNode) { 5344 // Has not already been removed from the DOM 5345 if (bindings = elem._jsvBnd) { 5346 // Get propertyChange bindings for this element 5347 // This may be an element with data-link, or the opening script marker node for a data-linked tag {^{...}} 5348 // bindings is a string with the syntax: "(&bindingId)*" 5349 bindings = bindings.slice(1).split("&"); 5350 elem._jsvBnd = ""; 5351 l = bindings.length; 5352 while (l--) { 5353 // Remove associated bindings 5354 removeViewBinding(bindings[l], elem._jsvLkEl, elem); // unbind bindings with this bindingId on this view 5355 } 5356 } 5357 disposeTokens(markerNodeInfo(elem) + (elem._df || "")); 5358 } 5359 } 5360} 5361 5362function removeViewBinding(bindId, linkedElemTag, elem) { 5363 // Unbind 5364 var objId, linkCtx, tag, object, obsId, tagCtxs, l, map, $linkedElem, linkedElem, trigger, view, 5365 binding = bindingStore[bindId]; 5366 5367 if (linkedElemTag) { 5368 if (elem === linkedElemTag.linkedElem[0]) { 5369 elem._jsvLkEl = undefined; 5370 linkedElemTag.linkedElem = undefined; 5371 } 5372 } else if (binding) { 5373 delete bindingStore[bindId]; // Delete already, so call to onDispose handler below cannot trigger recursive deletion (through recursive call to jQuery cleanData) 5374 for (objId in binding.bnd) { 5375 object = binding.bnd[objId]; 5376 obsId = binding.cbId; 5377 if ($isArray(object)) { 5378 $([object]).off(arrayChangeStr + obsId).off(propertyChangeStr + obsId); // There may be either or both of arrayChange and propertyChange 5379 } else { 5380 $(object).off(propertyChangeStr + obsId); 5381 } 5382 delete binding.bnd[objId]; 5383 } 5384 5385 if (linkCtx = binding.linkCtx) { 5386 if (tag = linkCtx.tag) { 5387 if (tagCtxs = tag.tagCtxs) { 5388 l = tagCtxs.length; 5389 while (l--) { 5390 if (map = tagCtxs[l].map) { 5391 map.unmap(); //unobserve 5392 } 5393 } 5394 } 5395 $linkedElem = tag.linkedElem; 5396 5397 if (tag.onUnbind) { 5398 tag.onUnbind(tag.tagCtx, linkCtx, tag.ctx, true); 5399 } 5400 if (tag.onDispose) { 5401 tag.onDispose(); 5402 } 5403 5404 if (!tag._elCnt) { 5405 if (tag._prv) { 5406 tag._prv.parentNode.removeChild(tag._prv); 5407 } 5408 if (tag._nxt) { 5409 tag._nxt.parentNode.removeChild(tag._nxt); 5410 } 5411 } 5412 } 5413 linkedElem = $linkedElem && $linkedElem[0] || linkCtx.elem; 5414 5415 if (trigger = linkedElem && linkedElem._jsvTr) { 5416 bindElChange($linkedElem || $(linkedElem), trigger, "off"); 5417 linkedElem._jsvTr = undefined; 5418 } 5419 view = linkCtx.view; 5420 if (view.type === "link") { 5421 view.parent.removeViews(view._.key, undefined, true); // A "link" view is associated with the binding, so should be disposed with binding. 5422 } else { 5423 delete view._.bnds[bindId]; 5424 } 5425 } 5426 delete cbBindingsStore[binding.cbId]; 5427 } 5428} 5429 5430function $unlink(to) { 5431 if (to) { 5432 to = to.jquery ? to : $(to); 5433 to.each(function() { 5434 var innerView; 5435 //TODO fix this for better perf. Rather that calling inner view multiple times which does querySelectorAll each time, consider a single querySelectorAll 5436 // or simply call view.removeViews() on the top-level views under the target 'to' node, then clean(...) 5437 while ((innerView = $view(this, true)) && innerView.parent) { 5438 innerView.parent.removeViews(innerView._.key, undefined, true); 5439 } 5440 clean(this.getElementsByTagName("*")); 5441 }); 5442 clean(to); 5443 } else { 5444 // Call to $.unlink() is equivalent to $.unlink(true, "body") 5445 if (activeBody) { 5446 $(activeBody) 5447 .off(elementChangeStr, elemChangeHandler) 5448 .off('blur', '[contenteditable]', elemChangeHandler); 5449 activeBody = undefined; 5450 } 5451 topView.removeViews(); 5452 clean(document.body.getElementsByTagName("*")); 5453 } 5454} 5455 5456//======== 5457// Helpers 5458//======== 5459 5460function getContextCb(view) { // Return a callback for accessing the context of a template/data-link expression - and convert
5460ing ~foo, #foo etc. 5461 // TODO Consider exposing or allowing override, as public API 5462 return function(path, object, depth) { 5463 // TODO consider only calling the contextCb on the initial token in path '~a.b.c' and not calling again on 5464 // the individual tokens, 'a', 'b', 'c'... Currently it is called multiple times 5465 var tokens, tag, items, helper, last, nextPath, l; 5466 if (view && path) { 5467 if (path._jsv) { 5468 return path._jsv.call(view.tmpl, object, view, $sub); 5469 } 5470 if (path.charAt(0) === "~") { 5471 // We return new items to insert into the sequence, replacing the "~a.b.c" string: 5472 // [helperObject 'a', "a.b.c" currentDataItem] so currentDataItem becomes the object for subsequent paths. 5473 if (path.slice(0, 4) === "~tag") { 5474 tag = view.ctx; 5475 if (path.charAt(4) === ".") { 5476 // "~tag.xxx" 5477 tokens = path.slice(5).split("."); 5478 tag = tag.tag; 5479 } 5480 if (tokens) { 5481 return tag ? [tag, tokens.join("."), object] : []; 5482 } 5483 } 5484 path = path.slice(1).split("."); 5485 if (helper = view.hlp(last = path.shift(), true)) { 5486 if (helper._cp) { // helper for (contextual parameter ~foo=...) is an array - [data, ctxPrmDependencies ...] 5487 if (path.length) { 5488 nextPath = "." + path.join("."); 5489 last = helper[l = helper.length-1]; 5490 if (last._jsv) { 5491 last.sb = nextPath; 5492 last.bnd = !!depth; 5493 } else { 5494 helper[l] = (last + nextPath).replace("#data.", ""); 5495 if (last.slice(0, 5) === "#view") { 5496 helper[l] = helper[l].slice(6); 5497 helper.splice(l, 0, view); 5498 } 5499 } 5500 } 5501 helper.push(object); 5502 items = helper; 5503 } else if (path.length || $isFunction(helper)) { 5504 items = [helper, path.join("."), object]; // 2way bindng on ~foo.helperLeafProperty or ~computed() or ~contextualParam 5505 } 5506 } 5507 return items || []; 5508 } 5509 if (path.charAt(0) === "#") { 5510 // We return new items to insert into the sequence, replacing the "#a.b.c" string: [view, "a.b.c" currentDataItem] 5511 // so currentDataItem becomes the object for subsequent paths. The 'true' flag makes the paths bind only to leaf changes. 5512 return path === "#data" ? [] : [view, path.replace(rViewPath, ""), object]; 5513 } 5514 } 5515 }; 5516} 5517 5518function inputAttrib(elem) { 5519 return elem.type === CHECKBOX ? elem[CHECKED] : elem.value; 5520} 5521 5522//========================== Initialize ========================== 5523 5524//===================== 5525// JsRender integration 5526//===================== 5527 5528$sub.onStore.template = function(name, item) { 5529 item.link = tmplLink; 5530 if (name) { 5531 $.link[name] = function() { 5532 return tmplLink.apply(item, arguments); 5533 }; 5534 } 5535}; 5536 5537$sub.viewInfos = viewInfos; // Expose viewInfos() as public helper method 5538 5539// Define JsViews version of delimiters(), and initialize 5540($viewsSettings.delimiters = function() { 5541 // Run delimiters initialization in context of jsrender.js 5542 var ret = oldJsvDelimiters.apply(0, arguments); 5543 5544 if (oldJsvDelimiters !== $viewsDelimiters) { 5545 // If JsRender was loaded before JsViews, then need also to initialize and set globals in that JsRender instance 5546 ret = $viewsDelimiters.apply(0, arguments); 5547 } 5548 5549 // Data-linking must use new delimiters 5550 rTagDatalink = new RegExp("(?:^|\\s*)([\\w-]*)(\\" + linkChar + ")?(\\" + delimOpenChar1 + $sub.rTag + "(:\\w*)?\\" + delimCloseChar0 + ")", "g"); 5551 return ret; 5552})(); // jshint ignore:line 5553 5554$sub.addSetting("trigger"); 5555 5556//==================================== 5557// Additional members for linked views 5558//==================================== 5559 5560function transferViewTokens(prevNode, nextNode, parentElem, id, viewOrTagChar, refresh) { 5561 // Transfer tokens on prevNode of viewToRemove/viewToRefresh to nextNode or parentElem._df 5562 var i, l, vwInfos, vwInfo, viewOrTag, viewId, tokens, 5563 precedingLength = 0, 5564 emptyView = prevNode === nextNode; 5565 5566 if (prevNode) { 5567 // prevNode is either the first node in the viewOrTag, or has been replaced by the vwInfos tokens string 5568 vwInfos = viewInfos(prevNode) || []; 5569 for (i = 0, l = vwInfos.length; i < l; i++) { 5570 // Step through views or tags on the prevNode 5571 vwInfo = vwInfos[i]; 5572 viewId = vwInfo.id; 5573 if (viewId === id && vwInfo.ch === viewOrTagChar) { 5574 if (refresh) { 5575 // This is viewOrTagToRefresh, this is the last viewOrTag to process... 5576 l = 0; 5577 } else { 5578 // This is viewOrTagToRemove, so we are done... 5579 break; 5580 } 5581 } 5582 if (!emptyView) { 5583 viewOrTag = vwInfo.ch === "_" 5584 ? viewStore[viewId] 5585 : bindingStore[viewId].linkCtx.tag;
5586 if (vwInfo.open) { 5587 // A "#m" token 5588 viewOrTag._prv = nextNode; 5589 } else if (vwInfo.close) { 5590 // A "/m" token 5591 viewOrTag._nxt = nextNode; 5592 } 5593 } 5594 precedingLength += viewId.length + 2; 5595 } 5596 5597 if (precedingLength) { 5598 prevNode.setAttribute(jsvAttrStr, prevNode.getAttribute(jsvAttrStr).slice(precedingLength)); 5599 } 5600 tokens = nextNode ? nextNode.getAttribute(jsvAttrStr) : parentElem._df; 5601 if (l = tokens.indexOf("/" + id + viewOrTagChar) + 1) { 5602 tokens = vwInfos._tkns.slice(0, precedingLength) + tokens.slice(l + (refresh ? -1 : id.length + 1)); 5603 } 5604 if (tokens) { 5605 if (nextNode) { 5606 // If viewOrTagToRemove was an empty viewOrTag, we will remove both #n and /n 5607 // (and any intervening tokens) from the nextNode (=== prevNode) 5608 // If viewOrTagToRemove was not empty, we will take tokens preceding #n from prevNode, 5609 // and concatenate with tokens following /n on nextNode 5610 nextNode.setAttribute(jsvAttrStr, tokens); 5611 } else { 5612 setDefer(parentElem, tokens); 5613 } 5614 } 5615 } else { 5616 // !prevNode, so there may be a deferred nodes token on the parentElem. Remove it. 5617 setDefer(parentElem, removeSubStr(parentElem._df, "#" + id + viewOrTagChar)); 5618 if (!refresh && !nextNode) { 5619 // If this viewOrTag is being removed, and there was no .nxt, remove closing token from deferred tokens 5620 setDefer(parentElem, removeSubStr(parentElem._df, "/" + id + viewOrTagChar)); 5621 } 5622 } 5623} 5624 5625function disposeTokens(tokens) { 5626 var i, l, vwItem, vwInfos; 5627 if (vwInfos = viewInfos(tokens, true, rOpenMarkers)) { 5628 for (i = 0, l = vwInfos.length; i < l; i++) { 5629 vwItem = vwInfos[i]; 5630 if (vwItem.ch === "_") { 5631 if ((vwItem = viewStore[vwItem.id]) && vwItem.type) { 5632 // If this is the _prv (prevNode) for a view, remove the view 5633 // - unless view.type is undefined, in which case it is already being removed 5634 vwItem.parent.removeViews(vwItem._.key, undefined, true); 5635 } 5636 } else { 5637 removeViewBinding(vwItem.id); // unbind bindings with this bindingId on this view 5638 } 5639 } 5640 } 5641} 5642 5643//==================================== 5644// Add link methods to data-linked view or tag 5645//==================================== 5646function addLinkMethods(tagOrView, isTag) { 5647 5648 tagOrView.contents = function(deep, select) { 5649 // For a view or a tag, return jQuery object with the content nodes, 5650 if (deep !== !!deep) { 5651 // deep not boolean, so this is contents(selector) 5652 select = deep; 5653 deep = undefined; 5654 } 5655 var filtered, 5656 nodes = $(this.nodes()); 5657 if (nodes[0]) { 5658 filtered = select ? nodes.filter(select) : nodes; 5659 nodes = deep && select ? filtered.add(nodes.find(select)) : filtered; 5660 } 5661 return nodes; 5662 }; 5663 5664 tagOrView.nodes = function(withMarkers, prevNode, nextNode) { 5665 // For a view or a tag, return top-level nodes 5666 // Do not return any script marker nodes, unless withMarkers is true 5667 // Optionally limit range, by passing in prevNode or nextNode parameters 5668 5669 var node, 5670 self = this, 5671 elCnt = self._elCnt, 5672 prevIsFirstNode = !prevNode && elCnt, 5673 nodes = []; 5674 5675 prevNode = prevNode || self._prv; 5676 nextNode = nextNode || self._nxt; 5677 5678 node = prevIsFirstNode 5679 ? (prevNode === self._nxt 5680 ? self.parentElem.lastSibling 5681 : prevNode) 5682 : (self._.inline === false 5683 ? prevNode || self.linkCtx.elem.firstChild 5684 : prevNode && prevNode.nextSibling); 5685 5686 while (node && (!nextNode || node !== nextNode)) { 5687 if (withMarkers || elCnt || node.tagName !== SCRIPT) { 5688 // All the top-level nodes in the view 5689 // (except script marker nodes, unless withMarkers = true) 5690 // (Note: If a script marker node, viewInfo.elCnt undefined) 5691 nodes.push(node); 5692 } 5693 node = node.nextSibling; 5694 } 5695 return nodes; 5696 }; 5697 5698 tagOrView.childTags = function(deep, tagName) { 5699 // For a view or a tag, return child tags - at any depth, or as immediate children only. 5700 if (deep !== !!deep) { 5701 // deep not boolean, so this is childTags(tagName) - which looks for top-level tags of given tagName 5702 tagName = deep; 5703 deep = undefined; 5704 } 5705 5706 var self = this, 5707 view = self.link ? self : self.tagCtx.view, // this may be a view or a tag. If a tag, get the view from tag.view.tagCtx 5708 prevNode = self._prv, 5709 elCnt = self._elCnt, 5710 tags = []; 5711 5712 view.link( 5713 undefined, 5714 self.parentElem, 5715 elCnt ? prevNode && prevNode.previousSibling : prevNode, 5716 self._nxt, 5717 undefined, 5718 {get:{ 5719 tags: tags, 5720 deep: deep, 5721 name: tagName, 5722 id: self.link ? self._.id + "_" : self._tgId + "^" 5723 }} 5724 ); 5725 return tags; 5726 }; 5727 5728 tagOrView.refresh = function(sourceValue) { 5729 var promise, attr, 5730 tag = this, 5731 linkCtx = tag.linkCtx, 5732 view = tag.tagCtx.view; 5733 5734 if (tag.disposed) { error("Removed tag"); } 5735 if (sourceValue === undefined) { 5736 sourceValue = $sub._tag(tag, view, view.tmpl, mergeCtxs(tag), true); // Get rendered HTML for tag, based on refreshed tagCtxs 5737 } 5738 if (sourceValue + "" === sourceValue) { 5739 // If no rendered content, sourceValue will not be a string (can be 0 or undefined) 5740 attr = tag._.inline ? HTML : (linkCtx.attr || defaultAttr(tag.parentElem, true)); 5741 promise = updateContent(sourceValue, linkCtx, attr, tag); 5742 } 5743 5744 callAfterLink(tag); 5745 return promise || tag; 5746 }; 5747 5748 tagOrView.update = function(value) { 5749 var linkedElem = this.linkedElem; 5750 if (linkedElem) {
5751 elemChangeHandler({ 5752 target: linkedElem[0] 5753 }, undefined, value); 5754 } 5755 }; 5756 5757 if (isTag) { 5758 // This is a TAG 5759 tagOrView.domChange = function() { // domChange notification support 5760 var elem = this.parentElem, 5761 hasListener = $.hasData(elem) && $._data(elem).events, 5762 domChangeNotification = "jsv-domchange"; 5763 5764 if (hasListener && hasListener[domChangeNotification]) { 5765 // Only trigger handler if there is a handler listening for this event. (Note using triggerHandler - so no event bubbling.) 5766 $(elem).triggerHandler(domChangeNotification, arguments); 5767 } 5768 }; 5769 } else { 5770 // This is a VIEW 5771 // Note: a linked view will also, after linking have nodes[], _prv (prevNode), _nxt (nextNode) ... 5772 tagOrView.addViews = function(index, dataItems) { 5773 // if view is not an array view, do nothing 5774 var i, viewsCount, 5775 self = this, 5776 itemsCount = dataItems.length, 5777 views = self.views; 5778 5779 if (!self._.useKey && itemsCount) { 5780 // view is of type "array" 5781 viewsCount = views.length + itemsCount; 5782 5783 if (viewsCount === self.data.length // If views not already synced to array (e.g. triggered by array.length propertyChange - jsviews/issues/301) 5784 && renderAndLink(self, index, self.tmpl, views, dataItems, self.ctx) !== false) { 5785 if (!self._.srt) { // Not part of a 'sort' on refresh 5786 self.fixIndex(index + itemsCount); 5787 } 5788 } 5789 } 5790 }; 5791 5792 tagOrView.removeViews = function(index, itemsCount, keepNodes, isMove) { 5793 // view.removeViews() removes all the child views 5794 // view.removeViews(index) removes the child view with specified index or key 5795 // view.removeViews(index, count) removes the specified nummber of child views, starting with the specified index 5796 function removeView(index) { 5797 var id, bindId, parentElem, prevNode, nextNode, nodesToRemove, 5798 viewToRemove = views[index]; 5799 5800 if (viewToRemove && viewToRemove.link) { 5801 id = viewToRemove._.id; 5802 if (!keepNodes) { 5803 // Remove the HTML nodes from the DOM, unless they have already been removed, including nodes of child views 5804 nodesToRemove = viewToRemove.nodes(); 5805 } 5806 5807 // Remove child views, without removing nodes 5808 viewToRemove.removeViews(undefined, undefined, true); 5809 5810 viewToRemove.type = undefined; // Set type to undefined: used as a flag that this view is being removed 5811 prevNode = viewToRemove._prv; 5812 nextNode = viewToRemove._nxt; 5813 parentElem = viewToRemove.parentElem; 5814 // If prevNode and nextNode are the same, the view is empty 5815 if (!keepNodes) { 5816 // Remove the HTML nodes from the DOM, unless they have already been removed, including nodes of child views 5817 if (viewToRemove._elCnt) { 5818 // if keepNodes is false (and transferring of tokens has not already been done at a higher level) 5819 // then transfer tokens from prevNode which is being removed, to nextNode. 5820 transferViewTokens(prevNode, nextNode, parentElem, id, "_"); 5821 } 5822 $(nodesToRemove).remove(); 5823 } 5824 if (!viewToRemove._elCnt) { 5825 try { 5826 prevNode.parentNode.removeChild(prevNode); // (prevNode.parentNode is parentElem, except if jQuery Mobile or similar has inserted an intermediate wrapper 5827 nextNode.parentNode.removeChild(nextNode); 5828 } catch (e) {} 5829 } 5830 setArrayChangeLink(viewToRemove); 5831 for (bindId in viewToRemove._.bnds) { 5832 removeViewBinding(bindId); 5833 } 5834 delete viewStore[id]; 5835 } 5836 } 5837 5838 var current, view, viewsCount, 5839 self = this, 5840 isArray = !self._.useKey, 5841 views = self.views; 5842 5843 if (isArray) { 5844 viewsCount = views.length; 5845 } 5846 if (index === undefined) { 5847 // Remove all child views 5848 if (isArray) { 5849 // views and data are arrays 5850 current = viewsCount; 5851 while (current--) { 5852 removeView(current); 5853 } 5854 self.views = []; 5855 } else { 5856 // views and data are objects 5857 for (view in views) { 5858 // Remove by key 5859 removeView(view); 5860 } 5861 self.views = {}; 5862 } 5863 } else { 5864 if (itemsCount === undefined) { 5865 if (isArray) { 5866 // The parentView is data array view. 5867 // Set itemsCount to 1, to remove this item 5868 itemsCount = 1; 5869 } else { 5870 // Remove child view with key 'index' 5871 removeView(index); 5872 delete views[index]; 5873 } 5874 } 5875 if (isArray && itemsCount 5876 && (isMove || viewsCount - itemsCount === self.data.length)) { // If views not already synced to array (e.g. triggered by array.length propertyChange - jsviews/issues/301) 5877 current = index + itemsCount; 5878 // Remove indexed items (parentView is data array view); 5879 while (current-- > index) { 5880 removeView(current); 5881 } 5882 views.splice(index, itemsCount); 5883 if (!self._.sort) { 5884 self.fixIndex(index); 5885 } 5886 } 5887 } 5888 }; 5889 5890 tagOrView.refresh = function() { 5891 var self = this, 5892 parent = self.parent; 5893 5894 if (parent) { 5895 renderAndLink(self, self.index, self.tmpl, parent.views, self.data, undefined, true); 5896 setArrayChangeLink(self); 5897 } 5898 }; 5899 5900 tagOrView.fixIndex = function(fromIndex) { 5901 // Fixup index on following view items... 5902 var views = this.views, 5903 index = views.length; 5904 while (fromIndex < index--) { 5905 if (views[index].index !== index) { 5906 $observable(views[index]).setProperty("index", index); 5907 // This is fixing up index, but not key, and not index on child views. From child views, use view.getIndex() 5908 } 5909 } 5910 }; 5911 5912 tagOrView.link = viewLink; 5913 } 5914} 5915 5916//======================== 5917// JsViews-specific converters 5918//======================== 5919 5920$converters.merge = function(val) { 5921 // Special converter used in data-linking to space-separated lists, such as className: 5922 // Currently only supports toggle semantics - and has no effect if toggle string is not specified 5923 // data-link="class{merge:boolExpr toggle=className}" 5924 var regularExpression, 5925 currentValue = this.linkCtx._val || "", 5926 toggle = this.tagCtx.props.toggle; 5927 5928 if (toggle) { 5929 // We are toggling the class specified by the toggle property, 5930 // and the boolean val binding is driving the insert/remove toggle 5931 5932 regularExpression = toggle.replace(/[\\^$.|?*+()[{]/g, "\\$&"); 5933 // Escape any regular expression special characters (metacharacters) within the toggle string 5934 regularExpression = "(\\s(?=" + regularExpression + "$)|(\\s)|^)(" + regularExpression + "(\\s|$))"; 5935 // Example: /(\s(?=myclass$)|(\s)|^)?(myclass(\s|$))/ - so matches (" myclass" or " " or ^ ) followed by ("myclass " or "myclass$") where ^/$ are beginning/end of string 5936 currentValue = currentValue.replace(new RegExp(regularExpression), "$2"); 5937 val = currentValue + (val ? (currentValue && " ") + toggle : ""); 5938 } 5939 return val; 5940}; 5941 5942//======================== 5943// JsViews-specific tags 5944//======================== 5945 5946$tags("on", {
5947 attr: NONE, 5948 noVal: true, // This tag control does not bind to arg[0] - no default binding to current #data context 5949 init: function(tagCtx) { 5950 var content, 5951 tag = this, 5952 i = 0, 5953 args = tagCtx.args, // [events,] [selector,] handler 5954 l = args.length; 5955 5956 for (; i<l && !$isFunction(args[i]); i++); // Handler is first arg of type function 5957 tag._hi = l>i && i+1; // handler index 5958 if (tag._.inline) { 5959 if (!$sub.rTmpl.exec(content = tagCtx.tmpl.markup)) { 5960 // Inline {^{on}} tag with no content (or external template content) or with content containing 5961 // no HTML or JsRender tags: We will wrap the (text) content, or the operation name in a <button> element 5962 // (Otherwise we will attach the handler to the element content after data-linking) 5963 tag.template = "<button>" + ($.trim(content) || tagCtx.params.args[i] || "noop") + "</button>"; 5964 } 5965 tag.attr = HTML; 5966 } 5967 }, 5968 render: function() { 5969 var tagCtx = this.tagCtx; 5970 return this._.inline && tagCtx.render(tagCtx.view, true); // no arg, so renders against parentView.data 5971 }, 5972 onAfterLink: function(tagCtx, linkCtx) { 5973 var handler, params, find, activeElem, 5974 tag = this, 5975 i = tag._hi, 5976 args = tagCtx.args, // [events,] [selector,] handler 5977 l = args.length, 5978 props = tagCtx.props, 5979 data = props.data, 5980 view = tagCtx.view, 5981 contextOb = props.context; // Context ('this' pointer) for attached handler 5982 5983 if (i) { // There is a handler 5984 handler = args[i-1]; 5985 params = args.slice(i); // Subsequent args are params 5986 args = args.slice(0, i-1); // Preceding args (if any) are events and selector 5987 tag._sel = args[1]; // Selector for descendant elements - for delegated events on those elements, delegating to the activeElem 5988 5989 activeElem = tag.activeElem = tag.activeElem || $(tag._.inline 5990 ? (tag._sel = args[1] || "*", tag.parentElem) 5991 // If inline, attach to child elements of tag parent element (filtered by selector argument if provided. 5992 // (In handler we'll filter out events from sibling elements preceding or following tag.) 5993 // This allows us to use the delegated pattern where the attached event works even for added elements satisfying the selector 5994 : linkCtx.elem); 5995 5996 if (!contextOb) { 5997 // Get the path for the preceding object (context object) of handler (which is the last arg), compile function 5998 // to return that context object, and run compiled function against data 5999 contextOb = /^(.*)[\.^][\w$]+$/.exec(tagCtx.params.args.slice(-params.length - 1)[0]); 6000 contextOb = contextOb && $sub.tmplFn(delimOpenChar1 + ":" + contextOb[1] + delimCloseChar0, view.tmpl, true)(linkCtx.data, view); 6001 } 6002 6003 if (tag._evs) { 6004 tag.onUnbind(); 6005 } 6006 6007 activeElem.on( 6008 tag._evs = args[0] || "click", // events defaults to "click" 6009 tag._sel, 6010 data == undefined ? null : data, 6011 tag._hlr = function hndlr(ev) { 6012 var nodes, length, 6013 found = !tag._.inline; 6014 6015 if (!found) { // If inline, filter out events from sibling elements preceding or following tag. 6016 nodes = tag.contents("*"); 6017 l = nodes.length; 6018 while (!found && l--) { 6019 if (nodes[l].contains(ev.target)) { 6020 found = true; 6021 } 6022 } 6023 } 6024 if (found) { // target elem is indeed within the tag, so call the {on} handler 6025 return handler.apply(contextOb || linkCtx.data, [].concat( 6026 params, // e.g. par1, par2 6027 ev, 6028 {change: ev.type, view: view, linkCtx: linkCtx}, 6029 params.slice.call(arguments, 1) // If triggering event (e.g. jsv-domchange) has additional arguments after ev, pass them too 6030 )); 6031 // for {on 'click' handler par1 par2} use handler(par1, par2, ev, domchangeEventArgs) 6032 // for {on 'jsv-domchange' handler par1 par2} use handler(par1, par2, ev, domchangeEventArgs, tagCtx, linkCtx, observableEventArgs) 6033 } 6034 } 6035 ); 6036 } 6037 }, 6038 onUpdate: function() { 6039 return false; 6040 }, 6041 onUnbind: function() { 6042 var self = this; 6043 if (self.activeElem) { 6044 self.activeElem.off(self._evs, self._sel, self._hlr); 6045 } 6046 }, 6047 flow: true, 6048 dataBoundOnly: true 6049}); 6050 6051$extend($tags["for"], { 6052 //onUpdate: function(ev, eventArgs, newTagCtxs) { 6053 //Consider adding filtering for perf optimization. However the below prevents update on some scenarios which _should_ update - namely when there is another array on which for also depen
6053ds. 6054 //var i, l, tci, prevArg; 6055 //for (tci = 0; (prevArg = this.tagCtxs[tci]) && prevArg.args.length; tci++) { 6056 // if (prevArg.args[0] !== newTagCtxs[tci].args[0]) { 6057 // return true; 6058 // } 6059 //} 6060 //return false; 6061 //}, 6062 onArrayChange: function(ev, eventArgs, tagCtx, linkCtx) { 6063 var arrayView, 6064 target = ev.target, 6065 targetLength = target.length, 6066 tag = this, 6067 change = eventArgs.change; 6068 if (tag._.noVws // Child views not supported because target is not html - e.g. data-link="title{for ...}" 6069 || tag.tagCtxs[1] && ( // There is an {{else}} 6070 change === "insert" && targetLength === eventArgs.items.length // inserting, and new length is same as inserted length, so going from 0 to n 6071 || change === "remove" && !targetLength // removing , and new length 0, so going from n to 0 6072 )) { 6073 tag.refresh(); 6074 } else { 6075 for (arrayView in tag._.arrVws) { 6076 arrayView = tag._.arrVws[arrayView]; 6077 if (arrayView.data === target) { 6078 arrayView._.onArrayChange.apply(arrayView, arguments); 6079 } 6080 } 6081 } 6082 tag.domChange(tagCtx, linkCtx, eventArgs); 6083 ev.done = true; 6084 }, 6085 onAfterLink: function(tagCtx, linkCtx) { 6086 var i, arrHandler, arrBinding, data, 6087 tag = this, 6088 arrayBindings = tag._ars || {}, 6089 tagCtxs = tag.tagCtxs, 6090 l = tagCtxs.length, 6091 selected = tag.selected || 0; 6092 6093 for (i = 0; i <= selected; i++) { 6094 tagCtx = tagCtxs[i]; // loop through tagCtxs up to selected 6095 data = tagCtx.map 6096 ? tagCtx.map.tgt // 'data' is mapped data 6097 : tagCtx.args.length 6098 ? tagCtx.args[0] // or args[0] 6099 : tagCtx.view.data; // or defaults to current data. 6100 6101 if ((arrBinding = arrayBindings[i]) && data !== arrBinding[0]) { // Is there previous array data on this tagCtx, different from new data 6102 $observe(arrBinding[0], arrBinding[1], true); //unobserve previous array 6103 delete arrayBindings[i]; 6104 } 6105 if (!arrayBindings[i] && $isArray(data)) { 6106 $observe(data, arrHandler = function(ev, eventArgs) { 6107 var tagCt = tagCtx; 6108 tag.onArrayChange(ev, eventArgs, tagCt, linkCtx); 6109 }); 6110 arrayBindings[i] = [data, arrHandler]; // Store array data and arrayChangeHandler on tag._ars[i] 6111 } 6112 } 6113 for (i = selected + 1; i < l; i++) { // If there were previous bindings on later tagCtxs, remove them 6114 if (arrBinding = arrayBindings[i]) { 6115 $observe(arrBinding[0], arrBinding[1], true); //unobserve previous binding 6116 delete arrayBindings[i]; 6117 } 6118 } 6119 tag._ars = arrayBindings; 6120 }, 6121 onDispose: function() { 6122 var l, tag = this; 6123 for (l in tag._ars) { 6124 $observe(tag._ars[l][0], tag._ars[l][1], true); //unobserve 6125 } 6126 } 6127}); 6128 6129$extend($tags["if"], { 6130 onUpdate: function(ev, eventArgs, newTagCtxs) { 6131 var tci, prevArg, different; 6132 for (tci = 0; (prevArg = this.tagCtxs[tci]); tci++) { 6133 different = prevArg.props.tmpl !== newTagCtxs[tci].props.tmpl || prevArg.args.length && !(prevArg = prevArg.args[0]) !== !newTagCtxs[tci].args[0]; 6134 if ((!this.convert && !!prevArg) || different) { 6135 return different; 6136 // If there is not a change of template, and there is no converter, and newArg and prevArg are both truthy, return false to cancel update. 6137 // (Even if values on later elses are different, we still don't want to update, since rendered output would be unchanged) 6138 // If newArg and prevArg are different, return true, to update 6139 // If newArg and prevArg are both falsey, move to the next {{else ...}} 6140 } 6141 } 6142 // Boolean value of all args are unchanged (falsey), so return false to cancel update 6143 return false; 6144 }, 6145 onAfterLink: function(tagCtx, linkCtx, ctx, ev, eventArgs) { 6146 if (eventArgs) { 6147 this.domChange(tagCtx, linkCtx, eventArgs); 6148 } 6149 } 6150}); 6151 6152function observeProps(map, ev, eventArgs) { 6153 if (eventArgs.change === "set") { 6154 var target = map.tgt, 6155 l = target.length; 6156 while (l--) { 6157 if (target[l].key === eventArgs.path) { 6158 break; 6159 } 6160 } 6161 if (l === -1) { 6162 if (eventArgs.path && !eventArgs.remove) { 6163 $observable(target).insert({key: eventArgs.path, prop: eventArgs.value}); 6164 } 6165 } else if (eventArgs.remove) { 6166 $observable(target).remove(l); 6167 } else { 6168 $observable(target[l]).setProperty("prop", eventArgs.value); 6169 } 6170 } 6171} 6172 6173function observeMappedProps(map, ev, eventArgs) { 6174 var item, 6175 source = map.src, 6176 change = eventArgs.change; 6177 6178 if (change === "set") { 6179 if (eventArgs.path === "prop") { 6180 $observable(source).setProperty(ev.target.key, eventArgs.value); 6181 } else { // path === "key" 6182 $observable(source).removeProperty(eventArgs.oldValue); // When key is modified observably, remove old one and set new one 6183 $observable(source).setProperty(eventArgs.value, ev.target.prop); 6184 } 6185 } else if (change === "remove") { 6186 item = eventArgs.items[0]; 6187 $observable(source).removeProperty(item.key); 6188 delete source[item.key]; 6189 } else if (change === "insert") { 6190 item = eventArgs.items[0]; 6191 if (item.key) { 6192 $observable(source).setProperty(item.key, item.prop); 6193 } 6194 } 6195} 6196 6197function shallowArrayFilter(allPath /*, object, parentObs*/) { // Filter used by {{props}} for the mappedProps target array 6198 return allPath.indexOf(".") < 0; 6199} 6200 6201$tags("props", { 6202 baseTag: "for", 6203 dataMap: $views.map({ 6204 getTgt: $tags.props.dataMap.getTgt, 6205 obsSrc: observeProps, 6206 obsTgt: observeMappedProps, 6207 tgtFlt: shallowArrayFilter 6208 }), 6209 flow: true 6210}); 6211 6212//======================== 6213// Extend jQuery namespace 6214//======================== 6215 6216$extend($, { 6217 6218 //======================= 6219 // jQuery $.view() plugin 6220 //======================= 6221 6222 view: $view = function(node, inner, type) { 6223 // $.view() returns top view
6224 // $.view(node) returns view that contains node 6225 // $.view(selector) returns view that contains first selected element 6226 // $.view(nodeOrSelector, type) returns nearest containing view of given type 6227 // $.view(nodeOrSelector, "root") returns root containing view (child of top view) 6228 // $.view(nodeOrSelector, true, type) returns nearest inner (contained) view of given type 6229 6230 function getInnerView(nd, isVl) { 6231 if (nd) { 6232 vwInfos = viewInfos(nd, isVl, rOpenViewMarkers); 6233 for (j = 0, k = vwInfos.length; j < k; j++) { 6234 if ((view = viewStore[vwInfos[j].id]) && (view = view && type ? view.get(true, type) : view)) { 6235 break; 6236 } 6237 } 6238 } 6239 } 6240 6241 if (inner !== !!inner) { 6242 // inner not boolean, so this is view(nodeOrSelector, type) 6243 type = inner; 6244 inner = undefined; 6245 } 6246 var view, vwInfos, i, j, k, l, elems, 6247 level = 0, 6248 body = document.body; 6249 6250 if (node && node !== body && topView._.useKey > 1) { 6251 // Perf optimization for common cases 6252 6253 node = "" + node === node 6254 ? $(node)[0] 6255 : node.jquery 6256 ? node[0] 6257 : node; 6258 6259 if (node) { 6260 if (inner) { 6261 getInnerView(node._df, true); 6262 if (!view) { 6263 // Treat supplied node as a container element and return the first view encountered. 6264 elems = qsa ? node.querySelectorAll(bindElsSel) : $(bindElsSel, node).get(); 6265 l = elems.length; 6266 for (i = 0; !view && i < l; i++) { 6267 getInnerView(elems[i]); 6268 } 6269 } 6270 return view; 6271 } 6272 while (node) { 6273 // Move back through siblings and up through parents to find preceding node which is a _prv (prevNode) 6274 // script marker node for a non-element-content view, or a _prv (first node) for an elCnt view 6275 if (vwInfos = viewInfos(node, undefined, rViewMarkers)) { 6276 l = vwInfos.length; 6277 while (l--) { 6278 view = vwInfos[l]; 6279 if (view.open) { 6280 if (level < 1) { 6281 view = viewStore[view.id]; 6282 return view && type ? view.get(type) : view || topView; 6283 } 6284 level--; 6285 } else { 6286 // level starts at zero. If we hit a view.close, then we move level to 1, and we don't return a view until 6287 // we are back at level zero (or a parent view with level < 0) 6288 level++; 6289 } 6290 } 6291 } 6292 node = node.previousSibling || node.parentNode; 6293 } 6294 } 6295 } 6296 return topView; 6297 }, 6298 6299 link: $link, 6300 unlink: $unlink, 6301 6302 //===================== 6303 // override $.cleanData 6304 //===================== 6305 cleanData: function(elems) { 6306 if (elems.length && isCleanCall) { 6307 // Remove JsViews bindings. Also, remove from the DOM any corresponding script marker nodes 6308 clean(elems); 6309 } 6310 oldCleanData.apply($, arguments); 6311 } 6312}); 6313 6314// Possible future addition - e.g. for ckeditor tag control 6315//$views.utility = { 6316// validate: function(html) { 6317// try { 6318// topView.link(undefined, document.createElement("div"), undefined, undefined, html, undefined, undefined, 1); 6319// } 6320// catch (e) { 6321// return e.message; 6322// } 6323// } 6324//}; 6325 6326//=============================== 6327// Extend jQuery instance plugins 6328//=============================== 6329 6330$extend($.fn, { 6331 link: function(expr, from, context, noIteration, parentView, prevNode, nextNode) { 6332 return $link(expr, this, from, context, noIteration, parentView, prevNode, nextNode); 6333 }, 6334 unlink: function() { 6335 return $unlink(this); 6336 }, 6337 view: function(inner, type) { 6338 return $view(this[0], inner, type); 6339 } 6340}); 6341 6342//============================================================================== 6343// Override jQuery methods that call our overridden cleanData, for disposal etc. 6344//============================================================================== 6345 6346$.each([HTML, "replaceWith", "empty", "remove"], function(i, name) { 6347 var oldFn = $.fn[name]; 6348 $.fn[name] = function() { 6349 var result; 6350 isCleanCall = 1; // Make sure cleanData does disposal only when coming from these calls. 6351 try { 6352 result = oldFn.apply(this, arguments); 6353 } 6354 finally { 6355 isCleanCall = 0; 6356 } 6357 return result; 6358 }; 6359}); 6360 6361//=============== 6362// Extend topView 6363//=============== 6364
6365addLinkMethods($extend(topView = $sub.topView, {tmpl: {links: {}}})); 6366 6367viewStore = {0: topView}; // Top-level view 6368//=============== 6369// Extend $.views 6370//=============== 6371 6372$views.getCtx = function(param) { // Return value of ctx.foo, including for compiled contextual parameters, ~foo=expr 6373 if (param && param._cp) { // If this helper resource is a contextual parameter, ~foo=expr 6374 param = param[1](param[0].data, param[0], $sub); 6375 } 6376 return param; 6377}; 6378 6379//=================== 6380// Extend $.views.sub 6381//=================== 6382 6383$sub._cp = function(paramVal, params, view) { // Get compiled contextual parameters (or properties) ~foo=expr. 6384 if (view.linked) { // In JsViews, returns [view, linkFn] where linkFn is compiled function for expression 6385 params = delimOpenChar1 + ":" + params + delimCloseChar0; 6386 var tmpl = view.tmpl, 6387 links = topView.tmpl.links, // Use topView links, as for compiled top-level linking expressions. To do - should this ever get disposed? 6388 linkFn = links[params]; 6389 if (!linkFn) { 6390 links[params] = linkFn = $sub.tmplFn(params, tmpl, true); 6391 } 6392 paramVal = [view, linkFn]; 6393 paramVal._cp = true; // Flag that this is a contextual parameter 6394 } 6395 return paramVal; // In JsRender returns evaluated expression 6396}; 6397 6398$sub._ceo = function cloneExprObjects(obs) { // Clone exprObs so that each referenced contextual parameter ~foo uses its own exprOb instances 6399 var ob, 6400 clones = obs, 6401 l = obs.length; 6402 if (l) { 6403 clones = []; 6404 while (l--) { 6405 ob = obs[l]; 6406 if (ob._jsv) { 6407 ob = $extend({}, ob); // If an exprOb, clone it. If a string, keep as is 6408 ob.prm = cloneExprObjects(ob.prm); // Recursively clone exprObs in parameters, too 6409 } 6410 clones.unshift(ob); 6411 } 6412 } 6413 return clones; 6414}; 6415 6416//========================= 6417// Extend $.views.settings 6418//========================= 6419 6420oldAdvSet = $sub.advSet; 6421 6422$sub.advSet = function() { // refresh advanced settings 6423 oldAdvSet(); 6424 global._jsv = $subSettingsAdvanced._jsv 6425 ? $extend(global._jsv || {}, { // create global _jsv, for accessing views, etc 6426 views: viewStore, 6427 bindings: bindingStore 6428 }) 6429 : undefined; // In IE8 cannot do delete global._jsv 6430 $viewsLinkAttr = $subSettingsAdvanced.linkAttr; 6431 linkViewsSel = bindElsSel + ",[" + $viewsLinkAttr + "]"; 6432 wrapMap = $subSettingsAdvanced._wm; 6433 wrapMap.optgroup = wrapMap.option; 6434 wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead; 6435 wrapMap.th = wrapMap.td; 6436}; 6437 6438$viewsSettings.advanced({ 6439 linkAttr: "data-link", 6440 useViews: false, 6441 noValidate: false, 6442 // wrapMap provide appropriate wrappers for inserting innerHTML, used in insertBefore 6443 // We have to close these tags to support XHTML (#13200) 6444 // TODO investigate whether more recent jQuery implementation using wrapMap in domManip/$().html() etc. is better optimized now... 6445 _wm: { 6446 option: [1, "<select multiple='multiple'>", "</select>"], 6447 legend: [1, "<fieldset>", "</fieldset>"], 6448 area: [1, "<map>", "</map>"], 6449 param: [1, "<object>", "</object>"], 6450 thead: [1, "<table>", "</table>"], 6451 tr: [2, "<table><tbody>", "</tbody></table>"], 6452 td: [3, "<table><tbody><tr>", "</tr></tbody></table>"], 6453 col: [2, "<table><tbody></tbody><colgroup>", "</colgroup></table>"], 6454 svg_ns: [1, "<svg>", "</svg>"], 6455 // IE6-8 can't serialize link, script, style, or any html5 (NoScope) tags, 6456 // unless wrapped in a div with non-breaking characters in front of it. 6457 div: $.support.htmlSerialize ? [0, "", ""] : [1, "X<div>", "</div>"] 6458 }, 6459 _fe: { 6460 input: { 6461 from: inputAttrib, to: "value" 6462 }, 6463 textarea: valueBinding, 6464 select: valueBinding, 6465 optgroup: { 6466 to: "label" 6467 } 6468 } 6469}); 6470 6471return $; 6472}, window));
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.