﻿/*! JsViews v1.0pre: http://github.com/BorisMoore/jsviews */
/*
* jQuery plugin providing interactive data-driven views, based on integration between jQuery Templates and jQuery Data Link.
* Requires jquery.render.js (optimized version of jQuery Templates, for rendering to string)
*    See JsRender at http://github.com/BorisMoore/jsrender
* and jquery.observable.js also at http://github.com/BorisMoore/jsviews
*
* Copyright 2011, Boris Moore
* Released under the MIT License.
*/
(function ($, undefined) {

    //===============
    // event handlers
    //===============

    // var TEST_EVENTS = { total:0, change:0, arrayChange:0, propertyChange:0 };
    var FALSE = false, TRUE = true,
	topView, viewsNs, decl,
	oldCleanData = $.cleanData,
	fnSetters = {
	    value: "val",
	    html: "html",
	    text: "text"
	},
	jsvData = "_jsvData";

    function elemChangeHandler(ev) {
        var setter, cancel, fromAttr, toPath, linkToInfo, linkContext, sourceValue, link, cnvt, target,
		data = this.target,
		source = ev.target,
		$source = $(source),
		view = $.view(source),
		links = this.links,
		context = view.ctx,
		beforeChange = context.beforeChange,
		l = links.length;

        while (l-- && !cancel) {
            link = links[l];
            if (!link.filter || (link.filter && $source.is(link.filter))) {
                fromAttr = link.fromAttr;
                if (!fromAttr) {
                    // Merge in the default attribute bindings for this source element
                    fromAttr = viewsNs.merge[source.nodeName.toLowerCase()];
                    fromAttr = fromAttr ? fromAttr.from.fromAttr : "text";
                }
                setter = fnSetters[fromAttr];
                sourceValue = $.isFunction(fromAttr) ? fromAttr(source) : setter ? $source[setter]() : $source.attr(fromAttr);
                if ((!beforeChange || !(cancel = beforeChange.call(view, ev) === FALSE)) && sourceValue !== undefined) {
                    // Find linkToInfo using link.to, or if not specified, use declarative specification
                    // provided by decl.applyLinkToInfo, applied to source element
                    linkToInfo = link.to || $.trim(source.getAttribute(viewsNs.linkToAttr));
                    // linkTo does not support multiple targets - use imperative linking for that scenario...
                    if (linkToInfo) {
                        cnvt = link.convert;
                        linkToInfo = splitParams(linkToInfo);
                        target = linkToInfo[0].slice(0, -1);
                        data = (view && view.data) || data;
                        // data is the current view data, or the top-level target of linkTo.
                        // get the target object
                        target = getTargetObject(data, view, target);
                        cnvt = linkToInfo[3];
                        toPath = $.trim(linkToInfo[1].slice(0, -1));
                        linkContext = {
                            source: source,
                            target: target,
                            convert: cnvt,
                            path: toPath
                        };
                        if (cnvt) {
                            sourceValue = getConvertedValue(linkContext, data, view, cnvt, sourceValue);
                        }
                        if (sourceValue !== undefined && target) {
                            $.observable(target).setProperty(toPath, sourceValue);
                            if (context.afterChange) {  //TODO only call this if the target property changed
                                context.afterChange.call(linkContext, ev);
                            }
                        }
                        ev.stopPropagation(); // Stop bubbling
                    }
                }
                if (cancel) {
                    ev.stopImmediatePropagation();
                }
            }
        }
    }

    function propertyChangeHandler(ev, eventArgs) {
        var pathInfo, setter, cancel, changed, attr, sourceValue, css,
		link = this,
		target = link.target,
		$target = $(target),
		source = ev.target,
		sourcePath = (eventArgs && eventArgs.path) || ev.path,
		view = $.view(target),
		context = view.ctx,
		beforeChange = context.beforeChange,
		pathInfos = getLinkFromDataInfo(target, source).paths[sourcePath],
		l = pathInfos && pathInfos.length;

        while (l--) {
            pathInfo = pathInfos[l];
            if ((!beforeChange || !(eventArgs && (cancel = beforeChange.call(this, ev, eventArgs) === FALSE)))
		&& (!view || view.onDataChanged(eventArgs) !== FALSE)) {

                attr = pathInfo.attr;

                sourceValue = getConvertedValue(link, link.source, view, pathInfo.expr, eventArgs && eventArgs.value);

                while ($.isFunction(sourceValue)) {
                    sourceValue = sourceValue.call(source);
                }

                if (!attr) {
                    // Merge in the default attribute bindings for this target element
                    attr = viewsNs.merge[target.nodeName.toLowerCase()];
                    attr = attr ? attr.to.toAttr : "text";
                }

                if (css = attr.indexOf("css-") === 0 && attr.substr(4)) {
                    if (changed = $target.css(css) !== sourceValue) {
                        $target.css(css, sourceValue);
                    }
                } else {
                    setter = fnSetters[attr];
                    if (setter) {
                        if (changed = $target[setter]() !== sourceValue) {
                            $target[setter](sourceValue);
                        }
                    } else if (changed = $target.attr(attr) !== sourceValue) {
                        $target.attr(attr, sourceValue);
                    }
                }

                if (eventArgs && changed && context.afterChange) {
                    context.afterChange.call(link, ev, eventArgs);
                }
            }
        }
    }

    function arrayChangeHandler(ev, eventArgs) {
        var cancel,
		context = this.ctx,
		beforeChange = context.beforeChange,
		sourceValue = eventArgs ? eventArgs.change : viewsNs.linkToAttr;  // linkToAttr used as a marker of trigger events

        if ((!beforeChange || !(cancel = beforeChange.call(this, ev, eventArgs) === FALSE))
		&& sourceValue !== undefined) {
            this.onDataChanged(eventArgs);
            if (context.afterChange) {
                context.afterChange.call(this, ev, eventArgs);
            }
        }
    }

    function setArrayChangeLink(view) {
        var handler,
		data = view.data,
		onArrayChange = view._onArrayChange;

        if (onArrayChange) {
            if (onArrayChange[1] === data) {
                return;
            }
            $([onArrayChange[1]]).unbind("arrayChange", onArrayChange[0]);
            //	$( "#console" ).append( --TEST_EVENTS.total + " - arrayChange " + --(TEST_EVENTS.arrayChange) + "<br/>");
        }

        if ($.isArray(data)) {
            handler = function () {
                arrayChangeHandler.apply(view, arguments);
            };
            $([data]).bind("arrayChange", handler);
            view._onArrayChange = [handler, data];
            //	$( "#console" ).append( ++TEST_EVENTS.total + " + arrayChange " + ++(TEST_EVENTS.arrayChange) + "<br/>");
        }
    }

    //===============
    // view hierarchy
    //===============

    function setViewContext(view, context, merge) {
        var parentContext = view.parent && view.parent.ctx,
		viewContext = view._ctx;
        // Propagate inherited context through children
        view.ctx = merge
		? (viewContext && parentContext ? $.extend({}, parentContext, viewContext) : parentContext || viewContext)
		: context && context !== parentContext
        // Set additional context on this view (which will modify the context inherited from the parent, and be inherited by child views)
			? (view._ctx = context, (parentContext ? $.extend({}, parentContext, context) : context))
			: parentContext;
    }

    function View(context, node, path, template, parentView, parentElViews, data) {
        var views, index, viewCount, tagInfo, ctx, presenter, options,
		self = this;

        $.extend(self, {
            views: [],
            nodes: node ? [] : [document.body],
            tmpl: template || (parentView && parentView.tmpl),
            path: path,
            parent: parentView,
            prevNode: node
        });
        if (parentView) {
            views = parentView.views;
            parentElViews.push(self);
            data = data || parentView.data;
            if ($.isArray(parentView.data)) {
                self.index = index = path;
                views.splice(index, 0, self);
                viewCount = views.length;
                data = data[index];
                while (index++ < viewCount - 1) {
                    $.observable(views[index]).setProperty("index", index);
                }
                self.tag = parentView.tag;
            } else {
                if (path) {
                    // TODO getDataAndContext and passing of context and data from tags needs work. 
                    // Also, consider more 'codeless' approach, and more consistent syntax with codeless tag markup
                    data = getDataAndContext(data, parentView, path);
                    context = context || data[1];
                    data = data[0];
                }
                self.index = views.length;
                views.push(self);
                if (context.tag) {
                    self.tag = [context.tag];
                } else {
                    template = template.split("=");
                    if (template.shift() === "tag") {
                        self.tag = template;
                    }
                }
            }
        }
        self.data = data;
        setViewContext(self, context);
        setArrayChangeLink(self);

        if ((tagInfo = self.tag) && !$.isArray(data)) {
            // This view is from a registered presenter
            presenter = viewsNs.tags[tagInfo[0]];
            if (presenter && presenter.presenter) {
                ctx = presenter.ctx || {};
                options = tagInfo[1]
				? getTargetObject(parentView.data, parentView, "{" + tagInfo[1] + "}")
                // Declarative presenter {{foo}}
				: parentView.ctx;
                // Imperative: link( data, foo )
                $.extend(options, presenter.options || {});
                // attach plugin to content of view

                ctx[tagInfo[0]] = new presenter.presenter(options, self);
                // Make presenter available off the ctx object
                self.context(ctx);
            }
        }
    }

    function createNestedViews(node, parent, nextNode, depth, data, context, prevNode, index) {
        var tokens, parentElViews, view, existing, parentNode, elem, elem2, params, presenter, tagInfo, ctx,
		currentView = parent,
		viewDepth = depth;
        context = context || {};
        index = index || 0;
        node = prevNode || node;

        if (!prevNode && node.nodeType === 1) {
            if (viewDepth++ === 0) {
                // Add top-level element nodes to view.nodes
                currentView.nodes.push(node);
            }
            var linkFromInfo = node.getAttribute(viewsNs.linkFromAttr),
			getFromInfo = node.getAttribute(viewsNs.getFromAttr);

            if (linkFromInfo || getFromInfo) {
                addLinksFromData(data || parent.data, node, getFromInfo, linkFromInfo, TRUE);
            }
            node = node.firstChild;
        } else {
            node = node.nextSibling;
        }

        while (node && node !== nextNode) {
            if (node.nodeType === 1) {
                createNestedViews(node, currentView, nextNode, viewDepth, data, context);
            } else if (node.nodeType === 8 && (tokens = /^(\/?)(?:(item)|(?:(tmpl)(?:\((.*)\))?(?:\s+([^\s]+))?))$/.exec(node.nodeValue))) {
                // tokens: [ commentText, "/", "item", dataParams, tmplParam ]

                parentNode = node.parentNode;
                if (tokens[1]) {
                    // <!--/item--> or <!--/tmpl-->
                    currentView.nextNode = node;
                    if ((tagInfo = currentView.tag) && (ctx = currentView.ctx[tagInfo[0]]) && ctx.onAfterCreate) {
                        // This view is from a registered presenter which has registered an onAfterCreate callback
                        ctx.onAfterCreate();
                    }
                    if (currentView.ctx.onAfterCreate) {
                        currentView.ctx.onAfterCreate.call(currentView, currentView);
                    }
                    if (tokens[2]) {
                        // An item close tag: <!--/item-->
                        currentView = parent;
                    } else {
                        // A tmpl close tag: <!--/tmpl-->
                        return node;
                    }
                } else {
                    // <!--item--> or <!--tmpl-->
                    parentElViews = parentElViews || jsViewsData(parentNode, "view", TRUE);
                    if (tokens[2]) {
                        // An item open tag: <!--item-->
                        currentView = new View(context, node, index++, undefined, currentView, parentElViews);
                    } else {
                        // A tmpl open tag: <!--tmpl(path) name-->
                        view = $.view(node);
                        if (view && view.prevNode === node) {
                            if (view.data === data) {
                                existing = view.nextNode;
                            } else {
                                view.data = data;
                                view.render(context);
                                return view.nextNode;
                            }
                        } else {
                            view = new View(context, node, tokens[4], tokens[5], currentView, parentElViews, data);
                        }
                        // Jump to the nextNode of the tmpl view
                        node = existing || createNestedViews(node, view, nextNode, 0);
                    }
                }
            } else if (viewDepth === 0) {
                // Add top-level non-element nodes to view.nodes
                currentView.nodes.push(node);
            }
            node = node.nextSibling;
        }
    }

    //=======================
    // Expression evaluation
    //=======================

    function getDataAndContext(source, view, paramString) {
        return Function("$", "$data", "$view", "$ctx",
		"with($data){return [" + paramString + "];}")($, source, view, view.ctx);
    }

    function getConvertedValue(context, source, view, expression, value) {
        try {
            return Function("$", "$data", "$view", "$ctx", "$value",
			"with($data){return " + expression + ";}")
			.call(context, $, source, view, view.ctx, value);
        } catch (e) {
            // in debug mode, throw 'bad syntax error';
            throw e.message;
        }
    }

    function getTargetObject(source, view, expression) {
        try {
            return expression
			? Function("$", "$data", "$view", "$ctx",
				"with($data){return " + expression + ";}")($, source, view, view.ctx)
			: source;
        } catch (e) {
            // in debug mode, throw 'bad syntax error';
            throw e.message;
        }
    }

    //===============
    // data linking
    //===============

    function link(from, to, links, context) {
        var lnk, filter, targetElems, toLinks, fromLinks, linksToData, i;

        if (links) {

            links = $.isArray(links) ? links : [links];

            toLinks = [];
            fromLinks = [];
            i = links.length;

            while (i--) {
                lnk = links[i];
                if (lnk.to) {
                    toLinks.push({ to: lnk.to, filter: lnk.filter });
                }
                if (lnk.from || lnk.getFrom) {
                    fromLinks.push(lnk);
                }
            }
            i = fromLinks.length;
            while (i--) {
                lnk = fromLinks[i];
                filter = lnk.filter;
                targetElems = filter ? from.find(filter).add($(this).filter(filter)) : from; // Use future findFilter method in jQuery 1.7?

                targetElems.each(function () {
                    // If 'from' path points to a property of a descendant 'leaf object',
                    // link not only from leaf object, but also from intermediate objects
                    addLinksFromData(to, this, lnk.getFrom, lnk.from);
                });
            }
        }
        if (!links || toLinks.length) {
            from.each(function () {
                if (!links) {
                    // DECLARATIVE DATA LINKING

                    // Linking HTML to object or array
                    linksToData = jsViewsData(this, "to");
                    i = linksToData.length;
                    while (i--) {
                        lnk = linksToData[i];
                        if (lnk.links === declLinkTo) {
                            if (lnk.target === to) {
                                // Already declaratively linked to the same object
                                return;
                            }
                            // Already linked to a different object, so unlink from previous object
                            removeLinksToData(this, declLinkTo);
                            linksToData.splice(i, 1);
                        }
                    }
                    toLinks = declLinkTo; // For declarative case

                    // Linking object or array to HTML
                    createNestedViews(this, $.view(this), undefined, undefined, to, context);
                }
                addLinksToData(this, to, toLinks);
            });
        }
        return from;
    }

    //function unlink( from, to, links ) { // TODO
    //}

    function addLinksFromData(source, target, getFrom, linkFrom) {
        var param, cnvtParam, i, l, lastChar, attr, openParenIndex, get, object, cnvtParams, view,
		triggers = [],
		cnvt = "";

        linkFrom = splitParams((linkFrom ? linkFrom + "," : "") + (getFrom ? "|," + getFrom + "," : ""), TRUE);
        while (param = linkFrom.shift()) {
            l = param.length;
            lastChar = param.charAt(l - 1);
            param = param.slice(0, -1);
            switch (lastChar) {
                case ':':
                    attr = $.trim(param);
                    break;
                case '[':
                    cnvt = $.trim(param);
                    break;
                case ')':
                    cnvt = cnvt || param;
                    break;
                case ']':
                    triggers = [[cnvt, param]];
                    cnvt = cnvt ? (cnvt + "." + param) : param;
                    cnvt += $.isFunction(source[param]) ? "()" : "";
                    break;
                case '\r':
                    openParenIndex = ++param; // Convert to integer and increment
                    break;
                case ',':
                    if (param === '|') {
                        get = TRUE;
                    } else {
                        // Apply binding
                        if (openParenIndex) {
                            cnvtParams = cnvt.slice(openParenIndex);
                            cnvtParams = splitParams(cnvtParams);
                            for (i = 0, l = cnvtParams.length; i < l; i++) {
                                cnvtParam = $.trim(cnvtParams[i]);
                                lastChar = cnvtParam.charAt(cnvtParam.length - 1);
                                cnvtParam = cnvtParam.slice(0, -1);
                                if (lastChar === '[') {
                                    cnvtParams[i] = object = cnvtParam;
                                } else if (lastChar === ']') {
                                    triggers.push([object, cnvtParam]);
                                    cnvtParams[i] = object ? ("." + cnvtParam) : cnvtParam;
                                    cnvtParams[i] += $.isFunction(source[cnvtParam]) ? "()" : "";
                                    object = "";
                                }
                            }
                            cnvt = cnvt.slice(0, openParenIndex) + cnvtParams.join("") + ")";
                        }

                        cnvt += param;
                        view = $.view(target);
                        l = triggers.length;
                        while (l--) {
                            var trigger = triggers[l],
						path = trigger[1],
						fromOb = getTargetObject(source, view, trigger[0]),
						link = { source: source, target: target },
						innerPath = path.split("."),
						innerOb = fromOb;

                            // If 'from' path points to a property of a descendant 'leaf object',
                            // link not only from leaf object, but also from intermediate objects
                            while (innerPath.length > 1) {
                                innerOb = innerOb[innerPath.shift()];
                                if (innerOb) {
                                    addLinkFromData(innerOb, link, innerPath.join("."), cnvt, attr);
                                }
                            }
                            // The last trigger of get bindings will be called on adding the link (to get/initialize the value)
                            addLinkFromData(fromOb, link, path, cnvt, attr, !l && get);
                        }
                        openParenIndex = 0;
                        triggers = [];
                        attr = cnvt = "";
                    }
            }
        }
    }

    function addLinkFromData(source, link, path, expr, attr, get) {
        var paths, pathInfos, handler,
		target = link.target,
		linkInfo = getLinkFromDataInfo(target, source),
		pathInfo = { attr: attr, expr: expr };

        if (linkInfo) {
            // Set path info for this path
            pathInfos = linkInfo.paths[path] = linkInfo.paths[path] || [];
            pathInfos.push(pathInfo);
            // get handler
            handler = linkInfo.handler;
        } else {
            handler = function () {
                propertyChangeHandler.apply(link, arguments);
            };

            // Store handler for unlinking
            if (target) {
                paths = {};
                paths[path] = [pathInfo];
                jsViewsData(target, "from", TRUE).push({ source: source, paths: paths, handler: handler });
            }
            $(source).bind("propertyChange", handler);
        }
        //	$( "#console" ).append( ++TEST_EVENTS.total + " + propertyChange " + ++(TEST_EVENTS.propertyChange) + "<br/>");
        if (get) {
            handler({ target: source, path: path });
        }
    }

    function addLinksToData(source, target, links) {
        var handler = function () {
            elemChangeHandler.apply({ target: target, links: links }, arguments);
        };

        // Store handler for unlinking
        jsViewsData(source, "to", TRUE).push({ target: target, links: links, handler: handler });
        $(source).bind("change", handler);
    }

    function removeLinksToData(source, links) {
        var prevLinkInfo,
		prevLinkInfos = jsViewsData(source, "to"),
		l = prevLinkInfos.length;
        while (l--) {
            prevLinkInfo = prevLinkInfos[l];
            if (prevLinkInfo.links === links) {
                $(source).unbind("change", prevLinkInfo.handler);
                prevLinkInfos.splice(l, 1);
                //	$( "#console" ).append( --TEST_EVENTS.total + " - change " + --(TEST_EVENTS.change) + "<br/>");
            }
        }
    }

    function getLinkFromDataInfo(target, source) {
        var link,
		links = jsViewsData(target, "from"),
		l = links.length;
        while (l--) {
            link = links[l];
            if (link.source === source) {
                // Set path info for this path
                return link;
            }
        }
    }

    //===============
    // helpers
    //===============

    function clean(i, el) { // TODO optimize for perf
        var link, links, l, views, parentView, view;

        if (jsViewsData(el, "to").length) {
            $(el).unbind("change");
            //	$( "#console" ).append( --TEST_EVENTS.total + " - change " + --(TEST_EVENTS.change) + "<br/>");
        }

        links = jsViewsData(el, "from");
        l = links.length;

        while (l--) {
            link = links[l];
            $(link.source).unbind("propertyChange", link.handler);
            //	$( "#console" ).append( --TEST_EVENTS.total + " - propertyChange " + --(TEST_EVENTS.propertyChange) + "<br/>");
        }

        views = jsViewsData(el, "view");
        if (l = views.length) {
            parentView = $.view(el);
            while (l--) {
                view = views[l];
                if (view.parent === parentView) {
                    parentView.removeViews(view.index, 1);  // NO - ONLY remove view if its top-level nodes are all.. (TODO)
                }
            }
        }
    }

    function jsViewsData(el, type, create) {
        var jqData = $.data(el, jsvData) || (create && $.data(el, jsvData, { "view": [], "from": [], "to": [] }));
        return jqData ? jqData[type] : [];
    }

    function splitParams(paramString, markParen) {
        // Split into params (or values in an array literal, or keys and values in an object literal)
        // (Achieved by splitting before top-level ':' or ',' chars)
        var openParenIndex,
		startIndex = 0,
		parenDepth = 0,
		quoted = FALSE, // boolean for string content in double qoutes
		aposed = FALSE; // boolean for string content in single qoutes

        paramString = paramString.replace(/\s+/g, " ");

        return paramString
		.replace(/(\))|([\:\,])|(\')|(\")|([\(\[\{])|([\}\]])/g, function (all, cnvt, colon, apos, quot, leftParen, rightParen, index) {
		    if (aposed) {
		        // within single-quoted string
		        aposed = !apos;
		        return all;
		    }
		    if (quoted) {
		        // within double-quoted string
		        quoted = !quot;
		        return all;
		    }
		    if (cnvt) {
		        // follow top-level ':' or ',' with '\t'
		        return --parenDepth
					? all
					: (markParen && paramString.charAt(index + 1) === ',')
						? (openParenIndex -= startIndex, startIndex = index,
							all + "\t" + openParenIndex + "\r\t")
						: all;
		    }
		    if (colon) {
		        // follow top-level ':' or ',' with '\t'
		        return parenDepth
					? all
					: (startIndex = index + 1, all + "\t");
		    }
		    if (leftParen) {
		        if (parenDepth++) {
		            return all;
		        }
		        if (all === '(') {
		            openParenIndex = index;
		        }
		        // follow top-level '[' by '\t'
		        return all !== '[' ? all : "[\t";
		    }
		    if (rightParen) {
		        // follow top-level ']' by '\t'
		        return (--parenDepth || all !== ']') ? all : "]\t";
		    }
		    aposed = apos;
		    quoted = quot;
		    return all;
		})
		.split("\t");
    }

    function inputAttrib(elem) {
        return elem.type === "checkbox" ? elem.checked : $(elem).val();
    }

    function textareaAttrib(elem) {
        return $(elem).html();
    }

    function selectAttrib(elem) {
        return $(elem).val();
    }

    $.extend({

        //=======================
        // jQuery $.view() plugin
        //=======================

        view: function (node, inner) {
            // $.view() returns top node
            // $.view( node ) returns view that contains node
            var returnView, view, parentElViews, i, finish,
			startTagReg = /^item|^tmpl(\(\$?[\w\.]*\))?(\s+[^\s]+)?$/,
			topNode = document.body,
			startNode = node;

            if (inner) {
                // Treat supplied node as a container element, step through content, and return the first view encountered.
                finish = node.nextSibling || node.parentNode; //
                while (finish !== (node = node.firstChild || node.nextSibling || node.parentNode.nextSibling)) {
                    if (node.nodeType === 8 && startTagReg.test(node.nodeValue)) {
                        view = $.view(node);
                        if (view.prevNode === node) {
                            return view;
                        }
                    }
                }
                return;
            }

            node = node || topNode;
            if (topView && !topView.views.length) {
                returnView = topView; // Perf optimization for common case
            } else {
                // Step up through parents to find an element which is a views container, or if none found, create the top-level view for the page
                while (!(parentElViews = jsViewsData(finish = node.parentNode || topNode, "view")).length) {
                    if (!finish || node === topNode) {
                        jsViewsData(topNode.parentNode, "view", TRUE).push(returnView = topView = new View());
                        topView.ctx = {};
                        break;
                    }
                    node = finish;
                }
                if (!returnView && node === topNode) {
                    returnView = parentElViews[0];
                }
                while (!returnView && node) {
                    // Step back through the nodes, until we find an item or tmpl open tag - in which case that is the view we want
                    if (node.nodeType === 8) {
                        if (/^\/item|^\/tmpl$/.test(node.nodeValue)) {
                            // A tmpl or item close tag: <!--/tmpl--> or <!--/item-->
                            i = parentElViews.length;
                            while (i--) {
                                view = parentElViews[i];
                                if (view.nextNode === node) {
                                    // If this was the node originally passed in, this is the view we want.
                                    returnView = (node === startNode && view);
                                    // If not, jump to the beginning of this item/tmpl and continue from there
                                    node = view.prevNode;
                                    break;
                                }
                            }
                        } else if (startTagReg.test(node.nodeValue)) {
                            // A tmpl or item open tag: <!--tmpl--> or <!--item-->
                            i = parentElViews.length;
                            while (i--) {
                                view = parentElViews[i];
                                if (view.prevNode === node) {
                                    returnView = view;
                                    break;
                                }
                            }
                        }
                    }
                    node = node.previousSibling;
                }
                // If not within any of the views in the current parentElViews collection, move up through parent nodes to find next parentElViews collection
                returnView = returnView || $.view(finish);
            }
            return returnView;
        },

        //=======================
        // override cleanData
        //=======================

        cleanData: function (elems) {
            $(elems).each(clean);  // TODO - look at perf optimization on this
            oldCleanData.call($, elems);
        }
    });

    //=======================
    // $.views (namespace)
    //=======================

    viewsNs = $.views = $.views || {};

    $.extend(viewsNs, {
        presenters: {},
        activeViews: true,
        getProperty: function (data, value) {
            // support for property getter on data
            return $.isFunction(value) ? value.call(data) : value;
        },
        linkToAttr: "data-to",
        linkFromAttr: "data-from",
        getFromAttr: "data-getfrom",
        merge: {
            input: {
                from: {
                    fromAttr: inputAttrib
                },
                to: {
                    toAttr: "value"
                }
            },
            textarea: {
                from: {
                    fromAttr: textareaAttrib
                },
                to: {
                    toAttr: "html"
                }
            },
            select: {
                from: {
                    fromAttr: selectAttrib
                },
                to: {
                    toAttr: "value"
                }
            }
        },

        //===============
        // registerPresenters
        //===============

        // Register a 'control' - which associates a Presenter object with a template
        // Optionally associate with a tag, for declarative use. (Rendering of template and instantiation/attaching of presenter).
        registerPresenters: registerPresenters = function (name, presenter) {
            var key, tag;

            if (typeof name === "object") {
                for (key in name) {
                    registerPresenters(key, name[key]);
                }
            } else {
                // Simple single property case.
                presenter.ctx = presenter.ctx || {};

                tag = (tag = presenter.tag) === undefined ? name : tag;
                // If tag not set to null or empty string, then register a tag for this control 
                if (tag) {
                    presenter.tag = tag;
                    viewsNs.tags[tag] = presenter;
                }

                plugin = (plugin = presenter.plugin) === undefined ? name : plugin;
                // If plugin not set to null or empty string, then register create a generated jQuery plugin for this control 
                if (plugin) {
                    //Generated jQuery plugin
                    $.fn[plugin] = $.fn[plugin] || function (data, options) {
                        return this.link(data, presenter, options);
                    }
                    presenter.plugin = plugin;
                }
                viewsNs.presenters[name] = presenter;
            }
            return this;
        },

        //=======================
        // view prototype
        //=======================

        view: {
            onDataChanged: function (eventArgs) {
                if (eventArgs) {
                    // This is an observable action (not a trigger/handler call from pushValues, or similar, for which eventArgs will be null)
                    var self = this,
					action = eventArgs.change,
					index = eventArgs.index,
					items = eventArgs.items;
                    switch (action) {
                        case "insert":
                            self.addViews(index, items);
                            break;
                        case "remove":
                            self.removeViews(index, items.length);
                            break;
                        case "move":
                            self.render(); // Could optimize this
                            break;
                        case "refresh":
                            self.render();
                            // Othercases: (e.g.undefined, for setProperty on observable object) etc. do nothing
                    }
                }
                return TRUE;
            },
            render: function () {
                var prevNode = this.prevNode,
				nextNode = this.nextNode,
				parentNode = prevNode.parentNode;

                $(this.nodes).remove();
                this.removeViews(0, this.views.length);
                this.nodes = [];
                $(prevNode).after($.render(this.data, this.tmpl, this.ctx));
                parentNode.removeChild(prevNode.nextSibling);
                parentNode.removeChild(nextNode.previousSibling);
                createNestedViews(parentNode, this, nextNode, 0, undefined, undefined, prevNode, 0); //this.index
                setArrayChangeLink(this);
                return this;
            },
            addViews: function (index, dataItems, tmpl) {
                var parent,
				itemsCount = dataItems.length,
				context = this.ctx,
				views = this.views;

                if (index && !views[index - 1]) {
                    return; // If subview for provided index does not exist, do nothing
                }
                if (itemsCount) {
                    if (this.path) {
                        parent = this.parent;
                        context = getDataAndContext(parent.data, parent, this.path)[1];
                    }
                    var html = $.render(dataItems, tmpl || this.tmpl, context, this, TRUE),
                    // Use passed-in template if provided, since this added view may use a different template than the original one used to render the array.

					prevNode = index ? views[index - 1].nextNode : this.prevNode,
					nextNode = prevNode.nextSibling,
					parentNode = prevNode.parentNode;
                    $(prevNode).after(html);
                    parentNode.removeChild(prevNode.nextSibling);
                    parentNode.removeChild(nextNode.previousSibling);
                    createNestedViews(parentNode, this, nextNode, 0, undefined, undefined, prevNode, index);
                }
                return this;
            },
            removeViews: function (index, itemsCount, keepHtml) {
                if (itemsCount) {
                    var parentElViews, parentViewsIndex, viewCount,
					views = this.views,
					current = index + itemsCount;

                    while (current-- > index) {
                        var view = views[current],
						i = view.views.length,
						node = view.prevNode,
						nextNode = view.nextNode,
						nodes = [node];

                        if (i) {
                            view.removeViews(0, i, keepHtml);
                        }

                        // Remove this view from the parentElViews collection
                        parentElViews = parentElViews || jsViewsData(view.nextNode.parentNode, "view");
                        i = parentElViews.length;
                        while (i-- && parentViewsIndex === undefined) {
                            if (parentElViews[i] === view) {
                                parentViewsIndex = i;
                            }
                        }
                        parentElViews.splice(parentViewsIndex, 1);

                        if (!keepHtml) {
                            while (node !== nextNode) {
                                node = node.nextSibling;
                                nodes.push(node);
                            }
                            $(nodes).remove();
                        }
                        view.data = undefined;
                        setArrayChangeLink(view);
                    }
                    views.splice(index, itemsCount);
                    viewCount = views.length;

                    while (index < viewCount) {
                        $.observable(views[index]).setProperty("index", index++);
                    }
                }
                return this;
            },
            context: function (context) {
                var self = this,
				parent = self.parent,
				parentCtx = parent ? parent.ctx : {};
                if (!context) {
                    // Clear context
                    self.each(function (view) {
                        view.ctx = parentCtx;
                        view._ctx = undefined;
                    });
                } else if (context !== self.ctx) {
                    self.each(function (view) {
                        setViewContext(view, context, view !== self);
                    });
                }
                return this;
            },
            each: function (callback) {
                callback(this);
                var l = this.views.length;
                while (l--) {
                    this.views[l].each(callback);
                }
                return this;
            },
            content: function (select) {
                return select ? $(select, this.nodes) : $(this.nodes);
            }
        }
    });

    //=======================
    // jQuery plugins
    //=======================

    $.fn.extend({
        view: function (inner) {
            return $.view(this[0], inner);
        },
        addLinks: function (data, links, context) {
            // Explicit Linking
            return link(this, data, links, context);
        },
        //	removeLinks: function( data, links, context ) { //TODO
        //		return unlink( this, data, links, context );
        //	},
        link: function (data, tmpl, context) {
            // Declarative Linking
            // If context is a function, cb - shorthand for { beforeChange: cb }
            // if tmpl not a map, corresponds to $("#container").html( $.render( data, tmpl )).link( data );
            if (!this.length) {
                return this;
            }

            if (tmpl) {
                tmpl = tmpl.tag && tmpl === $.views.tags[tmpl.tag]
				? (context.tag = tmpl.tag, context.tmpl || tmpl.tmpl)
                // Special case: tmpl is a presenter
				: $.isPlainObject(tmpl)
					? (context = tmpl, FALSE)
                // Linking only. (context was passed in as second parameter, no template parameter passed)
					: tmpl;
                // Linking and rendering (passing a template)

                if (tmpl && (tmpl = $.template(tmpl))) {
                    removeLinksToData(this[0], declLinkTo);
                    this.empty();
                    if (data) {
                        this.append($.render(data, tmpl, context));
                        // Using append, rather than html, as workaround for issues in IE compat mode. (Using innerHTML leads to initial comments being stripped)
                    }
                }
            }
            return link(this, data, undefined, context);
        }
    });

    View.prototype = viewsNs.view;
    decl = viewsNs.decl;
    declLinkTo = [{ filter: "input[" + viewsNs.linkToAttr + "]" }, { filter: "textarea[" + viewsNs.linkToAttr + "]" }, { filter: "select[" + viewsNs.linkToAttr + "]"}];
})(jQuery);
