diff --git a/JQueryPlugin/data/System/JQueryView.txt b/JQueryPlugin/data/System/JQueryView.txt new file mode 100644 index 0000000000..3ceedbc2d1 --- /dev/null +++ b/JQueryPlugin/data/System/JQueryView.txt @@ -0,0 +1,131 @@ +%META:TOPICINFO{author="ProjectContributor" comment="" date="1513189072" format="1.1" version="1"}% +%META:TOPICPARENT{name="JQueryPlugin"}% + +---+!! %TOPIC% + +%JQPLUGINS{"view" + format=" + Homepage: $homepage
+ Author(s): $author
+ Version: $version + " +}% + +%TOC% + +%STARTSECTION{"summary"}% +This plugin adds JsViews functionality to the JsRender plugin. JsViews builds off of standard JsRender templates, but adds +two-way declarative data-binding, MVVM, and MVP. + +See System.JQueryRender and https://www.jsviews.com/#jsviews for more details. +%ENDSECTION{"summary"}% + +---++ Usage + +%JQREQUIRE{"view"}% + +---+++ In the SCRIPT head + +var data = [ + { + "name": "Robert", + "nickname": "Bob", + "showNickname": true + }, + { + "name": "Susan", + "nickname": "Sue", + "showNickname": false + } +]; + +var template = $.templates("#theTmpl"); + +template.link("#result", data); + + +---+++ In the BODY + +
+ + +
+ +---+++ See the demo +
+ + + + + +Pretty cool! + +---++ Further reading + * http://www.jsviews.com + * [[http://borismoore.github.io/jsviews/demos/][JsViews Demos]] + +---++ Syntax + +!JsViews templates are very similar to !JsRender templates, but with minor changes to the tag structure. + * For data-dependent linking, {{:name}} becomes this {^{:name}} + * Tag attributes can also be data-linked: <button data-link="disabled{:disableButton} title{:theTitle} data-myvalue{:myVal} class{:disableButton ? 'class2' : 'class1'}"> + * If you are data-linking tags, you might be interested in two-way binding: <input data-link="{:name}" /> becomes this <input data-link="{:name:}" /> + * (Actually, the default for <input> elements is two-way binding, so you can just use the shorthand <input data-link="name" />. The more explicit form is only necessary if you want to force it to one-way binding.) + * You can also use this for contenteditable elements: <span data-link="name" contenteditable="true"></span> + * As with !JsRender, there is support for converters and helpers as well. + + Other functionality includes the $.observe() method for assigning callback functions to respond to observable + changes, and the $.view() method for retrieving the data slice associated with a particular View object. + + (see http://www.jsviews.com/#jsvapi for lots of details and examples) + diff --git a/JQueryPlugin/lib/Foswiki/Plugins/JQueryPlugin/Config.spec b/JQueryPlugin/lib/Foswiki/Plugins/JQueryPlugin/Config.spec index d4b5950879..ebe77ee45b 100644 --- a/JQueryPlugin/lib/Foswiki/Plugins/JQueryPlugin/Config.spec +++ b/JQueryPlugin/lib/Foswiki/Plugins/JQueryPlugin/Config.spec @@ -226,6 +226,9 @@ $Foswiki::cfg{JQueryPlugin}{Plugins}{'UI::Tooltip'}{Enabled} = 1; # **BOOLEAN LABEL="UI::Validate"** $Foswiki::cfg{JQueryPlugin}{Plugins}{Validate}{Enabled} = 1; +# **BOOLEAN LABEL="View"** +$Foswiki::cfg{JQueryPlugin}{Plugins}{View}{Enabled} = 1; + # **BOOLEAN LABEL="WikiWord"** $Foswiki::cfg{JQueryPlugin}{Plugins}{WikiWord}{Enabled} = 1; diff --git a/JQueryPlugin/lib/Foswiki/Plugins/JQueryPlugin/MANIFEST b/JQueryPlugin/lib/Foswiki/Plugins/JQueryPlugin/MANIFEST index 80fa6e8664..2ae5ea44d2 100644 --- a/JQueryPlugin/lib/Foswiki/Plugins/JQueryPlugin/MANIFEST +++ b/JQueryPlugin/lib/Foswiki/Plugins/JQueryPlugin/MANIFEST @@ -56,6 +56,7 @@ data/System/JQueryUIDialog.txt 0644 data/System/JQueryUITooltip.txt 0644 data/System/JQueryUI.txt 0644 data/System/JQueryValidate.txt 0644 +data/System/JQueryView.txt 0644 data/System/JQueryWikiWord.txt 0644 data/System/VarBUTTON.txt 0644 data/System/VarENDTABPANE.txt 0644 @@ -159,6 +160,7 @@ lib/Foswiki/Plugins/JQueryPlugin/UI/SPINNER.pm 0644 lib/Foswiki/Plugins/JQueryPlugin/UI/TABS.pm 0644 lib/Foswiki/Plugins/JQueryPlugin/UI/TOOLTIP.pm 0644 lib/Foswiki/Plugins/JQueryPlugin/VALIDATE.pm 0644 +lib/Foswiki/Plugins/JQueryPlugin/VIEW.pm 0644 lib/Foswiki/Plugins/JQueryPlugin/WIKIWORD.pm 0644 pub/System/JQueryCycle/beach1.jpg 0644 pub/System/JQueryCycle/beach2.jpg 0644 @@ -966,6 +968,13 @@ pub/System/JQueryPlugin/plugins/validate/Makefile 0644 pub/System/JQueryPlugin/plugins/validate/pkg.js 0644 pub/System/JQueryPlugin/plugins/validate/pkg.js.gz 0644 pub/System/JQueryPlugin/plugins/validate/pkg.uncompressed.js 0644 +pub/System/JQueryPlugin/plugins/view/jquery.observable.js 0644 +pub/System/JQueryPlugin/plugins/view/jquery.observable.js.gz 0644 +pub/System/JQueryPlugin/plugins/view/jquery.observable.uncompressed.js 0644 +pub/System/JQueryPlugin/plugins/view/jquery.views.js 0644 +pub/System/JQueryPlugin/plugins/view/jquery.views.js.gz 0644 +pub/System/JQueryPlugin/plugins/view/jquery.views.uncompressed.js 0644 +pub/System/JQueryPlugin/plugins/view/Makefile 0644 pub/System/JQueryPlugin/plugins/wikiword/downgradeMap.small.uncompressed.js 0644 pub/System/JQueryPlugin/plugins/wikiword/downgradeMap.uncompressed.js 0644 pub/System/JQueryPlugin/plugins/wikiword/jquery.wikiword.uncompressed.js 0644 diff --git a/JQueryPlugin/lib/Foswiki/Plugins/JQueryPlugin/RENDER.pm b/JQueryPlugin/lib/Foswiki/Plugins/JQueryPlugin/RENDER.pm index 7d3f10ab08..eae2c55e7b 100644 --- a/JQueryPlugin/lib/Foswiki/Plugins/JQueryPlugin/RENDER.pm +++ b/JQueryPlugin/lib/Foswiki/Plugins/JQueryPlugin/RENDER.pm @@ -16,7 +16,7 @@ sub new { $class->SUPER::new( $session, name => 'Render', - version => '0.9.83', + version => '0.9.90', author => 'Boris Moore', homepage => 'http://www.jsviews.com', javascript => [ 'jquery.render.js', 'jquery.template-loader.js' ], diff --git a/JQueryPlugin/lib/Foswiki/Plugins/JQueryPlugin/VIEW.pm b/JQueryPlugin/lib/Foswiki/Plugins/JQueryPlugin/VIEW.pm new file mode 100644 index 0000000000..77dca682b5 --- /dev/null +++ b/JQueryPlugin/lib/Foswiki/Plugins/JQueryPlugin/VIEW.pm @@ -0,0 +1,52 @@ +# See bottom of file for license and copyright information + +package Foswiki::Plugins::JQueryPlugin::VIEW; +use strict; +use warnings; + +use Foswiki (); +use Foswiki::Plugins::JQueryPlugin::Plugin (); +our @ISA = qw( Foswiki::Plugins::JQueryPlugin::Plugin ); + +sub new { + my $class = shift; + my $session = shift || $Foswiki::Plugins::SESSION; + + my $this = bless( + $class->SUPER::new( + $session, + name => 'View', + version => '0.9.90', + author => 'Boris Moore', + homepage => 'http://www.jsviews.com', + dependencies => ['render'], + javascript => [ + 'jquery.observable.uncompressed.js', + 'jquery.views.uncompressed.js' + ], + ), + $class + ); + + return $this; +} + +1; + +__END__ + +Foswiki - The Free and Open Source Wiki, http://foswiki.org/ + +Copyright (C) 2010-2016 Foswiki Contributors. Foswiki Contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. For +more details read LICENSE in the root of this distribution. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + +As per the GPL, removal of this notice is prohibited. diff --git a/JQueryPlugin/pub/System/JQueryPlugin/plugins/render/jquery.render.uncompressed.js b/JQueryPlugin/pub/System/JQueryPlugin/plugins/render/jquery.render.uncompressed.js index 4b1a5c5e28..9500ad31c1 100644 --- a/JQueryPlugin/pub/System/JQueryPlugin/plugins/render/jquery.render.uncompressed.js +++ b/JQueryPlugin/pub/System/JQueryPlugin/plugins/render/jquery.render.uncompressed.js @@ -1,15 +1,15 @@ -/*! JsRender v0.9.83 (Beta): http://jsviews.com/#jsrender */ +/*! JsRender v0.9.90 (Beta): http://jsviews.com/#jsrender */ /*! **VERSION FOR WEB** (For NODE.JS see http://jsviews.com/download/jsrender-node.js) */ /* * Best-of-breed templating in browser or on Node.js. * Does not require jQuery, or HTML DOM * Integrates with JsViews (http://jsviews.com/#jsviews) * - * Copyright 2016, Boris Moore + * Copyright 2017, Boris Moore * Released under the MIT License. */ -//jshint -W018, -W041 +//jshint -W018, -W041, -W120 (function(factory, global) { // global var is the this object, which is window when running in the usual browser environment @@ -44,8 +44,9 @@ var setGlobals = $ === false; // Only set globals if script block in browser (no $ = $ && $.fn ? $ : global.jQuery; // $ is jQuery passed in by CommonJS loader (Browserify), or global jQuery. -var versionNumber = "v0.9.83", +var versionNumber = "v0.9.90", jsvStoreName, rTag, rTmplString, topView, $views, $expando, + _ocp = "_ocp", // Observable contextual parameter //TODO tmplFnsCache = {}, $isFunction, $isArray, $templates, $converters, $helpers, $tags, $sub, $subSettings, $subSettingsAdvanced, $viewsSettings, delimOpenChar0, delimOpenChar1, delimCloseChar0, delimCloseChar1, linkChar, setting, baseOnError, @@ -116,20 +117,28 @@ var versionNumber = "v0.9.83", extend: $extend, extendCtx: extendCtx, syntaxErr: syntaxError, - onStore: {}, + onStore: { + template: function(name, item) { + if (item === null) { + delete $render[name]; + } else { + $render[name] = item; + } + } + }, addSetting: addSetting, settings: { allowCode: false }, advSet: noop, // Update advanced settings _ths: tagHandlersFromProps, + _gm: getMethod, _tg: function() {}, // Constructor for tagDef _cnvt: convertVal, _tag: renderTag, _er: error, _err: onRenderError, - _html: htmlEncode, - _cp: retVal, // Get compiled contextual parameters (or properties) ~foo=expr. In JsRender, simply returns val. + _cp: retVal, // Get observable contextual parameters (or properties) ~foo=expr. In JsRender, simply returns val. _sq: function(token) { if (token === "constructor") { syntaxError(""); @@ -149,7 +158,6 @@ var versionNumber = "v0.9.83", : $subSettingsAdvanced; } }, - getCtx: retVal, // Get ctx.foo value. In JsRender, simply returns val. map: dataMap // If jsObservable loaded first, use that definition of dataMap }; @@ -174,7 +182,7 @@ function getMethod(baseMethod, method) { !baseMethod ? noop // no base method implementation, so use noop as base method : baseMethod._d - ? baseMethod // baseMethod is a derived method, so us it + ? baseMethod // baseMethod is a derived method, so use it : getDerivedMethod(noop, baseMethod), // baseMethod is not derived so make its base method be the noop method method ); @@ -184,9 +192,11 @@ function getMethod(baseMethod, method) { } function tagHandlersFromProps(tag, tagCtx) { - for (var prop in tagCtx.props) { - if (rHasHandlers.test(prop)) { - tag[prop] = getMethod(tag[prop], tagCtx.props[prop]); + var prop, + props = tagCtx.props; + for (prop in props) { + if (rHasHandlers.test(prop) && !(tag[prop] && tag[prop].fix)) { // Don't override handlers with fix expando (used in datepicker and spinner) + tag[prop] = prop !== "convert" ? getMethod(tag.constructor.prototype[prop], props[prop]) : props[prop]; // Copy over the onFoo props, convert and convertBack from tagCtx.props to tag (overrides values in tagDef). // Note: unsupported scenario: if handlers are dynamically added ^onFoo=expression this will work, but dynamically removing will not work. } @@ -219,10 +229,12 @@ function JsViewsError(message) { } function $extend(target, source) { - for (var name in source) { - target[name] = source[name]; + if (target) { + for (var name in source) { + target[name] = source[name]; + } + return target; } - return target; } (JsViewsError.prototype = new Error()).constructor = JsViewsError; @@ -264,9 +276,9 @@ function $viewsDelimiters(openChars, closeChars, link) { // Default: bind tagName cvt cln html code params slash bind2 closeBlk comment // /(?:{(\^)?{(?:(\w+(?=[\/\s}]))|(\w+)?(:)|(>)|(\*))\s*((?:[^}]|}(?!}))*?)(\/)?|{(\^)?{(?:(?:\/(\w+))\s*|!--[\s\S]*?--))}} - $sub.rTmpl = new RegExp("<.*>|([^\\\\]|^)[{}]|" + openChars + ".*" + closeChars); - // $sub.rTmpl looks for html tags or { or } char not preceded by \\, or JsRender tags {{xxx}}. Each of these strings are considered - // NOT to be jQuery selectors + $sub.rTmpl = new RegExp("^\\s|\\s$|<.*>|([^\\\\]|^)[{}]|" + openChars + ".*" + closeChars); + // $sub.rTmpl looks for initial or final white space, html tags or { or } char not preceded by \\, or JsRender tags {{xxx}}. + // Each of these strings are considered NOT to be jQuery selectors return $viewsSettings; } @@ -306,10 +318,7 @@ function getView(inner, type) { //view.get(inner, type) } } else if (root) { // Find root view. (view whose parent is top view) - while (view.parent) { - found = view; - view = view.parent; - } + found = view.root; } else { while (view && !found) { // Go through views - this one, and all parent ones - and return first one with given type. @@ -339,40 +348,64 @@ getIndex.depends = "index"; // View.hlp //========== -function getHelper(helper, isContextCb) { - // Helper method called as view.hlp(key) from compiled template, for helpers or template parameters ~foo - var wrapped, deps, - view = this, - res = view.ctx; +function contextParameter(key, value, isContextCb) { + // Helper method called as view.ctxPrm(key) for helpers or template parameters ~foo - from compiled template or from context callback + var wrapped, deps, res, obsCtxPrm, + storeView = this, + isUpdate = !isRenderCall && value !== undefined, + store = storeView.ctx; - if (res) { - res = res[helper]; - } - if (res === undefined) { - res = $helpers[helper]; - } - if (res && res._cp) { // If this helper resource is a contextual parameter, ~foo=expr - if (isContextCb) { // In a context callback for a contextual param, return the [view, dependencies...] array - needed for observe call - deps = $sub._ceo(res[1].deps); // fn deps, with any exprObs cloned - deps.unshift(res[0]); // view - deps._cp = true; - return deps; + if (key in store || key in (store = $helpers)) { + res = store && store[key]; + if (key === "tag" || key === "root" || key === "parentTags" || storeView._.it === key ) { + return res; } - res = $views.getCtx(res); // If a contextual param, but not a context callback, return evaluated param - fn(data, view, j) - } - - if (res) { - if ($isFunction(res) && !res._wrp) { - // 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 - // view as 'this' context. If the helper ~foo() was in a data-link expression, the view will have a 'temporary' linkCtx property too. - // Note that helper functions on deeper paths will have specific this pointers, from the preceding path. - // For example, ~util.foo() will have the ~util object as 'this' pointer - wrapped = function() { - return res.apply((!this || this === global) ? view : this, arguments); - }; - wrapped._wrp = view; - $extend(wrapped, res); // Attach same expandos (if any) to the wrapped function + } else { + store = undefined; + } + if (!res || !$isFunction(res) && storeView.linked || storeView.tagCtx) { // Data-linked view, or tag instance + if (!res || !res._cxp) { + // Not a contextual parameter + if (store !== $helpers) { + // Set storeView to tag (if this is a tag.ctxPrm() call) or to root view ("data" view of linked template) + storeView = storeView.tagCtx + ? storeView // Is a tag, not a view + : (storeView = storeView.scope || storeView, !storeView.isTop && storeView.ctx.tag || storeView); + store = storeView._ocps; + res = store && store[key] || res; + } + if (!(res && res._cxp) && (isContextCb || isUpdate)) { + res = $sub._crcp(key, res, storeView, store); // Create observable contextual parameter + } } + if (obsCtxPrm = res && res._cxp) { + if (isUpdate) { + return $sub._ucp(key, value, storeView, obsCtxPrm); // Update observable contextual parameter + } + if (isContextCb) { // If this helper resource is an observable contextual parameter + // In a context callback for a contextual param, return the [view, dependencies...] array - needed for observe call + deps = res[1] ? $sub._ceo(res[1].deps) : [_ocp]; // fn deps (with any exprObs cloned using $sub._ceo) + deps.unshift(res[0]); // view + deps._cxp = obsCtxPrm; + return deps; + } + res = res[1] // linkFn for compiled expression + ? obsCtxPrm.tag && obsCtxPrm.tag.cvtArgs + ? obsCtxPrm.tag.cvtArgs(true, obsCtxPrm.tagElse)[obsCtxPrm.ind] // = tag.bndArgs() - for tag contextual parameter + : res[1](res[0].data, res[0], $sub) // = fn(data, view, $sub) for compiled binding expression + : res[0]._ocp; // Observable contextual parameter (uninitialized, or initialized as static expression, so no path dependencies) + } + } + if (res && $isFunction(res)) { + // If a helper 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 + // view as 'this' context. If the helper ~foo() was in a data-link expression, the view will have a 'temporary' linkCtx property too. + // Note that helper functions on deeper paths will have specific this pointers, from the preceding path. + // For example, ~util.foo() will have the ~util object as 'this' pointer + wrapped = function() { + return res.apply((!this || this === global) ? storeView : this, arguments); + }; + $extend(wrapped, res); // Attach same expandos (if any) to the wrapped function + wrapped._vw = storeView; } return wrapped || res; } @@ -388,84 +421,136 @@ function getTemplate(tmpl) { //============== function convertVal(converter, view, tagCtx, onError) { + // Called from compiled template code for {{:}} // self is template object or linkCtx object - var tag, value, - // if tagCtx is an integer, then it is the key for the compiled function to return the boundTag tagCtx + var tag, value, argsLen, bindTo, + // If tagCtx is an integer, then it is the key for the compiled function to return the boundTag tagCtx boundTag = typeof tagCtx === "number" && view.tmpl.bnds[tagCtx-1], linkCtx = view.linkCtx; // For data-link="{cvt:...}"... + if (onError === undefined && boundTag && boundTag._lr) { // lateRender + onError = ""; + } if (onError !== undefined) { tagCtx = onError = {props: {}, args: [onError]}; } else if (boundTag) { tagCtx = boundTag(view.data, view, $sub); } - + boundTag = boundTag._bd && boundTag; value = tagCtx.args[0]; if (converter || boundTag) { tag = linkCtx && linkCtx.tag; + tagCtx.view = view; if (!tag) { tag = $extend(new $sub._tg(), { _: { - inline: !linkCtx, bnd: boundTag, unlinked: true }, + inline: !linkCtx, tagName: ":", - cvt: converter, + convert: converter, flow: true, tagCtx: tagCtx }); + argsLen = tagCtx.args.length; + if (argsLen>1) { + bindTo = tag.bindTo = []; + while (argsLen--) { + bindTo.unshift(argsLen); // Bind to all the arguments - generate bindTo array: [0,1,2...] + } + } if (linkCtx) { linkCtx.tag = tag; tag.linkCtx = linkCtx; } tagCtx.ctx = extendCtx(tagCtx.ctx, (linkCtx ? linkCtx.view : view).ctx); + tagHandlersFromProps(tag, tagCtx); } tag._er = onError && value; - tagHandlersFromProps(tag, tagCtx); - - tagCtx.view = view; - tag.ctx = tagCtx.ctx || tag.ctx || {}; tagCtx.ctx = undefined; - value = tag.cvtArgs(converter !== "true" && converter)[0]; // If there is a convertBack but no convert, converter will be "true" - - // Call onRender (used by JsViews if present, to add binding annotations around rendered content) - value = boundTag && view._.onRender - ? view._.onRender(value, view, tag) - : value; + value = tag.cvtArgs()[0]; // If there is a convertBack but no convert, converter will be "true" } + + // Call onRender (used by JsViews if present, to add binding annotations around rendered content) + value = boundTag && view._.onRender + ? view._.onRender(value, view, tag) + : value; return value != undefined ? value : ""; } -function convertArgs(converter) { - var tag = this, - tagCtx = tag.tagCtx, - view = tagCtx.view, - args = tagCtx.args; +function convertArgs(bound, tagElse) { // tag.cvtArgs() + var l, key, boundArgs, args, bindTo, tag, converter, + tagCtx = this; + + if (tagCtx.tagName) { + tag = tagCtx; + tagCtx = tag.tagCtxs ? tag.tagCtxs[tagElse || 0] : tag.tagCtx; + } else { + tag = tagCtx.tag; + } - converter = converter || tag.convert; - converter = converter && ("" + converter === converter - ? (view.getRsc("converters", converter) || error("Unknown converter: '" + converter + "'")) - : converter); + bindTo = tag.bindTo; + args = tagCtx.args; - args = !args.length && !tagCtx.index // On the opening tag with no args, bind to the current data context - ? [view.data] - : converter - ? args.slice() // If there is a converter, use a copy of the tagCtx.args array for rendering, and replace the args[0] in - // the copied array with the converted value. But we do not modify the value of tag.tagCtx.args[0] (the original args array) - : args; // If no converter, get the original tagCtx.args + if ((converter = tag.convert) && "" + converter === converter) { + converter = converter === "true" + ? undefined + : (tagCtx.view.getRsc("converters", converter) || error("Unknown converter: '" + converter + "'")); + } + if (bound && bound.length) { + args = bound; + } else { + if (converter && !bound) { // If there is a converter, use a copy of the tagCtx.args array for rendering, and replace the args[0] in + args = args.slice(); // the copied array with the converted value. But we do not modify the value of tag.tagCtx.args[0] (the original args array) + } + if (bindTo) { // Get the values of the boundArgs + boundArgs = []; + l = bindTo.length; + while (l--) { + key = bindTo[l]; + boundArgs.unshift(argOrProp(tagCtx, key)); + } + if (bound) { + args = boundArgs; // Call to convertBoundArgs() - returns the boundArgs + } + } + } if (converter) { - if (converter.depends) { - tag.depends = $sub.getDeps(tag.depends, tag, converter.depends, converter); + bindTo = bindTo || [0]; + l = bindTo.length; + converter = converter.apply(tag, boundArgs || args); + if (!$isArray(converter) || converter.length !== l) { + converter = [converter]; + bindTo = [0]; + l = 1; + } + if (bound) { // Call to bndArgs convertBoundArgs() - so apply converter to all boundArgs + args = converter; // The array of values returned from the converter + } else { // Call to cvtArgs() + while (l--) { + key = bindTo[l]; + if (+key === key) { + args[key] = converter[l]; + } + } } - args[0] = converter.apply(tag, args); } return args; } +function argOrProp(context, key) { + context = context[+key === key ? "args" : "props"]; + return context && context[key]; +} + +function convertBoundArgs(tagElse) { // tag.bndArgs() + return this.cvtArgs(true, tagElse); +} + //============= // views._tag //============= @@ -473,23 +558,37 @@ function convertArgs(converter) { function getResource(resourceType, itemName) { var res, store, view = this; - while ((res === undefined) && view) { - store = view.tmpl && view.tmpl[resourceType]; - res = store && store[itemName]; - view = view.parent; + if ("" + itemName === itemName) { + while ((res === undefined) && view) { + store = view.tmpl && view.tmpl[resourceType]; + res = store && store[itemName]; + view = view.parent; + } + return res || $views[resourceType][itemName]; } - return res || $views[resourceType][itemName]; } function renderTag(tagName, parentView, tmpl, tagCtxs, isUpdate, onError) { + function makeArray(type) { + var linkedElement; + if (linkedElement = tag[type]) { + tag[type] = linkedElement = $isArray(linkedElement) ? linkedElement: [linkedElement]; + + if (bindToLength !== linkedElement.length) { + error(type + " length not same as bindTo "); + } + } + } + parentView = parentView || topView; - var tag, tag_, tagDef, template, tags, attr, parentTag, i, l, itemRet, tagCtx, tagCtxCtx, - content, callInit, mapDef, thisMap, args, props, initialTmpl, tagDataMap, contentCtx, + var tag, tag_, tagDef, template, tags, attr, parentTag, l, m, n, itemRet, tagCtx, tagCtxCtx, ctxPrm, bindTo, + content, callInit, mapDef, thisMap, args, props, tagDataMap, contentCtx, key, bindToLength, + i = 0, ret = "", linkCtx = parentView.linkCtx || 0, ctx = parentView.ctx, parentTmpl = tmpl || parentView.tmpl, - // if tagCtx is an integer, then it is the key for the compiled function to return the boundTag tagCtxs + // If tagCtxs is an integer, then it is the key for the compiled function to return the boundTag tagCtxs boundTag = typeof tagCtxs === "number" && parentView.tmpl.bnds[tagCtxs-1]; if (tagName._is === "tag") { @@ -501,25 +600,29 @@ function renderTag(tagName, parentView, tmpl, tagCtxs, isUpdate, onError) { tagDef = parentView.getRsc("tags", tagName) || error("Unknown tag: {{" + tagName + "}} "); template = tagDef.template; } - + if (onError === undefined && boundTag) { + if (boundTag._lr = (tagDef.lateRender || boundTag._lr) && boundTag._lr !== "false") { + onError = ""; // If lateRender, set temporary onError, to skip initial rendering (and render just "") + } + } if (onError !== undefined) { ret += onError; - tagCtxs = onError = [{props: {}, args: []}]; + tagCtxs = onError = [{props: {}, args: [], params: {}}]; } else if (boundTag) { tagCtxs = boundTag(parentView.data, parentView, $sub); } l = tagCtxs.length; - for (i = 0; i < l; i++) { + for (; i < l; i++) { tagCtx = tagCtxs[i]; - if (!linkCtx || !linkCtx.tag || i && !linkCtx.tag._.inline || tag._er) { + content = tagCtx.tmpl; + if (!linkCtx || !linkCtx.tag || i && !linkCtx.tag.inline || tag._er || content && +content===content) { // Initialize tagCtx // For block tags, tagCtx.tmpl is an integer > 0 - if (content = parentTmpl.tmpls && tagCtx.tmpl) { - content = tagCtx.content = parentTmpl.tmpls[content - 1]; + if (content && parentTmpl.tmpls) { + tagCtx.tmpl = tagCtx.content = parentTmpl.tmpls[content - 1]; // Set the tmpl property to the content of the block tag } tagCtx.index = i; - tagCtx.tmpl = content; // Set the tmpl property to the content of the block tag tagCtx.render = renderContent; tagCtx.view = parentView; tagCtx.ctx = extendCtx(tagCtx.ctx, ctx); // Clone and extend parentView.ctx @@ -527,11 +630,11 @@ function renderTag(tagName, parentView, tmpl, tagCtxs, isUpdate, onError) { if (tmpl = tagCtx.props.tmpl) { // If the tmpl property is overridden, set the value (when initializing, or, in case of binding: ^tmpl=..., when updating) tagCtx.tmpl = parentView.getTmpl(tmpl); + tagCtx.content = tagCtx.content || tagCtx.tmpl; } if (!tag) { // This will only be hit for initial tagCtx (not for {{else}}) - if the tag instance does not exist yet - // Instantiate tag if it does not yet exist // If the tag has not already been instantiated, we will create a new instance. // ~tag will access the tag, even within the rendering of the template content of this tag. // From child/descendant tags, can access using ~tag.parent, or ~parentTags.tagName @@ -543,7 +646,7 @@ function renderTag(tagName, parentView, tmpl, tagCtxs, isUpdate, onError) { tagDataMap = tag.dataMap; if (linkCtx) { - tag._.inline = false; + tag.inline = false; linkCtx.tag = tag; tag.linkCtx = linkCtx; } @@ -551,7 +654,7 @@ function renderTag(tagName, parentView, tmpl, tagCtxs, isUpdate, onError) { // Bound if {^{tag...}} or data-link="{tag...}" tag._.arrVws = {}; } else if (tag.dataBoundOnly) { - error("{^{" + tagName + "}} tag must be data-bound"); + error(tagName + " must be data-bound:\n{^{" + tagName + "}}"); } //TODO better perf for childTags() - keep child tag.tags array, (and remove child, when disposed) // tag.tags = []; @@ -578,31 +681,42 @@ function renderTag(tagName, parentView, tmpl, tagCtxs, isUpdate, onError) { if (!(tag._er = onError)) { tagHandlersFromProps(tag, tagCtxs[0]); tag.rendering = {}; // Provide object for state during render calls to tag and elses. (Used by {{if}} and {{for}}...) - for (i = 0; i < l; i++) { + for (i = 0; i < l; i++) { // Iterate tagCtx for each {{else}} block tagCtx = tag.tagCtx = tagCtxs[i]; props = tagCtx.props; - args = tag.cvtArgs(); - - if (mapDef = props.dataMap || tagDataMap) { - if (args.length || props.dataMap) { - thisMap = tagCtx.map; - if (!thisMap || thisMap.src !== args[0] || isUpdate) { - if (thisMap && thisMap.src) { - thisMap.unmap(); // only called if observable map - not when only used in JsRender, e.g. by {{props}} - } - thisMap = tagCtx.map = mapDef.map(args[0], props, undefined, !tag._.bnd); - } - args = [thisMap.tgt]; - } - } tag.ctx = tagCtx.ctx; if (!i) { if (callInit) { - initialTmpl = tag.template; tag.init(tagCtx, linkCtx, tag.ctx); callInit = undefined; } + if (!tagCtx.args.length && tag.argDefault !== false) { + tagCtx.args = args = [tagCtx.view.data]; // Missing first arg defaults to the current data context + tagCtx.params.args = ["#data"]; + } + + bindTo = tag.bindTo; + + if (bindTo !== undefined) { + bindTo = tag.bindTo = $isArray(bindTo) ? bindTo : [bindTo]; + m = bindTo.length; + while (m--) { + key = bindTo[m]; + if (!isNaN(parseInt(key))) { + key = parseInt(key); // Convert "0" to 0, etc. + } + bindTo[m] = key; + } + } + + bindTo = tag.bindTo || [0]; + bindToLength = bindTo.length; + if (tag._.bnd){ + makeArray("linkedElement"); + makeArray("linkedCtxParam"); + } + if (linkCtx) { // Set attr on linkCtx to ensure outputting to the correct target attribute. // Setting either linkCtx.attr or this.attr in the init() allows per-instance choice of target attrib. @@ -611,23 +725,54 @@ function renderTag(tagName, parentView, tmpl, tagCtxs, isUpdate, onError) { attr = tag.attr; tag._.noVws = attr && attr !== HTML; } + args = tag.cvtArgs(undefined, i); + if (tag.linkedCtxParam) { + m = bindToLength; + while (m--) { + if (ctxPrm = tag.linkedCtxParam[m]) { + key = bindTo[m]; + // Create tag contextual parameter + tagCtx.ctx[ctxPrm] = $sub._cp(argOrProp(tagCtx, key), argOrProp(tagCtx.params, key), tagCtx.view, tag._.bnd && {tag: tag, ind: m, tagElse: i}); + } + } + } + if (mapDef = props.dataMap || tagDataMap) { + if (args.length || props.dataMap) { + thisMap = tagCtx.map; + if (!thisMap || thisMap.src !== args[0] || isUpdate) { + if (thisMap && thisMap.src) { + thisMap.unmap(); // only called if observable map - not when only used in JsRender, e.g. by {{props}} + } + thisMap = tagCtx.map = mapDef.map(args[0], props, undefined, !tag._.bnd); + } + args = [thisMap.tgt]; + } + } itemRet = undefined; if (tag.render) { itemRet = tag.render.apply(tag, args); - if (parentView.linked && itemRet && tag.linkedElem && !rWrappedInViewMarker.test(itemRet)) { - // When a tag renders content from the render method, with data linking, and has a linkedElem binding, then we need to wrap with - // view markers, if absent, so the content is a view associated with the tag, which will correctly dispose bindings if deleted. - itemRet = renderWithViews($.templates(itemRet), args[0], undefined, undefined, parentView, undefined, undefined, tag); + if (parentView.linked && itemRet && !rWrappedInViewMarker.test(itemRet)) { + // When a tag renders content from the render method, with data linking then we need to wrap with view markers, if absent, + // to provide a contentView for the tag, which will correctly dispose bindings if deleted. The 'tmpl' for this view will + // be a dumbed-down template which will always return the itemRet string (no matter what the data is). The itemRet string + // is not compiled as template markup, so can include "{{" or "}}" without triggering syntax errors + tmpl = { // 'Dumbed-down' template which always renders 'static' itemRet string + links: [] + }; + tmpl.render = tmpl.fn = function() { + return itemRet; + }; + itemRet = renderWithViews(tmpl, parentView.data, undefined, true, parentView, undefined, undefined, tag); } } if (!args.length) { args = [parentView]; // no arguments - (e.g. {{else}}) get data context from view. } if (itemRet === undefined) { - contentCtx = args[0]; // Default data context for wrapped block content is the first argument. Defined tag.contentCtx function to override this. - if (tag.contentCtx) { - contentCtx = tag.contentCtx(contentCtx); + contentCtx = args[0]; // Default data context for wrapped block content is the first argument + if (tag.contentCtx) { // Set tag.contentCtx to true, to inherit parent context, or to a function to provide alternate context. + contentCtx = tag.contentCtx === true ? parentView : tag.contentCtx(contentCtx); } itemRet = tagCtx.render(contentCtx, true) || (isUpdate ? undefined : ""); } @@ -640,7 +785,7 @@ function renderTag(tagName, parentView, tmpl, tagCtxs, isUpdate, onError) { tag.ctx = tag.tagCtx.ctx; if (tag._.noVws) { - if (tag._.inline) { + if (tag.inline) { // inline tag with attr set to "text" will insert HTML-encoded content - as if it was element-based innerText ret = attr === "text" ? $converters.html(ret) @@ -662,27 +807,29 @@ function View(context, type, parentView, data, template, key, onRender, contentT var views, parentView_, tag, self_, self = this, isArray = type === "array"; + // If the data is an array, this is an 'array view' with a views array for each child 'item view' + // If the data is not an array, this is an 'item view' with a views 'hash' object for any child nested views self.content = contentTmpl; self.views = isArray ? [] : {}; - self.parent = parentView; - self.type = type || "top"; self.data = data; self.tmpl = template; - // If the data is an array, this is an 'array view' with a views array for each child 'item view' - // If the data is not an array, this is an 'item view' with a views 'hash' object for any child nested views - // ._.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 self_ = self._ = { key: 0, + // ._.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 useKey: isArray ? 0 : 1, id: "" + viewId++, onRender: onRender, bnds: {} }; self.linked = !!onRender; - if (parentView) { + self.type = type || "top"; + if (self.parent = parentView) { + self.root = parentView.root || self; // view whose parent is top view views = parentView.views; parentView_ = parentView._; + self.isTop = parentView_.scp; // Is top content view of a link("#container", ...) call + self.scope = (!context.tag || context.tag === parentView.ctx.tag) && !self.isTop && parentView.scope || self; if (parentView_.useKey) { // Parent is not an 'array view'. Add this view to its views object // self._key = is the key in the parent view hash @@ -698,7 +845,7 @@ function View(context, type, parentView, data, template, key, onRender, contentT // If context was passed in, it should have been merged already with parent context self.ctx = context || parentView.ctx; } else { - self.ctx = context; + self.ctx = context || {}; } } @@ -707,7 +854,7 @@ View.prototype = { getIndex: getIndex, getRsc: getResource, getTmpl: getTemplate, - hlp: getHelper, + ctxPrm: contextParameter, _is: "view" }; @@ -732,16 +879,16 @@ function compileChildResources(parentTmpl) { //=============== function compileTag(name, tagDef, parentTmpl) { - var tmpl, baseTag, prop, + var tmpl, baseTag, prop, l, key, bindToLength, + bindTo = tagDef.bindTo, compiledDef = new $sub._tg(); function Tag() { var tag = this; tag._ = { - inline: true, unlinked: true }; - + tag.inline = true; tag.tagName = name; } @@ -754,6 +901,7 @@ function compileTag(name, tagDef, parentTmpl) { } else if ("" + tagDef === tagDef) { tagDef = {template: tagDef}; } + if (baseTag = tagDef.baseTag) { tagDef.flow = !!tagDef.flow; // Set flow property, so defaults to false even if baseTag has flow=true tagDef.baseTag = baseTag = "" + baseTag === baseTag @@ -773,11 +921,7 @@ function compileTag(name, tagDef, parentTmpl) { if ((tmpl = compiledDef.template) !== undefined) { compiledDef.template = "" + tmpl === tmpl ? ($templates[tmpl] || $templates(tmpl)) : tmpl; } - if (compiledDef.init !== false) { - // Set init: false on tagDef if you want to provide just a render method, or render and template, but no constructor or prototype. - // so equivalent to setting tag to render function, except you can also provide a template. - (Tag.prototype = compiledDef).constructor = compiledDef._ctr = Tag; - } + (Tag.prototype = compiledDef).constructor = compiledDef._ctr = Tag; if (parentTmpl) { compiledDef._parentTmpl = parentTmpl; @@ -817,7 +961,7 @@ function compileTmpl(name, tmpl, parentTmpl, options) { } } else if ($.fn && !$sub.rTmpl.test(value)) { try { - elem = $(document).find(value)[0]; // if jQuery is loaded, test for selector returning elements, and get first element + elem = $ (value, document)[0]; // if jQuery is loaded, test for selector returning elements, and get first element } catch (e) {} }// END BROWSER-SPECIFIC CODE } //BROWSER-SPECIFIC CODE @@ -839,9 +983,10 @@ function compileTmpl(name, tmpl, parentTmpl, options) { value = $templates[currentName]; delete $templates[currentName]; } else if ($.fn) { - value = $.data(elem)[jsvTmpl]; + value = $.data(elem)[jsvTmpl]; // Get cached compiled template } - } else { + } + if (!currentName || !value) { // Not yet compiled, or cached version lost name = name || ($.fn ? jsvTmpl : value); value = compileTmpl(name, elem.innerHTML, parentTmpl, options); } @@ -865,6 +1010,7 @@ function compileTmpl(name, tmpl, parentTmpl, options) { var elem, compiledTmpl, tmplOrMarkup = tmpl = tmpl || ""; + $sub._html = $converters.html; //==== Compile the template ==== if (options === 0) { @@ -907,9 +1053,6 @@ function compileTmpl(name, tmpl, parentTmpl, options) { compileChildResources(compiledTmpl); } - if (name && !parentTmpl && name !== jsvTmpl) { - $render[name] = compiledTmpl; - } return compiledTmpl; } } @@ -921,15 +1064,16 @@ function compileTmpl(name, tmpl, parentTmpl, options) { //================= function getDefaultVal(defaultVal, data) { - return $.isFunction(defaultVal) + return $isFunction(defaultVal) ? defaultVal.call(data) : defaultVal; } function unmapArray(modelArr) { - var i, arr = [], + var arr = [], + i = 0, l = modelArr.length; - for (i=0; i= 0; } @@ -1929,8 +2080,9 @@ function parseParams(params, pathBindings, tmpl) { function buildCode(ast, tmpl, isLinkExpr) { // Build the template function code from the AST nodes, and set as property on the passed-in template object // Used for compiling templates, and also by JsViews to build functions for data link expressions - var i, node, tagName, converter, tagCtx, hasTag, hasEncoder, getsVal, hasCnvt, useCnvt, tmplBindings, pathBindings, params, boundOnErrStart, boundOnErrEnd, - tagRender, nestedTmpls, tmplName, nestedTmpl, tagAndElses, content, markup, nextIsElse, oldCode, isElse, isGetVal, tagCtxFn, onError, tagStart, trigger, + var i, node, tagName, converter, tagCtx, hasTag, hasEncoder, getsVal, hasCnvt, useCnvt, tmplBindings, pathBindings, params, boundOnErrStart, + boundOnErrEnd, tagRender, nestedTmpls, tmplName, nestedTmpl, tagAndElses, content, markup, nextIsElse, oldCode, isElse, isGetVal, tagCtxFn, + onError, tagStart, trigger, lateRender, tmplBindingKey = 0, useViews = $subSettingsAdvanced.useViews || tmpl.useViews || tmpl.tags || tmpl.templates || tmpl.helpers || tmpl.converters, code = "", @@ -1969,16 +2121,16 @@ function buildCode(ast, tmpl, isLinkExpr) { converter = node[1]; content = !isLinkExpr && node[2]; tagCtx = paramStructure(node[3], 'params') + '},' + paramStructure(params = node[4]); - onError = node[5]; trigger = node[6]; - markup = node[8] && node[8].replace(rUnescapeQuotes, "$1"); + lateRender = node[7]; + markup = node[9] && node[9].replace(rUnescapeQuotes, "$1"); if (isElse = tagName === "else") { if (pathBindings) { - pathBindings.push(node[7]); + pathBindings.push(node[8]); } } else { - tmplBindingKey = 0; - if (tmplBindings && (pathBindings = node[7])) { // Array of paths, or false if not data-bound + onError = node[5] || $subSettings.debugMode !== false && "undefined"; // If debugMode not false, set default onError handler on tag to "undefined" (see onRenderError) + if (tmplBindings && (pathBindings = node[8])) { // Array of paths, or false if not data-bound pathBindings = [pathBindings]; tmplBindingKey = tmplBindings.push(1); // Add placeholder in tmplBindings for compiled function } @@ -2018,12 +2170,14 @@ function buildCode(ast, tmpl, isLinkExpr) { boundOnErrStart = ""; boundOnErrEnd = ""; - if (isGetVal && (pathBindings || trigger || converter && converter !== HTML)) { + if (isGetVal && (pathBindings || trigger || converter && converter !== HTML || lateRender)) { // For convertVal we need a compiled function to return the new tagCtx(s) - tagCtxFn = new Function("data,view,j,u", " // " + tmplName + " " + tmplBindingKey + " " + tagName + tagCtxFn = new Function("data,view,j,u", "// " + tmplName + " " + (++tmplBindingKey) + " " + tagName + "\nreturn {" + tagCtx + "};"); tagCtxFn._er = onError; tagCtxFn._tag = tagName; + tagCtxFn._bd = !!pathBindings; // data-linked tag {^{.../}} + tagCtxFn._lr = lateRender; if (isLinkExpr) { return tagCtxFn; @@ -2037,7 +2191,7 @@ function buildCode(ast, tmpl, isLinkExpr) { } code += (isGetVal ? (isLinkExpr ? (onError ? "try{\n" : "") + "return " : tagStart) + (useCnvt // Call _cnvt if there is a converter: {{cnvt: ... }} or {^{cnvt: ... }} - ? (useCnvt = undefined, useViews = hasCnvt = true, tagRender + (pathBindings + ? (useCnvt = undefined, useViews = hasCnvt = true, tagRender + (tagCtxFn ? ((tmplBindings[tmplBindingKey - 1] = tagCtxFn), tmplBindingKey) // Store the compiled tagCtxFn in tmpl.bnds, and pass the key to convertVal() : "{" + tagCtx + "}") + ")") : tagName === ">" @@ -2062,6 +2216,7 @@ function buildCode(ast, tmpl, isLinkExpr) { if (pathBindings) { setPaths(tmplBindings[tmplBindingKey - 1] = code, pathBindings); } + code._lr = lateRender; if (isLinkExpr) { return code; // For a data-link expression we return the compiled tagCtxs function } @@ -2072,11 +2227,11 @@ function buildCode(ast, tmpl, isLinkExpr) { // This is the last {{else}} for an inline tag. // For a bound tag, pass the tagCtxs fn lookup key to renderTag. // For an unbound tag, include the code directly for evaluating tagCtxs array - code = oldCode + tagStart + tagRender + (tmplBindingKey || code) + ")"; + code = oldCode + tagStart + tagRender + (code.deps && tmplBindingKey || code) + ")"; pathBindings = 0; tagAndElses = 0; } - if (onError) { + if (onError && !nextIsElse) { useViews = true; code += ';\n}catch(e){ret' + (isLinkExpr ? "urn " : "+=") + boundOnErrStart + 'j._err(e,view,' + onError + ')' + boundOnErrEnd + ';}' + (isLinkExpr ? "" : 'ret=ret'); } @@ -2095,14 +2250,10 @@ function buildCode(ast, tmpl, isLinkExpr) { + code + (isLinkExpr ? "\n" : ";\nreturn ret;"); - if ($subSettings.debugMode !== false) { - code = "try {\n" + code + "\n}catch(e){\nreturn j._err(e, view);\n}"; - } - try { code = new Function("data,view,j,u", code); } catch (e) { - syntaxError("Compiled template code:\n\n" + code + '\n: "' + e.message + '"'); + syntaxError("Compiled template code:\n\n" + code + '\n: "' + (e.message||e) + '"'); } if (tmpl) { tmpl.fn = code; @@ -2152,7 +2303,8 @@ function $fnRender(data, context, noIteration) { var tmplElem = this.jquery && (this[0] || error('Unknown template')), // Targeted element not found for jQuery template selector such as "#myTmpl" tmpl = tmplElem.getAttribute(tmplAttr); - return renderContent.call(tmpl ? $.data(tmplElem)[jsvTmpl] : $templates(tmplElem), data, context, noIteration); + return renderContent.call(tmpl && $.data(tmplElem)[jsvTmpl] || $templates(tmplElem), + data, context, noIteration); } //========================== Register converters ========================== @@ -2179,7 +2331,9 @@ if (!(jsr || $ && $.render)) { $sub._tg.prototype = { baseApply: baseApply, - cvtArgs: convertArgs + cvtArgs: convertArgs, + bndArgs: convertBoundArgs, + ctxPrm: contextParameter }; topView = $sub.topView = new View(); @@ -2249,7 +2403,7 @@ if (!(jsr || $ && $.render)) { : ( $subSettings.debugMode = debugMode, $subSettings.onError = debugMode + "" === debugMode - ? new Function("", "return '" + debugMode + "';" ) + ? new Function("", "return '" + debugMode + "';") : $isFunction(debugMode) ? debugMode : undefined, @@ -2274,12 +2428,12 @@ if (!(jsr || $ && $.render)) { tagCtx = self.tagCtx, ret = (self.rendering.done || !val && (arguments.length || !tagCtx.index)) ? "" - : (self.rendering.done = true, self.selected = tagCtx.index, - // Test is satisfied, so render content on current context. We call tagCtx.render() rather than return undefined - // (which would also render the tmpl/content on the current context but would iterate if it is an array) - tagCtx.render(tagCtx.view, true)); // no arg, so renders against parentView.data + : (self.rendering.done = true, + self.selected = tagCtx.index, + undefined); // Test is satisfied, so render content on current context return ret; }, + contentCtx: true, // Inherit parent view data context flow: true }, "for": { @@ -2343,6 +2497,7 @@ $subSettings = $sub.settings; $isArray = ($||jsr).isArray; $viewsSettings.delimiters("{{", "}}", "^"); + if (jsrToJq) { // Moving from jsrender namespace to jQuery namepace - copy over the stored items (templates, converters, helpers...) jsr.views.sub._jq($); } diff --git a/JQueryPlugin/pub/System/JQueryPlugin/plugins/view/Makefile b/JQueryPlugin/pub/System/JQueryPlugin/plugins/view/Makefile new file mode 100644 index 0000000000..395e8e4417 --- /dev/null +++ b/JQueryPlugin/pub/System/JQueryPlugin/plugins/view/Makefile @@ -0,0 +1,17 @@ +TARGET=jquery.observable.js jquery.views.js + +-include ../../Makefile.include + +git: + git clone https://github.com/BorisMoore/jsviews.git git + +ifneq (,$(wildcard git)) +jquery.observable.uncompressed.js: git/jquery.observable.js + cp $< $@ + +jquery.views.uncompressed.js: git/jquery.views.js + cp $< $@ +endif + +clean: + rm -f $(TARGET) *.gz diff --git a/JQueryPlugin/pub/System/JQueryPlugin/plugins/view/jquery.observable.js b/JQueryPlugin/pub/System/JQueryPlugin/plugins/view/jquery.observable.js new file mode 100644 index 0000000000..1e057b196b --- /dev/null +++ b/JQueryPlugin/pub/System/JQueryPlugin/plugins/view/jquery.observable.js @@ -0,0 +1 @@ +(function(factory,global){var $=global.jQuery;if(typeof exports==="object"){module.exports=$?factory(global,$):function($){return factory(global,$)}}else if(typeof define==="function"&&define.amd){define(["jquery"],function($){return factory(global,$)})}else{factory(global,false)}})(function(global,$){"use strict";var setGlobals=$===false;$=$||global.jQuery;if(!$||!$.fn){throw"JsObservable requires jQuery"}var versionNumber="v0.9.90",_ocp="_ocp",$observe,$observable,$views=$.views=$.views||setGlobals&&global.jsrender&&jsrender.views||{jsviews:versionNumber,sub:{settings:{}},settings:{advanced:function(value){$subSettingsAdvanced=$subSettings.advanced=$subSettings.advanced||{_jsv:true};return value?("_jsv"in value&&($subSettingsAdvanced._jsv=value._jsv),$sub.advSet(),$views.settings):$subSettingsAdvanced}}},$sub=$views.sub,$subSettings=$sub.settings,$subSettingsAdvanced=$subSettings.advanced,$isFunction=$.isFunction,$expando=$.expando,$isArray=$.isArray,OBJECT="object";if(!$.observe){var $eventSpecial=$.event.special,slice=[].slice,splice=[].splice,concat=[].concat,PARSEINT=parseInt,rNotWhite=/\S+/g,propertyChangeStr=$sub.propChng=$sub.propChng||"propertyChange",arrayChangeStr=$sub.arrChng=$sub.arrChng||"arrayChange",cbBindingsStore={},observeStr=propertyChangeStr+".observe",observeObjKey=1,observeCbKey=1,observeInnerCbKey=1,$hasData=$.hasData,$data=$.data,remove={},getCbKey=function(cb){return cb._cId=cb._cId||".obs"+observeCbKey++},ObjectObservable=function(ns,data){this._data=data;this._ns=ns;return this},ArrayObservable=function(ns,data){this._data=data;this._ns=ns;return this},wrapArray=function(data){return $isArray(data)?[data]:data},dependsPaths=function(paths,root,callback){paths=paths?$isArray(paths)?paths:[paths]:[];var i,path,object,rt,nextObj=object=root,l=paths&&paths.length,out=[];for(i=0;i1){object=object[parts.shift()]}}if(object){self._setProperty(object,parts[0],value,nonStrict)}}}return self},removeProperty:function(path){this.setProperty(path,remove);return this},_setProperty:function(leaf,path,value,nonStrict){var setter,getter,removeProp,property=path?leaf[path]:leaf;if($isFunction(property)){if(property.set){leaf=leaf._vw||leaf;getter=property;setter=getter.set===true?getter:getter.set;property=getter.call(leaf)}}if((property!==value||nonStrict&&property!=value)&&(!(property instanceof Date&&value instanceof Date)||property>value||property-1){data=$isArray(data)?data:[data];if(data.length){this._insert(index,data)}}return this},_insert:function(index,data){var _data=this._data,oldLength=_data.length;if(index>oldLength){index=oldLength}splice.apply(_data,[index,0].concat(data));this._trigger({change:"insert",index:index,items:data},oldLength)},remove:function(index,numToRemove){var items,_data=this._data;if(index===undefined){index=_data.length-1}index=PARSEINT(index);numToRemove=numToRemove?PARSEINT(numToRemove):numToRemove===0?0:1;if(numToRemove>0&&index>-1){items=_data.slice(index,index+numToRemove);if(numToRemove=items.length){this._remove(index,numToRemove,items)}}return this},_remove:function(index,numToRemove,items){var _data=this._data,oldLength=_data.length;_data.splice(index,numToRemove);this._trigger({change:"remove",index:index,items:items},oldLength)},move:function(oldIndex,newIndex,numToMove){numToMove=numToMove?PARSEINT(numToMove):numToMove===0?0:1;oldIndex=PARSEINT(oldIndex);newIndex=PARSEINT(newIndex);if(numToMove>0&&oldIndex>-1&&newIndex>-1&&oldIndex!==newIndex){this._move(oldIndex,newIndex,numToMove)}return this},_move:function(oldIndex,newIndex,numToMove){var items,_data=this._data,oldLength=_data.length,excess=oldIndex+numToMove-oldLength;if(excess>0){numToMove-=excess}if(numToMove){items=_data.splice(oldIndex,numToMove);if(newIndex>_data.length){newIndex=_data.length}splice.apply(_data,[newIndex,0].concat(items));if(newIndex!==oldIndex){this._trigger({change:"move",oldIndex:oldIndex,index:newIndex,items:items},oldLength)}}},refresh:function(newItems){function insertAdded(){if(k){self.insert(j-k,addedItems);dataLength+=k;i+=k;k=0;addedItems=[]}}var i,j,k,newItem,num,self=this,addedItems=[],data=self._data,oldItems=data.slice(),oldLength=data.length,dataLength=oldLength,newLength=newItems.length;self._srt=true;for(j=k=0;jj){self.remove(j,dataLength-j)}self._srt=undefined;self._trigger({change:"refresh",oldItems:oldItems},oldLength);return self},_trigger:function(eventArgs,oldLength){var self=this,_data=self._data,length=_data.length,$_data=$([_data]);if(self._srt){eventArgs.refresh=true}else if(length!==oldLength){$_data.triggerHandler(propertyChangeStr,{change:"set",path:"length",value:length,oldValue:oldLength})}$_data.triggerHandler(arrayChangeStr+(self._ns?"."+/^\S+/.exec(self._ns)[0]:""),eventArgs)}};$eventSpecial[propertyChangeStr]=$eventSpecial[arrayChangeStr]={remove:function(handleObj){var cbBindings,found,events,l,data,evData=handleObj.data;if(evData&&(evData.off=true,evData=evData.cb)){if(cbBindings=cbBindingsStore[evData._cId]){events=$._data(this).events[handleObj.type];l=events.length;while(l--&&!found){found=(data=events[l].data)&&data.cb&&data.cb._cId===evData._cId}if(!found){delete cbBindings[$data(this).obId];removeCbBindings(cbBindings,evData._cId)}}}}};$views.map=function(mapDef){function Map(source,options,target,unbound){var changing,map=this;if(this.src){this.unmap()}if(typeof source===OBJECT){map.src=source;map.tgt=target||map.tgt||[];map.options=options||map.options;map.update();if(!unbound){if(mapDef.obsSrc){$observable(map.src).observeAll(map.obs=function(ev,eventArgs){if(!changing){changing=true;mapDef.obsSrc(map,ev,eventArgs);changing=undefined}},map.srcFlt)}if(mapDef.obsTgt){$observable(map.tgt).observeAll(map.obt=function(ev,eventArgs){if(!changing){changing=true;mapDef.obsTgt(map,ev,eventArgs);changing=undefined}},map.tgtFlt)}}}}if($isFunction(mapDef)){mapDef={getTgt:mapDef}}if(mapDef.baseMap){mapDef=$.extend({},mapDef.baseMap,mapDef)}mapDef.map=function(source,options,target,unbound){return new Map(source,options,target,unbound)};(Map.prototype={srcFlt:mapDef.srcFlt||shallowFilter,tgtFlt:mapDef.tgtFlt||shallowFilter,update:function(options){var map=this;$observable(map.tgt).refresh(mapDef.getTgt(map.src,map.options=options||map.options))},unmap:function(){var map=this;if(map.src){if(map.obs){$observable(map.src).unobserveAll(map.obs,map.srcFlt)}if(map.obt){$observable(map.tgt).unobserveAll(map.obt,map.tgtFlt)}map.src=undefined}},map:Map,_def:mapDef}).constructor=Map;return mapDef};$sub.advSet=function(){$sub._gccb=this._gccb;global._jsv=$subSettings.advanced._jsv?{cbBindings:cbBindingsStore}:undefined};$sub._dp=dependsPaths}return $},window); \ No newline at end of file diff --git a/JQueryPlugin/pub/System/JQueryPlugin/plugins/view/jquery.observable.uncompressed.js b/JQueryPlugin/pub/System/JQueryPlugin/plugins/view/jquery.observable.uncompressed.js new file mode 100644 index 0000000000..8ee6e4e53e --- /dev/null +++ b/JQueryPlugin/pub/System/JQueryPlugin/plugins/view/jquery.observable.uncompressed.js @@ -0,0 +1,1146 @@ +/*! JsObservable v0.9.90 (Beta): http://jsviews.com/#jsobservable */ +/* + * Subcomponent of JsViews + * Data change events for data-linking + * + * Copyright 2017, Boris Moore + * Released under the MIT License. + */ + +//jshint -W018, -W041, -W120 + +(function(factory, global) { + // global var is the this object, which is window when running in the usual browser environment + var $ = global.jQuery; + + if (typeof exports === "object") { // CommonJS e.g. Browserify + module.exports = $ + ? factory(global, $) + : function($) { // If no global jQuery, take jQuery passed as parameter: require("jsobservable")(jQuery) + return factory(global, $); + }; + } else if (typeof define === "function" && define.amd) { // AMD script loader, e.g. RequireJS + define(["jquery"], function($) { + return factory(global, $); // Require jQuery + }); + } else { // Browser using plain ',openScript='', + openScript = ' - data-linked tag, close marker + // We validate with inTag so no script markers are inserted in attribute context e.g. for: + // "" or "
...{{/if}}..." + preceding = id + ? (preceding + endOfElCnt + spaceBefore + (inTag ? "" : openScript + id + closeScript)+ spaceAfter + tag) + : endOfElCnt || all; + } + + if (validate && boundId) { + if (inTag) { + // JsViews data-linking tags are not allowed within element markup. + // See jsviews/issues/303 + syntaxError('{^{ within elem markup (' + inTag + ' ). Use data-link="..."'); + } + if (id.charAt(0) === "#") { + tagStack.unshift(id.slice(1)); + } else if (id.slice(1) !== (bndId = tagStack.shift())) { + // See jsviews/issues/213 + syntaxError('Closing tag for {^{...}} under different elem: <' + bndId + '>'); + } + } + if (tag) { + inTag = tag; + // If there are ids (markers since the last tag), move them to the defer string + tagStack.unshift(parentTag); + parentTag = tag.slice(1); + if (validate && tagStack[0] && tagStack[0] === badParent[parentTag]) { + // Missing + // TODO: replace this by smart insertion of tags + error('Parent of must be '); + } + isVoid = voidElems[parentTag]; + if ((elCnt = elContent[parentTag]) && !prevElCnt) { + deferStack.unshift(defer); + defer = ""; + } + prevElCnt = elCnt; +//TODO Consider providing validation which throws if you place as child of , etc. - since if not caught, +//this can cause errors subsequently which are difficult to debug. +// if (elContent[tagStack[0]]>2 && !elCnt) { +// error(parentTag + " in " + tagStack[0]); +// } + if (defer && elCnt) { + defer += "+"; // Will be used for stepping back through deferred tokens + } + } + return preceding; + } + + function processViewInfos(vwInfos, targetParent) { + // If targetParent, we are processing viewInfos (which may include navigation through '+-' paths) and hooking up to the right parentElem etc. + // (and elem may also be defined - the next node) + // If no targetParent, then we are processing viewInfos on newly inserted content + var deferPath, deferChar, bindChar, parentElem, id, onAftCr, deep, + addedBindEls = []; + + // In elCnt context (element-only content model), prevNode is the first node after the open, nextNode is the first node after the close. + // If both are null/undefined, then open and close are at end of parent content, so the view is empty, and its placeholder is the + // 'lastChild' of the parentNode. If there is a prevNode, then it is either the first node in the view, or the view is empty and + // its placeholder is the 'previousSibling' of the prevNode, which is also the nextNode. + if (vwInfos) { + if (vwInfos._tkns.charAt(0) === "@") { + // We are processing newly inserted content. This is a special script element that was created in convertMarkers() to process deferred bindings, + // and inserted following the target parent element - because no element tags (outside elCnt) were encountered to carry those binding tokens. + // We will step back from the preceding sibling of this element, looking at targetParent elements until we find the one that the current binding + // token belongs to. Set elem to null (the special script element), and remove it from the DOM. + targetParent = elem.previousSibling; + elem.parentNode.removeChild(elem); + elem = undefined; + } + len = vwInfos.length; + while (len--) { + vwInfo = vwInfos[len]; +//if (prevIds.indexOf(vwInfo.token) < 0) { // This token is a newly created view or tag binding + bindChar = vwInfo.ch; + if (deferPath = vwInfo.path) { + // We have a 'deferred path' + j = deferPath.length - 1; + while (deferChar = deferPath.charAt(j--)) { + // Use the "+" and"-" characters to navigate the path back to the original parent node where the deferred bindings ocurred + if (deferChar === "+") { + if (deferPath.charAt(j) === "-") { + j--; + targetParent = targetParent.previousSibling; + } else { + targetParent = targetParent.parentNode; + } + } else { + targetParent = targetParent.lastChild; + } + // Note: Can use previousSibling and lastChild, not previousElementSibling and lastElementChild, + // since we have removed white space within elCnt. Hence support IE < 9 + } + } + if (bindChar === "^") { + if (tag = bindingStore[id = vwInfo.id]) { + // The binding may have been deleted, for example in a different handler to an array collectionChange event + // This is a tag binding + deep = targetParent && (!elem || elem.parentNode !== targetParent); // We are stepping back looking for the right targetParent, + // or we are linking existing content and this element is in elCnt, not an immediate child of the targetParent. + if (!elem || deep) { + tag.parentElem = targetParent; + } + if (vwInfo.elCnt && deep) { + // With element only content, if there is no following element, or if the binding is deeper than the following element + // then we need to set the open or close token as a deferred binding annotation on the parent + setDefer(targetParent, (vwInfo.open ? "#" : "/") + id + bindChar + (targetParent._df || "")); + } + // This is an open or close marker for a data-linked tag {^{...}}. Add it to bindEls. + addedBindEls.push([deep ? null : elem, vwInfo]); + } + } else if (view = viewStore[id = vwInfo.id]) { + // The view may have been deleted, for example in a different handler to an array collectionChange event + if (!view.parentElem) { + // If view is not already extended for JsViews, extend and initialize the view object created in JsRender, as a JsViews view + view.parentElem = targetParent || elem && elem.parentNode || parentNode; + view._.onRender = addBindingMarkers; + view._.onArrayChange = arrayChangeHandler; + setArrayChangeLink(view); + } + parentElem = view.parentElem; + if (vwInfo.open) { + // This is an 'open view' node (preceding script marker node, + // or if elCnt, the first element in the view, with a data-jsv annotation) for binding + view._elCnt = vwInfo.elCnt; + if (targetParent && !elem) { + setDefer(targetParent, "#" + id + bindChar + (targetParent._df || "")); + } else { + // No targetParent, so there is a ._nxt elem (and this is processing tokens on the elem) + if (!view._prv) { + setDefer(parentElem, removeSubStr(parentElem._df, "#" + id + bindChar)); + } + view._prv = elem; + } + } else { + // This is a 'close view' marker node for binding + if (targetParent && (!elem || elem.parentNode !== targetParent)) { + // There is no ._nxt so add token to _df. It is deferred. + setDefer(targetParent, "/" + id + bindChar + (targetParent._df || "")); + view._nxt = undefined; + } else if (elem) { + // This view did not have a ._nxt, but has one now, so token may be in _df, and must be removed. (No longer deferred) + if (!view._nxt) { + setDefer(parentElem, removeSubStr(parentElem._df, "/" + id + bindChar)); + } + view._nxt = elem; + } + if (onAftCr = view.ctx && view.ctx[onAfterCreateStr] || onAfterCreate) { + onAftCr.call(view.ctx.tag, view); + } + } +//} + } + } + len = addedBindEls.length; + while (len--) { + // These were added in reverse order to addedBindEls. We push them in BindEls in the correct order. + bindEls.push(addedBindEls[len]); + } + } + return !vwInfos || vwInfos.elCnt; + } + + function getViewInfos(vwInfos) { + // Used by view.childTags() and tag.childTags() + // Similar to processViewInfos in how it steps through bindings to find tags. Only finds data-linked tags. + var level, parentTag, named; + + if (vwInfos) { + len = vwInfos.length; + for (j = 0; j < len; j++) { + vwInfo = vwInfos[j]; + // 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. + // Note - if bindingStore[vwInfo.id]._is === "tag" then getViewInfos is being called too soon - during first linking pass + tag = bindingStore[vwInfo.id]; + if (!tag._is && tag.linkCtx) { + parentTag = tag = tag.linkCtx.tag; + named = tag.tagName === tagName; + if (!tag.flow || named) { + if (!deep) { + level = 1; + while (parentTag = parentTag.parent) { + level++; + } + tagDepth = tagDepth || level; // The level of the first tag encountered. + } + if ((deep || level === tagDepth) && (!tagName || named)) { + // Filter on top-level or tagName as appropriate + tags.push(tag); + } + } + } + } + } + } + + function dataLink() { + //================ Data-link and fixup of data-jsv annotations ================ + var j, index, + tokens = "", + wrap = {}, + selector = linkViewsSel + (get ? ",[" + deferAttr + "]" : ""); + // If a childTags() call, get = ",[" + deferAttr + "]" - since we need to include elements that have a ._df expando for deferred tokens + + elems = qsa ? parentNode.querySelectorAll(selector) : $(selector, parentNode).get(); + l = elems.length; + + // The prevNode will be in the returned query, since we called markPrevOrNextNode() on it. + // But it may have contained nodes that satisfy the selector also. + if (prevNode && prevNode.innerHTML) { + // Find the last contained node of prevNode, to use as the prevNode - so we only link subsequent elems in the query + prevNodes = qsa ? prevNode.querySelectorAll(selector) : $(selector, prevNode).get(); + prevNode = prevNodes.length ? prevNodes[prevNodes.length - 1] : prevNode; + } + + tagDepth = 0; + for (i = 0; i < l; i++) { + elem = elems[i]; + if (prevNode && !found) { + // If prevNode is set, not false, skip linking. If this element is the prevNode, set to false so subsequent elements will link. + found = (elem === prevNode); + } else if (nextNode && elem === nextNode) { + // If nextNode is set then break when we get to nextNode + if (get) { + tokens += markerNodeInfo(elem); + } + break; + } else if (elem.parentNode) { + // elem has not been removed from DOM + if (get) { + tokens += markerNodeInfo(elem); + if (elem._df) { + j = i + 1; + while (j < l && elem.contains(elems[j])) { + j++; + } + // Add deferred tokens after any tokens on descendant elements of this one + wrap[j-1] = elem._df; + } + if (wrap[i]) { + tokens += wrap[i] || ""; + } + } else { + if (isLink && (vwInfo = viewInfos(elem, undefined, rViewMarkers)) && (vwInfo = vwInfo[0])) { + // If this is a link(trueOrString ...) call we will avoid re-binding to elems that are within template-rendered views + skip = skip ? (vwInfo.id !== skip && skip) : vwInfo.open && vwInfo.id; + } + if (!skip && processInfos(viewInfos(elem)) + // If a link() call, processViewInfos() adds bindings to bindEls, and returns true for non-script nodes, for adding data-link bindings + // If a childTags() call, getViewInfos returns array of tag bindings. + && elem.getAttribute($viewsLinkAttr)) { + bindEls.push([elem]); // A data-linked element so add to bindEls too + } + } + } + } + + if (get) { + tokens += parentNode._df || ""; + if (index = tokens.indexOf("#" + get.id) + 1) { + // We are looking for view.childTags() or tag.childTags() - so start after the open token of the parent view or tag. + tokens = tokens.slice(index + get.id.length); + } + index = tokens.indexOf("/" + get.id); + if (index + 1) { + // We are looking for view.childTags() or tag.childTags() - so don't look beyond the close token of the parent view or tag. + tokens = tokens.slice(0, index); + } + // Call getViewInfos to add the found childTags to the tags array + getViewInfos(viewInfos(tokens, undefined, rOpenTagMarkers)); + } + + if (html === undefined && parentNode.getAttribute($viewsLinkAttr)) { + bindEls.push([parentNode]); // Support data-linking top-level element directly (not within a data-linked container) + } + + // Remove temporary marker script nodes they were added by markPrevOrNextNode + unmarkPrevOrNextNode(prevNode, elCnt); + unmarkPrevOrNextNode(nextNode, elCnt); + + if (get) { + return; // We have added childTags to the tags array, so we are done + } + + if (elCnt && defer + ids) { + // 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 + elem = nextNode; + if (defer) { + if (nextNode) { + processViewInfos(viewInfos(defer + "+", true), nextNode); + } else { + processViewInfos(viewInfos(defer, true), parentNode); + } + } + processViewInfos(viewInfos(ids, true), parentNode); + // If there were any tokens on nextNode which have now been associated with inserted HTML tags, remove them from nextNode + if (nextNode) { + tokens = nextNode.getAttribute(jsvAttrStr); + if (l = tokens.indexOf(prevIds) + 1) { + tokens = tokens.slice(l + prevIds.length - 1); + } + nextNode.setAttribute(jsvAttrStr, ids + tokens); + } + } + +// if (context.lazyLink) { +// setTimeout(doLinking) (doLinking is function wrapper of following lines) +// See Future tasks, and https://github.com/BorisMoore/jsviews/issues/368. +// Could call context.lazyLink as callback, on async completion - or return promise. + //================ Bind the data-linked elements and tags ================ + l = bindEls.length; + for (i = 0; i < l; i++) { + elem = bindEls[i]; + linkInfo = elem[1]; + elem = elem[0]; + if (linkInfo) { + if (tag = bindingStore[linkInfo.id]) { + if (linkCtx = tag.linkCtx) { + // The tag may have been stored temporarily on the bindingStore - or may have already been replaced by the actual binding + tag = linkCtx.tag; + tag.linkCtx = linkCtx; + } + if (linkInfo.open) { + // This is an 'open linked tag' binding annotation for a data-linked tag {^{...}} + if (elem) { + tag.parentElem = elem.parentNode; + tag._prv = elem; + } + tag._elCnt = linkInfo.elCnt; + // We data-link depth-first ("on the way in"), which is better for perf - and allows setting parent tags etc. + view = tag.tagCtx.view; + + // Add data binding (unless skipped due to lateRender) + addDataBinding(late, undefined, tag._prv, view, linkInfo.id); + } else { + tag._nxt = elem; + if (tag._.unlinked && !tag._toLk) { + // This is a 'close linked tag' binding annotation (and data-binding was not skipped due to lateRender) + tagCtx = tag.tagCtx; + view = tagCtx.view; + callAfterLink(tag); + } + } + } + } else { + // Add data binding for a data-linked element (with data-link attribute) + addDataBinding(late, elem.getAttribute($viewsLinkAttr), elem, $view(elem), undefined, isLink, outerData, context); + } + } +//}); + } + //==== /end of nested functions ==== + + var inTag, linkCtx, tag, i, l, j, len, elems, elem, view, vwInfo, linkInfo, prevNodes, token, prevView, nextView, + node, tags, deep, tagName, tagCtx, validate, tagDepth, depth, fragment, copiedNode, firstTag, parentTag, + isVoid, wrapper, div, tokens, elCnt, prevElCnt, htmlTag, ids, prevIds, found, skip, isLink, get, + self = this, + thisId = self._.id + "_", + defer = "", + // The marker ids for which no tag was encountered (empty views or final closing markers) which we carry over to container tag + bindEls = [], + tagStack = [], + deferStack = [], + late = [], + onAfterCreate = changeHandler(self, onAfterCreateStr), + processInfos = processViewInfos; + + if (refresh) { + if (refresh.tmpl) { + // refresh is the prevView, passed in from addViews() + prevView = "/" + refresh._.id + "_"; + } else { + isLink = refresh.lnk; // Top-level linking + if (refresh.tag) { + thisId = refresh.tag + "^"; + refresh = true; + } + if (get = refresh.get) { + processInfos = getViewInfos; + tags = get.tags; + deep = get.deep; + tagName = get.name; + } + } + refresh = refresh === true; + } + + parentNode = parentNode + ? ("" + parentNode === parentNode + ? $(parentNode)[0] // It is a string, so treat as selector + : parentNode.jquery + ? parentNode[0] // A jQuery object - take first element. + : parentNode) + : (self.parentElem // view.link() + || document.body); // link(null, data) to link the whole document + + validate = !$subSettingsAdvanced.noValidate && parentNode.contentEditable !== TRUE; + parentTag = parentNode.tagName.toLowerCase(); + elCnt = !!elContent[parentTag]; + + prevNode = prevNode && markPrevOrNextNode(prevNode, elCnt); + nextNode = nextNode && markPrevOrNextNode(nextNode, elCnt) || null; + + if (html != undefined) { + //================ Insert html into DOM using documentFragments (and wrapping HTML appropriately). ================ + // Also convert markers to DOM annotations, based on content model. + // Corresponds to nextNode ? $(nextNode).before(html) : $(parentNode).html(html); + // but allows insertion to wrap correctly even with inserted script nodes. jQuery version will fail e.g. under tbody or select. + // This version should also be slightly faster + div = document.createElement("div"); + wrapper = div; + prevIds = ids = ""; + htmlTag = parentNode.namespaceURI === "http://www.w3.org/2000/svg" ? "svg_ns" : (firstTag = rFirstElem.exec(html)) && firstTag[1] || ""; + if (elCnt) { + // Now look for following view, and find its tokens, or if not found, get the parentNode._df tokens + node = nextNode; + while (node && !(nextView = viewInfos(node))) { + node = node.nextSibling; + } + if (tokens = nextView ? nextView._tkns : parentNode._df) { + token = prevView || ""; + if (refresh || !prevView) { + token += "#" + thisId; + } + j = tokens.indexOf(token); + if (j + 1) { + j += token.length; + // Transfer the initial tokens to inserted nodes, by setting them as the ids variable, picked up in convertMarkers + prevIds = ids = tokens.slice(0, j); + tokens = tokens.slice(j); + if (nextView) { + node.setAttribute(jsvAttrStr, tokens); + } else { + setDefer(parentNode, tokens); + } + } + } + } + + //================ Convert the markers to DOM annotations, based on content model. ================ +// oldElCnt = elCnt; + isVoid = undefined; + html = ("" + html).replace(rConvertMarkers, convertMarkers); +// if (!!oldElCnt !== !!elCnt) { +// error("Parse: " + html); // Parse error. Content not well-formed? +// } + if (validate && tagStack.length) { + syntaxError("Mismatched '<" + parentTag + "...>' in:\n" + html); // Unmatched tag + } + if (validateOnly) { + return; + } + // Append wrapper element to doc fragment + safeFragment.appendChild(div); + + // Go to html and back, then peel off extra wrappers + // Corresponds to jQuery $(nextNode).before(html) or $(parentNode).html(html); + // but supports svg elements, and other features missing from jQuery version (and this version should also be slightly faster) + htmlTag = wrapMap[htmlTag] || wrapMap.div; + depth = htmlTag[0]; + wrapper.innerHTML = htmlTag[1] + html + htmlTag[2]; + while (depth--) { + wrapper = wrapper.lastChild; + } + safeFragment.removeChild(div); + fragment = document.createDocumentFragment(); + while (copiedNode = wrapper.firstChild) { + fragment.appendChild(copiedNode); + } + // Insert into the DOM + parentNode.insertBefore(fragment, nextNode); + } + dataLink(); + + return late; +} + +function addDataBinding(late, linkMarkup, node, currentView, boundTagId, isLink, data, context) { + // Add data binding for data-linked elements or {^{...}} data-linked tags + var tmpl, tokens, attr, convertBack, tagExpr, linkFn, linkCtx, tag, rTagIndex, hasElse, lastIndex, + linkExpressions = []; + + if (boundTagId) { + // boundTagId is a string for {^{...}} data-linked tag. So only one linkTag in linkMarkup + // data and context arguments are undefined + tag = bindingStore[boundTagId]; + tag = tag.linkCtx ? tag.linkCtx.tag : tag; + + linkCtx = tag.linkCtx || { + type: "inline", + data: currentView.data, // source + elem: tag._elCnt ? tag.parentElem : node, // target + view: currentView, + ctx: currentView.ctx, + attr: HTML, // Script marker nodes are associated with {^{ and always target HTML. + fn: tag._.bnd, + tag: tag, + // Pass the boundTagId in the linkCtx, so that it can be picked up in observeAndBind + _bndId: boundTagId + }; + tag.linkCtx = linkCtx; + bindDataLinkTarget(linkCtx, late); + tag._toLk = linkCtx._bndId; // If data binding happened, remove _toLk flag from tag + } else if (linkMarkup && node) { + // Data-linked element + + // If isLink then this is a top-level linking: .link(expression, target, data, ....) or + // .link(true, target, data, ....) scenario - and data and context are passed in separately from the view + data = isLink ? data : currentView.data; + + // Compiled linkFn expressions could be stored in the tmpl.links array of the template + // TODO - consider also caching globally so that if {{:foo}} or data-link="foo" occurs in different places, + // the compiled template for this is cached and only compiled once... + //links = currentView.links || currentView.tmpl.links; + + tmpl = currentView.tmpl; + +// if (!(linkTags = links[linkMarkup])) { + // This is the first time this view template has been linked, so we compile the data-link expressions, and store them on the template. + + linkMarkup = normalizeLinkTag(linkMarkup, defaultAttr(node)); + lastIndex = rTagDatalink.lastIndex = 0; + while (tokens = rTagDatalink.exec(linkMarkup)) { // TODO require } to be followed by whitespace or $, and remove the \}(!\}) option. + linkExpressions.push(tokens); + lastIndex = rTagDatalink.lastIndex; + } + if (lastIndex < linkMarkup.length) { + syntaxError(linkMarkup); + } + while (tokens = linkExpressions.shift()) { + // Iterate over the data-link expressions, for different target attrs, + // e.g. 15 && which < 21 || which > 32 && which < 41 || which > 111 && which < 131 || which === 27 || which === 144)) { + // Shift, Ctrl, Alt, Pause, Caplock, Page up/down End, Home, Left, Up, Right, Down, Function keys, Escape, Numlock + setTimeout(function() { + onElemChange(ev); + }); + } +} + +function bindTriggerEvent($elem, trig, onoff) { + // Bind keydown, or other trigger - (rather than use the default change event bubbled to activeBody) + if (trig === true && useInput) { + $elem[onoff]("input.jsv", onElemChange); // For HTML5 browser with "oninput" support - for mouse editing of text + } else { + trig = "" + trig === trig ? trig : "keydown.jsv"; // Set trigger to (true || truey non-string (e.g. 1) || 'keydown') + $elem[onoff](trig, trig.indexOf("keydown") >= 0 ? asyncOnElemChange : onElemChange); // Get 'keydown' with async + } +} + +function bindLinkedElChange(tag, linkedElem) { + // Two-way binding for linkedElem - in the case of input, textarea or contentEditable elements. + // Trigger setting may have changed. Unbind previous trigger binding (if any) and bind new one. + + var $linkedElem, newTrig, + oldTrig = linkedElem._jsvTr || false; + + if (tag) { + newTrig = tag.tagCtx.props.trigger; + if (newTrig === undefined) { + newTrig = tag.trigger; + } + } + if (newTrig === undefined) { + newTrig = $subSettings.trigger; + } + // Trigger is noop except for text box, textarea, contenteditable... + newTrig = newTrig && (linkedElem.tagName === "INPUT" && linkedElem.type !== CHECKBOX && linkedElem.type !== RADIO + || linkedElem.type === "textarea" || linkedElem.contentEditable === TRUE) && newTrig || false; + + if (oldTrig !== newTrig) { + $linkedElem = $(linkedElem); + bindTriggerEvent($linkedElem, oldTrig, "off"); + bindTriggerEvent($linkedElem, linkedElem._jsvTr = newTrig, "on"); + } +} + +function defineBindToDataTargets(binding, tag, cvtBk) { + // Two-way binding. + // We set the binding.to[1] to be the cvtBack, and binding.to[0] to be either the path to the target, or [object, path] where the target is the + // path on the provided object. So for a computed path with an object call: a.b.getObject().d.e, we set to[0] to be [exprOb, "d.e"], and + // we bind to the path on the returned object, exprOb.ob, as target. Otherwise our target is the first path, paths[0], which we will convert + // with contextCb() for paths like ~a.b.c or #x.y.z + + var pathIndex, path, lastPath, bindtoOb, to, bindTo, paths, k, l, obsCtxPrm, linkedCtxParam, contextCb, targetPaths, bindTos, + tagElse = 1, + tos = [], + linkCtx = binding.linkCtx, + source = linkCtx.data, + targetPathsElses = linkCtx.fn.paths; + + if (binding && !binding.to) { + if (tag) { + tag.convertBack = tag.convertBack || cvtBk; + bindTo = tag.bindTo; + tagElse = tag.tagCtxs ? tag.tagCtxs.length : 1; + } + while (tagElse--) { + bindTos = []; + if (targetPaths = targetPathsElses[tagElse]) { + bindTo = targetPaths._jsvto ? ["_jsvto"] : (bindTo || [0]); + k = bindTo.length; + while (k--) { + path = ""; + contextCb = linkCtx._ctxCb; + paths = targetPaths[bindTo[k]]; + if (pathIndex = paths && paths.length) { + lastPath = paths[pathIndex - 1]; + if (lastPath._cpfn) { // Computed property exprOb + + bindtoOb = lastPath; + while (lastPath.sb && lastPath.sb._cpfn) { + path = lastPath = lastPath.sb; + } + path = lastPath.sb || path && path.path; + lastPath = path ? path.slice(1) : bindtoOb.path; + } + to = path + ? [bindtoOb, // 'exprOb' for this expression and view-binding. So bindtoOb.ob is current object returned by expression. + lastPath] + : resolveDataTargetPath(lastPath, source, contextCb); // Get 'to' for target path: lastPath + } else { + // Contextual parameter ~foo with no external binding - has ctx.foo = [{_ocp: xxx}] and binds to ctx.foo._ocp + linkedCtxParam = tag.linkedCtxParam; + to = []; + if (linkedCtxParam && linkedCtxParam[k]) { + // This is a tag binding, with linked tag contextual parameters + to = [tag.tagCtxs[tagElse].ctx[linkedCtxParam[k]][0], _ocp]; + } + } + if ((obsCtxPrm = to._cxp) && obsCtxPrm.tag && lastPath.indexOf(".")<0) { + // This is a binding for a tag contextual parameter (e.g. within a tag block content + to = obsCtxPrm; + } + bindTos.unshift(to); + } + } + tos.unshift(bindTos); + } + binding.to = tos; + } +} + +function resolveDataTargetPath(targetPath, source, contextCb) { + // Iteratively process targetPath, resolving ~a.b.c paths for contextual parameters + var path, bindtoOb, to, l, obsCtxPrm, view, topCp, data; + + while (targetPath && targetPath !== _ocp && (to = contextCb(path = targetPath.split("^").join("."), source)) && (l = to.length)) { + if (obsCtxPrm = to[0]._cxp) { // Two-way binding to a contextual parameter reference, ~foo (declared as ~foo=expr on a parent tag) + topCp = topCp || obsCtxPrm; + view = to[0][0]; + if (_ocp in view) { + data = view; + view = view._vw; + } else { + data = view.data; + } + topCp.path = targetPath = to[0][1]; + to = [topCp.data = data, targetPath]; + contextCb = $sub._gccb(view); + if (targetPath._cpfn) { // computed property + bindtoOb = targetPath; + bindtoOb.data = to[0]; + bindtoOb._cpCtx = contextCb; + while (targetPath.sb && targetPath.sb._cpfn) { + path = targetPath = targetPath.sb; + } + path = targetPath.sb || path && path.path; + targetPath = path ? path.slice(1) : bindtoOb.path; + to = [ + bindtoOb, // 'exprOb' for this expression and view-binding. So bindtoOb.ob is current object returned by expression. + targetPath + ]; + } else if (obsCtxPrm.tag && obsCtxPrm.path === _ocp) { + to = obsCtxPrm; + } + } else { // Two-way binding to a helper - e.g. ~address.street, or computed, e.g. ~fullName(), or view property e.g. #data.foo + to = l>2 + ? [to[l-3], to[l-2]] // With path: [object, path] + : [to[l-2]]; // No path, (e.g. [function] for computed with setter) + } + source = to[0]; + targetPath = to[1]; + } + to = to || [source, path]; + to._cxp = topCp; + return to; +} + +function mergeCtxs(tag, newCtxs, replace) { // Merge updated tagCtxs into tag.tagCtxs + var tagCtx, newTagCtx, + view = tag.tagCtx.view, + tagCtxs = tag.tagCtxs || [tag.tagCtx], + l = tagCtxs.length, + refresh = !newCtxs; + + newCtxs = newCtxs || tag._.bnd.call(view.tmpl, (tag.linkCtx || view).data, view, $sub); + + if (replace) { + // Replace previous tagCtxs by new ones, rather than merging + tagCtxs = tag.tagCtxs = newCtxs; + tag.tagCtx = tagCtxs[0]; + addLinkMethods(tag); + } else { + while (l--) { + tagCtx = tagCtxs[l]; + newTagCtx = newCtxs[l]; + $observable(tagCtx.props).setProperty(newTagCtx.props); + $extend(tagCtx.ctx, newTagCtx.ctx); // We don't support propagating ctx variables, ~foo, observably, to nested views. So extend, not setProperty... + tagCtx.args = newTagCtx.args; + if (refresh) { + tagCtx.tmpl = newTagCtx.tmpl; + } + } + } + $sub._ths(tag, tagCtxs[0]); // tagHandlersFromProps + return tagCtxs; +} + +//========= +// Disposal +//========= + +function clean(elems) { + // Remove data-link bindings, or contained views + var l, elem, bindings, + elemArray = [], + len = elems.length, + i = len; + while (i--) { + // Copy into an array, so that deletion of nodes from DOM will not cause our 'i' counter to get shifted + // (Note: This seems as fast or faster than elemArray = [].slice.call(elems); ...) + elemArray.push(elems[i]); + } + i = len; + while (i--) { + elem = elemArray[i]; + if (elem.parentNode) { + // Has not already been removed from the DOM + if (bindings = elem._jsvBnd) { + // Get propertyChange bindings for this element + // This may be an element with data-link, or the opening script marker node for a data-linked tag {^{...}} + // bindings is a string with the syntax: "(&bindingId)*" + bindings = bindings.slice(1).split("&"); + elem._jsvBnd = ""; + l = bindings.length; + while (l--) { + // Remove associated bindings + removeViewBinding(bindings[l], elem._jsvLkEl, elem); // unbind bindings with this bindingId on this view + } + } + disposeTokens(markerNodeInfo(elem) + (elem._df || ""), elem); + } + } +} + +function removeViewBinding(bindId, linkedElemTag, elem) { + // Unbind + var objId, linkCtx, tag, object, obsId, tagCtxs, l, map, linkedElem, trigger, view, tagCtx, linkedElems, allLinkedElems, + binding = bindingStore[bindId]; + + if (linkedElemTag) { + elem._jsvLkEl = undefined; + } else if (binding && (!elem || elem === binding.elem)) { // Test that elem is actually binding.elem, since cloned elements can have inappropriate markerNode info + delete bindingStore[bindId]; // Delete already, so call to onDispose handler below cannot trigger recursive deletion (through recursive call to jQuery cleanData) + for (objId in binding.bnd) { + object = binding.bnd[objId]; + obsId = binding.cbId; + if ($isArray(object)) { + $([object]).off(arrayChangeStr + obsId).off(propertyChangeStr + obsId); // There may be either or both of arrayChange and propertyChange + } else { + $(object).off(propertyChangeStr + obsId); + } + delete binding.bnd[objId]; + } + + if (linkCtx = binding.linkCtx) { + if (tag = linkCtx.tag) { + if (tagCtxs = tag.tagCtxs) { + l = tagCtxs.length; + while (l--) { + tagCtx = tagCtxs[l]; + if (map = tagCtx.map) { + map.unmap(); //unobserve + } + // Copy linkedElems in case tag.linkedElem or tag.linkedElems are undefined in onUnbind + if (linkedElems = tagCtx.linkedElems) { + allLinkedElems = (allLinkedElems || []).concat(linkedElems); + } + } + } + + if (tag.onUnbind) { + tag.onUnbind(tag.tagCtx, linkCtx, tag.ctx, true); + } + if (tag.onDispose) { + tag.onDispose(); + } + + if (!tag._elCnt) { + if (tag._prv) { + tag._prv.parentNode.removeChild(tag._prv); + } + if (tag._nxt) { + tag._nxt.parentNode.removeChild(tag._nxt); + } + } + } + + linkedElems = allLinkedElems || [$(linkCtx.elem)]; + l = linkedElems.length; + while (l--) { + linkedElem = linkedElems[l]; + if (trigger = linkedElem && linkedElem[0] && linkedElem[0]._jsvTr) { + bindTriggerEvent(linkedElem, trigger, "off"); + linkedElem[0]._jsvTr = undefined; + } + } + + view = linkCtx.view; + if (view.type === "link") { + view.parent.removeViews(view._.key, undefined, true); // A "link" view is associated with the binding, so should be disposed with binding. + } else { + delete view._.bnds[bindId]; + } + } + delete binding.s[binding.cbId]; + } +} + +function $unlink(to) { + if (to) { + to = to.jquery ? to : $(to); + to.each(function() { + var innerView; + //TODO fix this for better perf. Rather that calling inner view multiple times which does querySelectorAll each time, consider a single querySelectorAll + // or simply call view.removeViews() on the top-level views under the target 'to' node, then clean(...) + while ((innerView = $view(this, true)) && innerView.parent) { + innerView.parent.removeViews(innerView._.key, undefined, true); + } + clean(this.getElementsByTagName("*")); + }); + clean(to); + } else { + // Call to $.unlink() is equivalent to $.unlink(true, "body") + if (activeBody) { + $(activeBody) + .off(elementChangeStr, onElemChange) + .off('blur.jsv', '[contenteditable]', onElemChange); + activeBody = undefined; + } + topView.removeViews(); + clean(document.body.getElementsByTagName("*")); + } +} + +//======== +// Helpers +//======== + +function inputAttrib(elem) { + return elem.type === CHECKBOX ? elem[CHECKED] : elem.value; +} + +function changeHandler(view, name, tag) { + // Get onBeforeChange, onAfterChange, onAfterCreate handler - if there is one; + return tag && tag[name] || view.ctx[name] && view.ctxPrm(name) || $views.helpers[name]; +} + +//========================== Initialize ========================== + +//===================== +// JsRender integration +//===================== + +addLinkMethods($sub.View.prototype); // Modify the View prototype to include link methods + +$sub.onStore.template = function(name, item, parentTmpl) { + if (item === null) { + delete $.link[name]; + delete $.render[name]; + } else { + item.link = tmplLink; + + if (name && !parentTmpl && name !== "jsvTmpl") { + $.render[name] = item; + $.link[name] = function() { + return tmplLink.apply(item, arguments); + }; + } + } +}; + +$sub.viewInfos = viewInfos; // Expose viewInfos() as public helper method + +// Define JsViews version of delimiters(), and initialize +($viewsSettings.delimiters = function() { + // Run delimiters initialization in context of jsrender.js + var ret = oldJsvDelimiters.apply(0, arguments), + // Now set also delimOpenChar0 etc. in context of jquery.views.js... + delimChars = $subSettings.delimiters; + + delimOpenChar0 = delimChars[0].charAt(0); + delimOpenChar1 = delimChars[0].charAt(1); + delimCloseChar0 = delimChars[1].charAt(0); + delimCloseChar1 = delimChars[1].charAt(1); + linkChar = delimChars[2]; + + // Data-linking must use new delimiters + rTagDatalink = new RegExp("(?:^|\\s*)([\\w-]*)(\\" + linkChar + ")?(\\" + delimOpenChar1 + $sub.rTag + "(:\\w*)?\\" + delimCloseChar0 + ")", "g"); + return ret; +})(); // jshint ignore:line + +$sub.addSetting("trigger"); + +//==================================== +// Additional members for linked views +//==================================== + +function transferViewTokens(prevNode, nextNode, parentElem, id, viewOrTagChar, refresh) { + // Transfer tokens on prevNode of viewToRemove/viewToRefresh to nextNode or parentElem._df + // view marker tokens: #m_...VIEW.../m_ + // tag marker tokens: #m^...TAG..../m^ + + var i, l, vwInfos, vwInfo, viewOrTag, viewId, tokens, + precedingLength = 0, + emptyView = prevNode === nextNode; + + if (prevNode) { + // prevNode is either the first node in the viewOrTag, or has been replaced by the vwInfos tokens string + vwInfos = viewInfos(prevNode) || []; + for (i = 0, l = vwInfos.length; i < l; i++) { + // Step through views or tags on the prevNode + vwInfo = vwInfos[i]; + viewId = vwInfo.id; + if (viewId === id && vwInfo.ch === viewOrTagChar) { + if (refresh) { + // This is viewOrTagToRefresh, this is the last viewOrTag to process... + l = 0; + } else { + // This is viewOrTagToRemove, so we are done... + break; + } + } + if (!emptyView) { + viewOrTag = vwInfo.ch === "_" + ? viewStore[viewId] // A view: "#m_" or "/m_" + : bindingStore[viewId].linkCtx.tag; // A tag "#m^" or "/m^" + if (vwInfo.open) { // A "#m_" or "#m^" token + viewOrTag._prv = nextNode; + } else if (vwInfo.close) { // A "/m_" or "/m^" token + viewOrTag._nxt = nextNode; + } + } + precedingLength += viewId.length + 2; + } + + if (precedingLength) { + prevNode.setAttribute(jsvAttrStr, prevNode.getAttribute(jsvAttrStr).slice(precedingLength)); + } + tokens = nextNode ? nextNode.getAttribute(jsvAttrStr) : parentElem._df; + if (l = tokens.indexOf("/" + id + viewOrTagChar) + 1) { + tokens = vwInfos._tkns.slice(0, precedingLength) + tokens.slice(l + (refresh ? -1 : id.length + 1)); + } + if (tokens) { + if (nextNode) { + // If viewOrTagToRemove was an empty viewOrTag, we will remove both #n and /n + // (and any intervening tokens) from the nextNode (=== prevNode) + // If viewOrTagToRemove was not empty, we will take tokens preceding #n from prevNode, + // and concatenate with tokens following /n on nextNode + nextNode.setAttribute(jsvAttrStr, tokens); + } else { + setDefer(parentElem, tokens); + } + } + } else { + // !prevNode, so there may be a deferred nodes token on the parentElem. Remove it. + setDefer(parentElem, removeSubStr(parentElem._df, "#" + id + viewOrTagChar)); + if (!refresh && !nextNode) { + // If this viewOrTag is being removed, and there was no .nxt, remove closing token from deferred tokens + setDefer(parentElem, removeSubStr(parentElem._df, "/" + id + viewOrTagChar)); + } + } +} + +function disposeTokens(tokens, elem) { + var i, l, vwItem, vwInfos; + if (vwInfos = viewInfos(tokens, true, rOpenMarkers)) { + for (i = 0, l = vwInfos.length; i < l; i++) { + vwItem = vwInfos[i]; + if (vwItem.ch === "_") { + if ((vwItem = viewStore[vwItem.id]) && vwItem.type && (!elem || vwItem._prv === elem || vwItem.parentElem === elem )) { + // If this is the _prv (prevNode) for a view, remove the view + // - unless view.type is undefined, in which case it is already being removed + // (or unless the elem is not related - e.g. a cloned element which 'accidentally' picked up the data-jsv atttribute of the ._df expando) + vwItem.parent.removeViews(vwItem._.key, undefined, true); + } + } else { + removeViewBinding(vwItem.id, undefined, elem); // unbind bindings with this bindingId on this view + } + } + } +} + +//============================================ +// Add link methods to data-linked view or tag +//============================================ + +function updateValue(val, index, tagElse, bindId, ev) { +// Observably update a data value targeted by bindTo +// Called when linkedElem changes: called as updateValue(val, index, tagElse, bindId, ev) - this: undefined +// Called directly as tag.updateValue(val, index, tagElse) - this: tag + var values = []; + if (this && this._tgId) { + bindId = this; + } + values[index||0] = val; + updateValues(values, tagElse, bindId, ev); + return this; +} + +function setValues() { + var args = arguments, + m = args.length; + + if (!m) { + args = this.tag.cvtArgs(true, this.index); // setValues() with no arguments calls setValue with boundArgs values + m = args.length; + } + while (m--) { + this.tag.setValue(args[m], m, this.index); + } +} + +function addLinkMethods(tagOrView) { // tagOrView is View prototype or tag instance + + var l, m, tagCtx, boundProps, bindTo, key, theTag, theView; + + tagOrView.contents = function(deep, select) { + // For a view, a tag or a tagCtx, return jQuery object with the content nodes, + if (deep !== !!deep) { + // deep not boolean, so this is contents(selector) + select = deep; + deep = undefined; + } + var filtered, + nodes = $(this.nodes()); + if (nodes[0]) { + filtered = select ? nodes.filter(select) : nodes; + nodes = deep && select ? filtered.add(nodes.find(select)) : filtered; + } + return nodes; + }; + + tagOrView.nodes = function(withMarkers, prevNode, nextNode) { + // For a view, a tag or a tagCtx, return top-level nodes + // Do not return any script marker nodes, unless withMarkers is true + // Optionally limit range, by passing in prevNode or nextNode parameters + + var node, + self = this.contentView || this, // If tagCtx, use tagCtx.contentView + elCnt = self._elCnt, + prevIsFirstNode = !prevNode && elCnt, + nodes = []; + + if (!self.args) { // If tagCtx with no content (so no contentView) self is tagCtx: return empty []; + prevNode = prevNode || self._prv; + nextNode = nextNode || self._nxt; + + node = prevIsFirstNode + ? (prevNode === self._nxt + ? self.parentElem.lastSibling + : prevNode) + : (self.inline === false + ? prevNode || self.linkCtx.elem.firstChild + : prevNode && prevNode.nextSibling); + + while (node && (!nextNode || node !== nextNode)) { + if (withMarkers || elCnt || node.tagName !== SCRIPT) { + // All the top-level nodes in the view + // (except script marker nodes, unless withMarkers = true) + // (Note: If a script marker node, viewInfo.elCnt undefined) + nodes.push(node); + } + node = node.nextSibling; + } + } + return nodes; + }; + + tagOrView.childTags = function(deep, tagName) { + // For a view, a tagor a tagCtx, return child tags - at any depth, or as immediate children only. + if (deep !== !!deep) { + // deep not boolean, so this is childTags(tagName) - which looks for top-level tags of given tagName + tagName = deep; + deep = undefined; + } + + var self = this.contentView || this, // If tagCtx, use tagCtx.contentView + view = self.link ? self : self.tagCtx.view, // This may be a view or a tag. If a tag, get the view from tag.tagCtx.view + prevNode = self._prv, + elCnt = self._elCnt, + tags = []; + + if (!self.args) { // If tagCtx with no content (so no contentView) self is tagCtx: return empty []; + view.link( + undefined, + self.parentElem, + elCnt ? prevNode && prevNode.previousSibling : prevNode, + self._nxt, + undefined, + {get:{ + tags: tags, + deep: deep, + name: tagName, + id: self.link ? self._.id + "_" : self._tgId + "^" + }} + ); + } + return tags; + }; + + if (tagOrView._is === "tag") { + //======================= + // This is a TAG instance + //======================= + + theTag = tagOrView; + + m = theTag.tagCtxs.length; + while (m--) { + tagCtx = theTag.tagCtxs[m]; + + tagCtx.setValues = setValues; + tagCtx.cvtArgs = $sub._tg.prototype.cvtArgs; + tagCtx.bndArgs = $sub._tg.prototype.bndArgs; + tagCtx.contents = tagOrView.contents; + tagCtx.childTags = tagOrView.childTags; + tagCtx.nodes = tagOrView.nodes; + } + + boundProps = theTag.boundProps = theTag.boundProps || []; + if (bindTo = theTag.linkTo ? ["linkTo"] : theTag.bindTo) { + l = bindTo.length; + while (l--) { + key = bindTo[l]; + if (key + "" === key) { + bindTo[key] = 1; + if ($.inArray(key, boundProps) < 0) { + boundProps.push(key); // Add any 'bindTo' props to boundProps array. (So two-way binding works without writing ^foo=expression) + } + } + } + } + + theTag.setValue = $sub._gm(theTag.constructor.prototype.setValue, function(val, index, tagElse) { + if (!arguments.length) { // tag.setValue() calls setValue(val, index) for each parameter in bindTo array + theTag.setValues(); // = theTag.bndArgs() + return theTag; + } + var linkedElem, linkedEl, linkedTag, + linkedCtxParam = theTag.linkedCtxParam, + tagCtx = theTag.tagCtxs[tagElse || 0], + props = tagCtx.props, + linkCtx = theTag.linkCtx, + linkedElems = tagCtx.linkedElems || theTag.linkedElem && [theTag.linkedElem]; + + if (val !== undefined) { + theTag.base.call(theTag , val, index, tagElse); + } else if (theTag.getValue && (val = theTag.getValue(tagElse)) && val !== undefined) { + // If bound args are not initialized, and getValue is defined, use getValue to initialize + if (theTag.bindTo.length > 1) { + val = val[index]; // getVal returns value if tag.bndArgs() (and bindTo) length is 1, or array of values if bindTo.length > 1 + } + if (linkedCtxParam && linkedCtxParam[index]) { + // Values of tag contextual param were already intialized (during rendering) so need to observably update to values from tag.getValue() + $.observable(tagCtx.ctx[linkedCtxParam[index]][0]).setProperty(_ocp, val); + } + } + if ((linkedElem = linkedElems && linkedElems[index]) && linkedElem[0]) { + l = linkedElem.length; + while (l--) { + linkedEl = linkedElem[l]; + if (theTag._.unlinked) { + linkedTag = linkedEl._jsvLkEl; + if (!linkedTag || linkedTag !== theTag) { + if (linkedTag) { + val = linkedTag.cvtArgs(true, tagElse)[index]; // Need to use converter of linked tag + } + // For data-linked tags, identify the linkedEl with the tag, for "to" binding + // (For data-linked elements, if not yet bound, we identify later when the linkCtx.elem is bound) + linkedEl._jsvLkEl = theTag; + linkedEl._jsvInd = index; + linkedEl._jsvElse = tagElse; + bindLinkedElChange(theTag, linkedEl); + linkedEl._jsvBnd = "&" + theTag._tgId + "+"; // Add a "+" for cloned binding - so removing + // elems with cloned bindings will not remove the 'parent' binding from the bindingStore. + } + } + if (val !== undefined && !linkedEl._jsvChg && linkCtx._val !== val) { + if (linkedEl.value !== undefined) { + if (linkedEl.type === CHECKBOX) { + linkedEl[CHECKED] = val && val !== "false"; + } else if (linkedEl.type === RADIO) { + linkedEl[CHECKED] = (linkedEl.value === val); + } else if ($isArray(val)) { + linkedEl.value = val; // Don't use jQuery since it replaces array by mapped clone + } else { + $(linkedEl).val(val); // Use jQuery for attrHooks - can't just set value (on select, for example) + } + } else if (linkedEl.contentEditable === TRUE) { + linkedEl.innerHTML = val; + } + } + if (props.name) { + linkedEl.name = linkedEl.name || props.name; + } + } + } + return theTag; + }); + + theTag.updateValue = updateValue; + + theTag.updateValues = function() { + return updateValues(arguments, undefined, this); + }; + + theTag.setValues = function() { + var m = arguments.length ? 1 : theTag.tagCtxs.length; + while (m--) { + setValues.apply(theTag.tagCtxs[m], arguments); + } + }; + + theTag.refresh = function(sourceValue) { + var attr, + linkCtx = theTag.linkCtx, + view = theTag.tagCtx.view; + + if (theTag.onUnbind) { + theTag.onUnbind(theTag.tagCtx, linkCtx, theTag.ctx); + } + attr = theTag.inline ? HTML : (linkCtx.attr || defaultAttr(theTag.parentElem, true)); + sourceValue = $sub._tag(theTag, view, view.tmpl, mergeCtxs(theTag), true); // Get rendered HTML for tag, based on refreshed tagCtxs + updateContent(sourceValue, linkCtx, attr, theTag); + callAfterLink(theTag); + return theTag; + }; + + theTag.domChange = function() { // domChange notification support + var elem = this.parentElem, + hasListener = $.hasData(elem) && $._data(elem).events, + domChangeNotification = "jsv-domchange"; + + if (hasListener && hasListener[domChangeNotification]) { + // Only trigger handler if there is a handler listening for this event. (Note using triggerHandler - so no event bubbling.) + $(elem).triggerHandler(domChangeNotification, arguments); + } + }; + + //==================================== + // End of added link methods for TAG + //==================================== + } else { + //========================= + // This is a VIEW prototype + //========================= + + theView = tagOrView; + + // Note: a linked view will also, after linking have nodes[], _prv (prevNode), _nxt (nextNode) ... + theView.addViews = function(index, dataItems) { + // if view is not an array view, do nothing + var i, viewsCount, + view = this, + itemsCount = dataItems.length, + views = view.views; + + if (!view._.useKey && itemsCount) { + // view is of type "array" + viewsCount = views.length + itemsCount; + + if (viewsCount === view.data.length // If views not already synced to array (e.g. triggered by array.length propertyChange - jsviews/issues/301) + && renderAndLink(view, index, view.tmpl, views, dataItems, view.ctx) !== false) { + if (!view._.srt) { // Not part of a 'sort' on refresh + view.fixIndex(index + itemsCount); + } + } + } + }; + + theView.removeViews = function(index, itemsCount, keepNodes, isMove) { + // view.removeViews() removes all the child views + // view.removeViews(index) removes the child view with specified index or key + // view.removeViews(index, count) removes the specified nummber of child views, starting with the specified index + function removeView(index) { + var id, bindId, parentElem, prevNode, nextNode, nodesToRemove, + viewToRemove = views[index]; + + if (viewToRemove && viewToRemove.link) { + id = viewToRemove._.id; + if (!keepNodes) { + // Remove the HTML nodes from the DOM, unless they have already been removed, including nodes of child views + nodesToRemove = viewToRemove.nodes(); + } + + // Remove child views, without removing nodes + viewToRemove.removeViews(undefined, undefined, true); + + viewToRemove.type = undefined; // Set type to undefined: used as a flag that this view is being removed + prevNode = viewToRemove._prv; + nextNode = viewToRemove._nxt; + parentElem = viewToRemove.parentElem; + // If prevNode and nextNode are the same, the view is empty + if (!keepNodes) { + // Remove the HTML nodes from the DOM, unless they have already been removed, including nodes of child views + if (viewToRemove._elCnt) { + // if keepNodes is false (and transferring of tokens has not already been done at a higher level) + // then transfer tokens from prevNode which is being removed, to nextNode. + transferViewTokens(prevNode, nextNode, parentElem, id, "_"); + } + $(nodesToRemove).remove(); + } + if (!viewToRemove._elCnt) { + try { + prevNode.parentNode.removeChild(prevNode); // (prevNode.parentNode is parentElem, except if jQuery Mobile or similar has inserted an intermediate wrapper + nextNode.parentNode.removeChild(nextNode); + } catch (e) {} + } + setArrayChangeLink(viewToRemove); + for (bindId in viewToRemove._.bnds) { + removeViewBinding(bindId); + } + delete viewStore[id]; + } + } + + var current, childView, viewsCount, + view = this, + isArray = !view._.useKey, + views = view.views; + + if (isArray) { + viewsCount = views.length; + } + if (index === undefined) { + // Remove all child views + if (isArray) { + // views and data are arrays + current = viewsCount; + while (current--) { + removeView(current); + } + view.views = []; + } else { + // views and data are objects + for (childView in views) { + // Remove by key + removeView(childView); + } + view.views = {}; + } + } else { + if (itemsCount === undefined) { + if (isArray) { + // The parentView is data array view. + // Set itemsCount to 1, to remove this item + itemsCount = 1; + } else { + // Remove child view with key 'index' + removeView(index); + delete views[index]; + } + } + if (isArray && itemsCount + && (isMove || viewsCount - itemsCount === view.data.length)) { // If views not already synced to array (e.g. triggered by array.length propertyChange - jsviews/issues/301) + current = index + itemsCount; + // Remove indexed items (parentView is data array view); + while (current-- > index) { + removeView(current); + } + views.splice(index, itemsCount); + if (!view._.srt) { + view.fixIndex(index); + } + } + } + }; + + theView.moveViews = function(oldIndex, index, itemsCount) { + function parts(itemView, str) { + return RegExp("^(.*)(" + (str ? "\\/" : "#") + itemView._.id + "_.*)$").exec(str || itemView._prv.getAttribute(jsvAttrStr)); + } + function setPrv(itemView, tokens) { + itemView._prv.setAttribute(jsvAttrStr, tokens); + } + var nodes, childView, nxtView, insertBefore, viewId, + view = this, + selfNxt = view._nxt, + views = view.views, + backwards = index < oldIndex, + firstChange = backwards ? index : oldIndex, + lastChange = backwards ? oldIndex : index, + i = index, + movedNodes = [], + + viewsToMove = views.splice(oldIndex, itemsCount); // remove + + if (index > views.length) { + index = views.length; + } + views.splice.apply(views, [index, 0].concat(viewsToMove)); //re-insert + + itemsCount = viewsToMove.length; + insertBefore = index + itemsCount; + lastChange += itemsCount; + + for (i; i < insertBefore; i++) { + childView = views[i]; + nodes = childView.nodes(true); + movedNodes = view._elCnt ? movedNodes.concat(nodes) : movedNodes.concat(childView._prv, nodes, childView._nxt); + } + movedNodes = $(movedNodes); + + if (insertBefore < views.length) { + movedNodes.insertBefore(views[insertBefore]._prv); + } else if (selfNxt) { + movedNodes.insertBefore(selfNxt); + } else { + movedNodes.appendTo(view.parentElem); + } + + if (view._elCnt) { + var afterParts, + endChange = backwards ? firstChange + itemsCount : lastChange - itemsCount, + beforeView = views[firstChange-1], + startView = views[firstChange], + endView = views[endChange], + afterView = views[lastChange], + startParts = parts(startView), + endParts = parts(endView); + + setPrv(startView, endParts[1] + startParts[2]); + if (afterView) { + afterParts = parts(afterView); + setPrv(afterView, startParts[1] + afterParts[2]); + } else { + if (selfNxt) { + afterParts = parts(view, selfNxt.getAttribute(jsvAttrStr)); + selfNxt.setAttribute(jsvAttrStr, startParts[1] + afterParts[2]); + } else { + afterParts = parts(view, view.parentElem._df); + setDefer(view.parentElem, startParts[1] + afterParts[2]); + } + } + setPrv(endView, afterParts[1] + endParts[2]); + if (beforeView) { + beforeView._nxt = startView._prv; + } else { + view._prv = startView._prv; + } + views[endChange-1]._nxt = endView._prv; + views[lastChange-1]._nxt = afterView ? afterView._prv : selfNxt; + } + view.fixIndex(firstChange); + }; + + theView.refresh = function() { + var view = this, + parent = view.parent; + + if (parent) { + renderAndLink(view, view.index, view.tmpl, parent.views, view.data, undefined, true); + setArrayChangeLink(view); + } + }; + + theView.fixIndex = function(fromIndex) { + // Fixup index on following view items... + var views = this.views, + index = views.length; + while (fromIndex < index--) { + if (views[index].index !== index) { + $observable(views[index]).setProperty("index", index); + // This is fixing up index, but not key, and not index on child views. From child views, use view.getIndex() + } + } + }; + + theView.link = viewLink; + + //==================================== + // End of added link methods for VIEW + //==================================== + } +} + +//======================== +// JsViews-specific converters +//======================== + +$converters.merge = function(val) { + // Special converter used in data-linking to space-separated lists, such as className: + // Currently only supports toggle semantics - and has no effect if toggle string is not specified + // data-link="class{merge:boolExpr toggle=className}" + var regularExpression, + currentValue = this.linkCtx.elem.className, + toggle = this.tagCtx.props.toggle; + + if (toggle) { + // We are toggling the class specified by the toggle property, + // and the boolean val binding is driving the insert/remove toggle + + regularExpression = toggle.replace(/[\\^$.|?*+()[{]/g, "\\$&"); + // Escape any regular expression special characters (metacharacters) within the toggle string + regularExpression = "(\\s(?=" + regularExpression + "$)|(\\s)|^)(" + regularExpression + "(\\s|$))"; + // Example: /(\s(?=myclass$)|(\s)|^)?(myclass(\s|$))/ - so matches (" myclass" or " " or ^ ) followed by ("myclass " or "myclass$") where ^/$ are beginning/end of string + currentValue = currentValue.replace(new RegExp(regularExpression), "$2"); + val = currentValue + (val ? (currentValue && " ") + toggle : ""); + } + return val; +}; + +//======================== +// JsViews-specific tags +//======================== + +$tags({ + on: { + attr: NONE, + init: function(tagCtx) { + var content, + tag = this, + i = 0, + args = tagCtx.args, // [events,] [selector,] handler + l = args.length; + + for (; ii && i+1; // handler index + if (tag.inline) { + if (!$sub.rTmpl.exec(content = $.trim(tagCtx.tmpl.markup))) { + // Inline {^{on}} tag with no content (or external template content) or with content containing + // no HTML or JsRender tags: We will wrap the (text) content, or the operation name in a "; + } + tag.attr = HTML; + } + }, + onBind: function() { + if (this.template) { // {^{on/}} with no content has template rendering
", "
"], + tr: [2, "", "
"], + td: [3, "", "
"], + col: [2, "", "
"], + svg_ns: [1, "", ""], + // IE6-8 can't serialize link, script, style, or any html5 (NoScope) tags, + // unless wrapped in a div with non-breaking characters in front of it. + div: $.support.htmlSerialize ? [0, "", ""] : [1, "X
", "
"] + }, + _fe: { + input: { + from: inputAttrib, to: VALUE + }, + textarea: valueBinding, + select: valueBinding, + optgroup: { + to: "label" + } + } +}); + + return $; +}, window));