Click here to Skip to main content
15,886,199 members
Articles / Web Development / HTML5

Anatomy of HTML5/JavaScript Single Page Application in Samples (basics, navigation, composition, communications with the server)

Rate me:
Please Sign up or sign in to vote.
4.99/5 (44 votes)
20 Dec 2012CPOL37 min read 195K   5.1K   143  
Describes Single Page Applications and a new BPF Framework that helps to develop SPAs.
///#source 1 1 /Scripts/BPF/ArrayExtensions.js
// BPF JavaScript library version 0.9
// (c) Nick Polyak 2012 - http://awebpros.com/
// License: Code Project Open License (CPOL) 1.92(http://www.codeproject.com/info/cpol10.aspx)
//
// short overview of copyright rules:
// 1. you can use this framework in any commercial or non-commercial 
//    product as long as you retain this copyright message
// 2. Do not blame the author(s) of this software if something goes wrong. 
// 
// Also as a courtesy, please, mention this software in any documentation for the 
// products that use it.

var isObjectArray = function (obj) {
    if (Object.prototype.toString.call(obj) === '[object Array]') {
        return true;
    }

    return false;
}

// remove an element from an array
Array.prototype.remove = function (arrayElement) {
    var currentIndex = 0;
    do {
        if (this[currentIndex] === arrayElement) {
            this.splice(currentIndex, 1);
        }
        else {
            currentIndex++;
        }
    } while (currentIndex < this.length);
};

Array.prototype.addArray = function (array) {
    for (var idx = 0; idx < array.length; idx++) {
        this.push(array[idx]);
    }
};

// insert an element at specified index
Array.prototype.insert = function (idxToInsertAfter, arrayElement) {
    this.splice(idxToInsertAfter, 0, arrayElement);
};

// return index of the first occurance of an element
Array.prototype.firstIndexOf = function (arrayElement) {
    var currentIndex = 0;
    do {
        if (this[currentIndex] === arrayElement) {
            return currentIndex;
        }

        currentIndex++;
    } while (currentIndex < this.length);
};

// return index of the last occurance of an element
Array.prototype.lastIndexOf = function (arrayElement) {
    var currentIndex = this.length - 1;
    do {
        if (this[currentIndex] === arrayElement) {
            return currentIndex;
        }

        currentIndex--;
    } while (currentIndex >= 0);
};

// clear all elements from an array
Array.prototype.clear = function () {
    this.length = 0;
};

// copies a subset of an array to a new array
Array.prototype.copy = function (beginIdx, numberElements) {
    if (!beginIdx) {
        beginIdx = 0;
    }
        
    var endIdx;

    if (!numberElements) {
        endIdx = this.length;
    }
    else
    {
        endIdx = beginIdx + numberElements;

        if (endIdx > this.length)
        {
            endIdx = this.length;
        }
    }

    var copiedArray = new Array();

    for(var i = beginIdx; i < endIdx; i++)
    {
        copiedArray.push(this[i]);
    }

    return copiedArray;
};

///#source 1 1 /Scripts/BPF/CheckboxNavAdaptor.js
// BPF JavaScript library version 0.9
// (c) Nick Polyak 2012 - http://awebpros.com/
// License: Code Project Open License (CPOL) 1.92(http://www.codeproject.com/info/cpol10.aspx)
//
// short overview of copyright rules:
// 1. you can use this framework in any commercial or non-commercial 
//    product as long as you retain this copyright message
// 2. Do not blame the author(s) of this software if something goes wrong. 
// 
// Also as a courtesy, please, mention this software in any documentation for the 
// products that use it.

/// <reference path="../jquery-1.8.2.js" />
/// <reference path="ArrayExtensions.js" />
/// <reference path="SimpleEvent.js" />
/// <reference path="HashStrings.js" />

var bpf = bpf || {};

bpf.nav = bpf.nav || {};

bpf.nav.CheckboxNavAdaptor = function (checkBoxDOM, name) {
    var self = this;
    var _checkBoxDOM = checkBoxDOM;
    var _name = name;

    // event fired when a tab changes
    self.onSelectionChanged = new SimpleEvent();

    var getCheckBox = function() {
        return _checkBoxDOM[0];
    };

    var hashFromVal = function (binValue) {
        if (binValue === true) {
            return "selected_" + name;
        }
        else {
            return "unselected_" + name;
        }
    }

    var hashFromState = function () {
        return hashFromVal (getCheckBox().checked);
    };

    _checkBoxDOM.bind("change", function (event, ui) {
        self.fireSelectedHashChanged();
    });

    self.fireSelectedHashChanged = function () {
        self.onSelectionChanged.fire(self, self.getSelectedKey());
    };

    self.getSelectedKey = function () {
        return hashFromState();
    };

    // select method
    self.select = function (hash) {
        var checkedState; 
        if (hash === hashFromVal(true)) {
            checkedState = true;
        }
        else if (hash === hashFromVal(false)) {
            checkedState = false;
        }

        if (checkedState || (checkedState === false)) {
            getCheckBox().checked = checkedState;
            _checkBoxDOM.trigger("change");
        }
    };

    // do nothing
    self.unselect = function () {

    };
};

///#source 1 1 /Scripts/BPF/Composite.js
// BPF JavaScript library version 0.9
// (c) Nick Polyak 2012 - http://awebpros.com/
// License: Code Project Open License (CPOL) 1.92(http://www.codeproject.com/info/cpol10.aspx)
//
// short overview of copyright rules:
// 1. you can use this framework in any commercial or non-commercial 
//    product as long as you retain this copyright message
// 2. Do not blame the author(s) of this software if something goes wrong. 
// 
// Also as a courtesy, please, mention this software in any documentation for the 
// products that use it.

/// <reference path="jquery-1.8.2.js" />
/// <reference path="JQueryUtils.js" />
/// <reference path="ArrayExtensions.js" />
/// <reference path="SimpleEvent.jsx" />
/// <reference path="EventBarrier.js" />
/// <reference path="SimpleEvent.js" />
var bpf = bpf || {};

bpf.cmpst = bpf.cmpst || {};

bpf.cmpst.pluginCache = {
    cache : {},

    getPlugin : function(url) {
        var result = bpf.cmpst.pluginCache.cache[url];

        return result;
    },

    addPlugin : function(url, plugin) {
        bpf.cmpst.pluginCache.cache[url] = plugin;
    },

    hasPlugin : function(url) {
        var plugin = bpf.cmpst.pluginCache.cache[url];

        return plugin? true : false;
    }
};

bpf.cmpst.rootPluginInfo = {};

var replaceChildren = function (domToPlugInto, html) {
    domToPlugInto.contents().remove(); // remove old children

    domToPlugInto.append(html);
    return domToPlugInto;
}
// search for javascript within html file
var getScriptTextFromHTML = function (downloadedHtmlDOM) {
    var scriptTags = downloadedHtmlDOM.filter("script[data-type='script-interface']");

    if ((!scriptTags) || (scriptTags.length === 0))
        return;

    var scriptTag = scriptTags[0];

    if (!scriptTag)
        return;

    var scriptText = scriptTag.text || scriptTag.textContent || scriptTag.innerHTML;

    return scriptText;
}

var hasCodeBehindInASeparateFile = function (downloadedHtmlDOM) {
    var scriptTags = downloadedHtmlDOM.filter("script[data-has-external-code-behind='true']");

    return scriptTags.length ? true : false;
}

var downloadPluginAndAddToCache = function(pluginUrl, pluginAddedToCacheEvent) {
    $.ajax({
        "url": pluginUrl,
        "async": true,
        "success" : function(downloadedHtml) {
            var plugin = 
                {
                };

            plugin.downloadedHtmlContainer = function () {
                var _downloadedHtml;
                var _downloadedHtmlDOM;

                var accessor = {
                    getDownloadedHtml: function () {
                        return _downloadedHtml;
                    },
                    setDownloadedHtml: function (downloadedHtml) {
                        _downloadedHtml = downloadedHtml;

                        _downloadedHtmlDOM = $(_downloadedHtml);
                    },
                    getDownloadedHtmlDOM: function () {
                        return _downloadedHtmlDOM;
                    },
                    setDownloadedHtmlDOM: function (downloadedHtmlDOM) {
                        _downloadedHtmlDOM = downloadedHtmlDOM;

                        _downloadedHtml = _downloadedHtmlDOM.outerHTML();
                    }
                }

                return accessor;
            }();

            plugin.downloadedHtmlContainer.setDownloadedHtml(downloadedHtml);

            var downloadedHtmlDOM =
                plugin.downloadedHtmlContainer.getDownloadedHtmlDOM();

            var addPluginToCache = function(scriptText) {
                plugin.codeBehind = eval(scriptText);
                
                bpf.cmpst.pluginCache.addPlugin(pluginUrl, plugin);

                pluginAddedToCacheEvent.fire(plugin);
            }

            var scriptText = getScriptTextFromHTML(downloadedHtmlDOM);

            var thereIsCodeBehindInASeparateFile = hasCodeBehindInASeparateFile(downloadedHtmlDOM);

            // remove script tags from html
            downloadedHtmlDOM = downloadedHtmlDOM.not("script");

            plugin.downloadedHtmlContainer.setDownloadedHtmlDOM(downloadedHtmlDOM);

            if (scriptText) {
                addPluginToCache(scriptText);
                return;
            }

            if ( thereIsCodeBehindInASeparateFile ) {
                var scriptUrl = pluginUrl + ".js";
                $.getScript(scriptUrl, function (downloadedScriptText) {
                    addPluginToCache(downloadedScriptText);
                    return;
                });
                return;
            }

            addPluginToCache(); // no code behind
        }
    });
}

var processAndInsertPluginFromCacheImpl = function 
(
    currentDOMPluginInfo, 
    selectorToPlugInto, 
    pluginUrl,
    addedDownloadedEvent, 
    compositionReadyEventAccumulator,
    postRenderArguments,
    preRenderArguments) 
{
    var plugin = bpf.cmpst.pluginCache.getPlugin(pluginUrl);
    
    var codeBehind = plugin.codeBehind;

    if (codeBehind) {
        if (codeBehind.preRender) {
            codeBehind.preRender.call(plugin.downloadedHtmlContainer, preRenderArguments);
        }
    }

    var downloadedHtml = plugin.downloadedHtmlContainer.getDownloadedHtml();
    var downloadedHtmlDOM = plugin.downloadedHtmlContainer.getDownloadedHtmlDOM();

    var downloadedHtmlToInsert = downloadedHtml;

    if (!currentDOMPluginInfo) {
        currentDOMPluginInfo = bpf.cmpst.rootPluginInfo;
    }

    var currentDOM = currentDOMPluginInfo.currentDOM;

    var domToPlugInto;
    if (!currentDOM) {
        domToPlugInto = $(selectorToPlugInto);
    }
    else {
        domToPlugInto = currentDOM.find(selectorToPlugInto);
    }

    replaceChildren(domToPlugInto, downloadedHtmlToInsert);

    if (codeBehind) {
        if (!currentDOMPluginInfo.pluginMap) {
            currentDOMPluginInfo.pluginMap = {};
        }

        var subPluginDOMInfo = {
            "currentDOM": domToPlugInto,
            "codeBehind": codeBehind,
            "postRenderArguments": postRenderArguments
        };
        currentDOMPluginInfo.pluginMap[selectorToPlugInto] = subPluginDOMInfo;

        if (codeBehind.postRender)
            codeBehind.postRender.call(subPluginDOMInfo, compositionReadyEventAccumulator);
    }

    addedDownloadedEvent.fire(null, true);
}

bpf.cmpst.getPlugin =
    function (
        url, 
        currentDOMPluginInfo, 
        selectorToPlugInto, 
        compositionReadyEventAccumulator, 
        postRenderArguments,
        preRenderArgs) 
    {
        var addedDownloadedEvent = new SimpleEvent();
    
        compositionReadyEventAccumulator.addCallback(addedDownloadedEvent, url);

        var processAndInsertPluginFromCache = function(){
            processAndInsertPluginFromCacheImpl (
                currentDOMPluginInfo, 
                selectorToPlugInto, 
                url,
                addedDownloadedEvent, 
                compositionReadyEventAccumulator,
                postRenderArguments,
                preRenderArgs);
        };

        if (bpf.cmpst.pluginCache.hasPlugin()) {
            processAndInsertPluginFromCache();
            return;
        }

        var pluginAddedToCacheEvent = new SimpleEvent();

        pluginAddedToCacheEvent.addSimpleEventHandler(function () {
            processAndInsertPluginFromCache();
        });

        downloadPluginAndAddToCache(url, pluginAddedToCacheEvent);

    };

var getCallableControl = function (currentDOMPluginInfo) {
    return {
        "currentDOMPluginInfo": currentDOMPluginInfo,
        "call": function (callFunctionName, otherArgs) {
            var argsToPassToEvents = Array.prototype.copy.call(arguments, 1);
            //argsToPassToEvents.insert(0, parentDomElement);
            currentDOMPluginInfo.codeBehind[callFunctionName].apply(currentDOMPluginInfo, argsToPassToEvents);
        }
    };
}

bpf.control = function (selector, currentDOMPluginInfo) {
    if (!currentDOMPluginInfo) {
        currentDOMPluginInfo = bpf.cmpst.rootPluginInfo;
    }

    var selectorPluginInfo = currentDOMPluginInfo.pluginMap[selector];

    return getCallableControl(selectorPluginInfo);
}

///#source 1 1 /Scripts/BPF/EventBarrier.js
// BPF JavaScript library version 0.9
// (c) Nick Polyak 2012 - http://awebpros.com/
// License: Code Project Open License (CPOL) 1.92(http://www.codeproject.com/info/cpol10.aspx)
//
// short overview of copyright rules:
// 1. you can use this framework in any commercial or non-commercial 
//    product as long as you retain this copyright message
// 2. Do not blame the author(s) of this software if something goes wrong. 
// 
// Also as a courtesy, please, mention this software in any documentation for the 
// products that use it.

/// <reference path="SimpleEvent.js" />

// should fire only if all events completed. 
// if one or more events failed returns false, iff all events succeeded returns true
"use strict";

var bpf = bpf || {};

bpf.utils = bpf.utils || {};

bpf.utils.EventBarrier = function () {
    var eventToFireAtTheEnd = new SimpleEvent();
    var events = {};
    var eventId = 0;

    var resultSuccess = true;
    var numberEvents = 0;

    this.getNumberEvents = function() {
        return numberEvents;
    }
    
    this.addSimpleEventHandler = function (eventHandler) {
        eventToFireAtTheEnd.addSimpleEventHandler(eventHandler);
    };

    this.addCallback = function (event, eventName) {
        var self = this;
        eventId++;

        var eventData = {
            id: eventId,
            event: event
        };

        if (eventName)
            eventData.name = eventName;

        events[eventId] = eventData;

        numberEvents++;

        event.addSimpleEventHandler(function (success) {
            delete events[eventId];
            numberEvents--;

            resultSuccess = resultSuccess && success;

            if (numberEvents === 0)
                eventToFireAtTheEnd.fire(self, resultSuccess);
        });
    }

    this.createChildEventBarrier = function(childEventBarrierName) {
        var childEventBarrier = new bpf.utils.EventBarrier();

        var childEventBarrierFiredEvent = new SimpleEvent();

        this.addCallback(childEventBarrierFiredEvent, childEventBarrierName);

        // when the child event product is done, the simple event 
        // which is one of the callbacks for the parent. This will reduce the 
        // parent's reference count and if the parent's reference count is zero, 
        // fire the event-at-the-end.
        childEventBarrier.addSimpleEventHandler(function (success) {
            childEventBarrierFiredEvent.fire();
        });

        return childEventBarrier;
    }
};

///#source 1 1 /Scripts/BPF/HashStrings.js
// BPF JavaScript library version 0.9
// (c) Nick Polyak 2012 - http://awebpros.com/
// License: Code Project Open License (CPOL) 1.92(http://www.codeproject.com/info/cpol10.aspx)
//
// short overview of copyright rules:
// 1. you can use this framework in any commercial or non-commercial 
//    product as long as you retain this copyright message
// 2. Do not blame the author(s) of this software if something goes wrong. 
// 
// Also as a courtesy, please, mention this software in any documentation for the 
// products that use it.

var bpf = bpf || {};

bpf.utils = bpf.utils || {};

bpf.utils.segmentSeparationCharacter = '.';

// strips the leading '#' character.
bpf.utils.stripFirstPound = function (str) {
    if (!str)
        return str;

    if (str.charAt(0) === '#')
        return str.slice(1);

    return str;
};

bpf.utils.stripTrailingDot = function (str) {
    if (!str)
        return str;

    if (str.charAt(str.length - 1) === '.')
        return str.slice(0, str.length - 1);

    return str;
};

/// replaces blacks with dashes
bpf.utils.fillBlanks = function (str) {
    if (!str)
        return str;

    return str.replace(/\s+/g, "-");
};

///#source 1 1 /Scripts/BPF/Iterator.js
// BPF JavaScript library version 0.9
// (c) Nick Polyak 2012 - http://awebpros.com/
// License: Code Project Open License (CPOL) 1.92(http://www.codeproject.com/info/cpol10.aspx)
//
// short overview of copyright rules:
// 1. you can use this framework in any commercial or non-commercial 
//    product as long as you retain this copyright message
// 2. Do not blame the author(s) of this software if something goes wrong. 
// 
// Also as a courtesy, please, mention this software in any documentation for the 
// products that use it.

/// <reference path="ArrayExtensions.js" />
"use strict";

var bpf = bpf || {};

bpf.utils = bpf.utils || {};

// creates an iterator to iterate through an array.
bpf.utils.Iterator = function (array) {
    var _self = this;
    var _currentIdx = 0;

    // returns current value
    _self.current = function () {
        return array[_currentIdx];
    };

    // checks if current position is valid
    _self.isCurrentValid = function () {
        if (!array)
            return false;

        if (!isObjectArray(array))
            return false;

        if (_currentIdx < array.length)
            return true;

        return false;
    };

    // moves to the next position
    _self.moveToNext = function () {
        _currentIdx++;
    };

    _self.getCurrentIdx = function () {
        return _currentIdx;
    }
};

///#source 1 1 /Scripts/BPF/JQTabsNavAdaptor.js
// BPF JavaScript library version 0.9
// (c) Nick Polyak 2012 - http://awebpros.com/
// License: Code Project Open License (CPOL) 1.92(http://www.codeproject.com/info/cpol10.aspx)
//
// short overview of copyright rules:
// 1. you can use this framework in any commercial or non-commercial 
//    product as long as you retain this copyright message
// 2. Do not blame the author(s) of this software if something goes wrong. 
// 
// Also as a courtesy, please, mention this software in any documentation for the 
// products that use it.

/// <reference path="jquery-1.8.2.js" />
/// <reference path="ArrayExtensions.js" />
/// <reference path="SimpleEvent.js" />
/// <reference path="HashStrings.js" />
/// <reference path="NavigationNode.js" />

var bpf = bpf || {};

bpf.nav = bpf.nav || {};

bpf.nav.JQTabsNavAdaptor = function (tabsObj) {
    var self = this;

    var getSelectedHash = function () {
        var selectedHash = bpf.utils.stripFirstPound(tabsObj.find('.ui-tabs-active a').attr("href"));
        return selectedHash;
    }

    // event fired when a tab changes
    self.onSelectionChanged = new SimpleEvent();

    // fires self.onSelectionChanged event when the tab selection changes
    tabsObj.bind("tabsselect", function (event, ui) {
        var tObj = tabsObj[0];

        if (event.target !== tObj)
            return;

        self.selectedKey = bpf.utils.stripFirstPound(ui.tab.hash);

        fireSelectedHashChanged();
    });

    var fireSelectedHashChanged = function () {
        self.onSelectionChanged.fire(self, self.selectedKey);
    };

    // returns the key corresponding to the currently selected tab
    self.getSelectedKey = function () {
        if (self.selectedKey)
            return self.selectedKey;

        var selectedKey = getSelectedHash();

        return selectedKey;
    };

    // selects the tab corresponding to the passed key
    self.select = function (key) {
        tabsObj.tabs("select", key);
    };

    // Interface unselect method implementation
    // (in case of tabs, it does nothing since tabs cannot be unselected - one tab should always be selected)
    self.unselect = function () {

    };
};

bpf.nav.getJQTabsNode = function (tabsObj) {
    var adaptedData = new bpf.nav.JQTabsNavAdaptor(tabsObj);

    return new bpf.nav.Node(adaptedData);
};

bpf.nav.addJQTabsChild = function (parentNode, key, tabsObj) {

    var adaptedChild = new bpf.nav.JQTabsNavAdaptor(tabsObj);
    return parentNode.addAdaptedChild(key, adaptedChild);
};
///#source 1 1 /Scripts/BPF/JQueryUiUtils.js
// BPF JavaScript library version 0.9
// (c) Nick Polyak 2012 - http://awebpros.com/
// License: Code Project Open License (CPOL) 1.92(http://www.codeproject.com/info/cpol10.aspx)
//
// short overview of copyright rules:
// 1. you can use this framework in any commercial or non-commercial 
//    product as long as you retain this copyright message
// 2. Do not blame the author(s) of this software if something goes wrong. 
// 
// Also as a courtesy, please, mention this software in any documentation for the 
// products that use it.

/// <reference path="jquery-1.8.2.js" />
/// <reference path="jquery-ui-1.9.0.js" />

if ($.ui) {
    $.ui.setSpinner = function (selectorString) {
        var spinnerElements = $(selectorString).spinner({
            min: 0,
            max: 1000
        });

        $(spinnerElements).spinner().bind("spinstop", function (event, data) {
            $(this).spinner().trigger("change");
        });
    }
};

///#source 1 1 /Scripts/BPF/JQueryUtils.js
// BPF JavaScript library version 0.9
// (c) Nick Polyak 2012 - http://awebpros.com/
// License: Code Project Open License (CPOL) 1.92(http://www.codeproject.com/info/cpol10.aspx)
//
// short overview of copyright rules:
// 1. you can use this framework in any commercial or non-commercial 
//    product as long as you retain this copyright message
// 2. Do not blame the author(s) of this software if something goes wrong. 
// 
// Also as a courtesy, please, mention this software in any documentation for the 
// products that use it.

/// <reference path="jquery-1.8.2.js" />
$.fn.outerHTMLForSimpleJQObj = function () {
    var t = $(this);

    if ('outerHTML' in t[0]) {
        return t[0].outerHTML;
    } else {
        var content = t.wrap('<div></div>').parent().html();
        t.unwrap();
        return content;
    }
};

$.fn.outerHTML = function () {
    var t = $(this);

    var result = ""

    for (var i = 0; i < t.length; i++) {
        var currentPart = t[i];

        if (currentPart instanceof Text) {
            result += currentPart.data;
        }
        else {
            result += $(currentPart).outerHTMLForSimpleJQObj();
        }
    }

    return result;
};

///#source 1 1 /Scripts/BPF/KoObservableNavAdapter.js
// BPF JavaScript library version 0.9
// (c) Nick Polyak 2012 - http://awebpros.com/
// License: Code Project Open License (CPOL) 1.92(http://www.codeproject.com/info/cpol10.aspx)
//
// short overview of copyright rules:
// 1. you can use this framework in any commercial or non-commercial 
//    product as long as you retain this copyright message
// 2. Do not blame the author(s) of this software if something goes wrong. 
// 
// Also as a courtesy, please, mention this software in any documentation for the 
// products that use it.

/// <reference path="jquery-1.8.2.js" />
/// <reference path="ArrayExtensions.js" />
/// <reference path="SimpleEvent.js" />
/// <reference path="HashStrings.js" />

var bpf = bpf || {};

bpf.nav = bpf.nav || {};

// observable adaptor. it fires onSelectionChanged event
// when observable changes. 
// it also provides functions for turning key to object and vice versa
// and uses these functions for selecting.
bpf.nav.KoObservableNavAdaptor = function (observable, keyToObjectFn, objectToKeyFn) {
    var _self = this;

    var _selectedObject;

    var _keyToObjectFn = keyToObjectFn;
    var _objectToKeyFn = objectToKeyFn;

    // event fired when a tab changes
    _self.onSelectionChanged = new SimpleEvent();

    observable.subscribe(function (selectedObject) {
        if (_selectedObject === selectedObject)
            return;

        _selectedObject = selectedObject;

        fireSelectedHashChanged();
    });

    var fireSelectedHashChanged = function () {
        _self.onSelectionChanged.fire(_self, _self.selectedKey);
    };

    _self.getSelectedKey = function () {
        if (!_selectedObject)
            return;

        var resultSelectedKey = _objectToKeyFn(_selectedObject);

        return resultSelectedKey;
    };

    _self.getChildObjectByKey = function(key) {
        return _keyToObjectFn(key);
    };

    // select method
    _self.select = function (key) {
        _selectedObject = _keyToObjectFn(key);

        if (_selectedObject)
            observable(_selectedObject);
        else
            _self.unselect();
    };

    // do nothing
    _self.unselect = function () {

        _selectedObject = null;
        observable("");
    };
};

///#source 1 1 /Scripts/BPF/NavigationFactoryNode.js
// BPF JavaScript library version 0.9
// (c) Nick Polyak 2012 - http://awebpros.com/
// License: Code Project Open License (CPOL) 1.92(http://www.codeproject.com/info/cpol10.aspx)
//
// short overview of copyright rules:
// 1. you can use this framework in any commercial or non-commercial 
//    product as long as you retain this copyright message
// 2. Do not blame the author(s) of this software if something goes wrong. 
// 
// Also as a courtesy, please, mention this software in any documentation for the 
// products that use it.

/// <reference path="underscore.js" />
/// <reference path="ArrayExtensions.js" />
/// <reference path="StringExtensions.js" />
/// <reference path="SimpleEvent.js" />
/// <reference path="HashStrings.js" />
/// <reference path="NavigationNodeBase.js" />

var bpf = bpf || {};

bpf.nav = bpf.nav || {};

bpf.nav.FactoryNode = function (data, parentNode, childNodeProducingFunction) {
    var _self = this;
    var _data = data;

    var _childNodeProducingFunction = childNodeProducingFunction;

    bpf.nav.Node.call(_self, data, parentNode); // extend the class bpf.nav.NodeBase

    _self.getOnSelectionChangedEvent().addSimpleEventHandler(function () {
        var selectedKey = _self.getSelectedKey();

        if (!selectedKey)
            return;

        addNodeIfNeeded(selectedKey);
    });

    var addNodeIfNeeded = function (key) {
        var children = _self.getChildren();
        if ((!children) || (!children.containsKey(key))) {
            if (childNodeProducingFunction) {
                var childNode = childNodeProducingFunction(key, _data);

                if (!childNode)
                    return;

                _self.addChildNode(key, childNode);
            }
        }
    }

    _self.select = function (key) {
        if (_self.getSelectedKey() === key)
            return;

        _self.data.select(key);

        addNodeIfNeeded(key);
    };
};
///#source 1 1 /Scripts/BPF/NavigationNode.js
// BPF JavaScript library version 0.9
// (c) Nick Polyak 2012 - http://awebpros.com/
// License: Code Project Open License (CPOL) 1.92(http://www.codeproject.com/info/cpol10.aspx)
//
// short overview of copyright rules:
// 1. you can use this framework in any commercial or non-commercial 
//    product as long as you retain this copyright message
// 2. Do not blame the author(s) of this software if something goes wrong. 
// 
// Also as a courtesy, please, mention this software in any documentation for the 
// products that use it.

/// <reference path="underscore.js" />
/// <reference path="ArrayExtensions.js" />
/// <reference path="StringExtensions.js" />
/// <reference path="SimpleEvent.js" />
/// <reference path="HashStrings.js" />
/// <reference path="NavigationNodeBase.js" />
var bpf = bpf || {};

bpf.nav = bpf.nav || {};

bpf.nav.Node = function (data, parentNode) {
    var _self = this;

    bpf.nav.NodeBase.call(_self, parentNode); // extend the class

    // 'data' class member contains the actuall object 
    // providing a way to access child objects one of which can 
    // be selected and whose selection should change the hash and 
    // vice versa - whose selection change should be 
    // triggered by a changing hash. Usually 
    // 'data' object is an adaptor providing 
    // a predefined selection interface
    // to some selectable data.
    // 'data' should satisfy the following interface
    // requirements:
    //     1. It should provide onSelectionChanged simple event
    //        to be fired when its selection changes
    //     2. its getSelectedKey() function should return a 
    //        a string corresponding to the selected key within the data
    //     3. It should have a function "void select(key)" which will 
    //        select a child object within the data corresponding to the 
    //        passed key.
    //     4. It should have a function "unselect()" that will unselect 
    //        the currently selected child
    var setData = function (data) {
        _self.data = data;

        data.onSelectionChanged.addSimpleEventHandler(function (key) {
            // if we are here this means that the key has already been 
            // selected by the adapter. There is no reason to select it again.
            // self.select(key); 
            _self.fireSelectionChanged();
        });
    };

    setData(data);

    // we can add multiple children using addChildren function.
    // it takes 3 function - to return a child collection, to 
    // adapt each child to the 'data' interface and get a key from 
    // the child. This function returns a collection of children. 
    // of the current node
    _self.addChildren = function (getChildrenFn, adaptChildFn, keyGetterFn) {
        var children = getChildrenFn();

        _(children).each(function (child) {
            var key = keyGetterFn(child);

            var adaptedChild = adaptChildFn(child);

            _self.addAdaptedChild(key, adaptedChild);
        });

        return _childNodes;
    };

    _self.getSelectedKey = function () {
        return _self.data.getSelectedKey();
    };

    _self.unselect = function () {
        _self.data.unselect();
    };

    _self.chainUnselect = function () {
        var selectedChild = _self.getSelectedChild();

        if (selectedChild)
            selectedChild.chainUnselect();

        _self.unselect();
    }

    _self.select = function (key) {
        if (_self.getSelectedKey() === key)
            return;

        _self.data.select(key);
    };

    _self.getSelectedChild = function () {
        return _self.getChild(_self.getSelectedKey());
    };

    _self.setSelectedKeySegmentsRecursive = function (urlRemainder) {
        var endings = ['.', '(', ')'];

        var remainder;
        var ending; 

        var key = urlRemainder.getStrUpTo(endings, function (chosenEnding, remainderAfterGettingString) {
            ending = chosenEnding;
            remainder = remainderAfterGettingString;
        });

        _self.select(key);
        var childNode = _self.getSelectedChild();

        if (!childNode)
            return remainder;

        if ((ending === '.') || (ending === '(')) {
            remainder = childNode.setSelectedKeySegmentsRecursive(remainder);
        }
        else {
            childNode.chainUnselect();
        }

        if (ending === ')') {
            // add it back to the remainder in order not to treat the case 
            // when the product node is at the top level as a special case
            remainder = ending + remainder;
        }

        return remainder;
    }

    _self.getUrlRecursive = function () {
        var result = "";

        var key = _self.data.getSelectedKey();

        if (!key) {
            return result;
        }
        else {
            result += key;
        }

        var selectedChild = _self.getSelectedChild();

        if (selectedChild) {
            var childUrl = selectedChild.getUrlRecursive();

            if (childUrl)
                result += bpf.utils.segmentSeparationCharacter + childUrl;
        }

        return result;
    }
};

// creates a navigation node from some data. 
// adaptorConstructor is called on the data to create adaptedData
// that has needed methods and events.
bpf.nav.createNode = function (data, adaptorFunction) {
    var adaptedData = adaptorFunction(data);

    var node = new bpf.nav.Node(adaptedData);

    return node;
};

///#source 1 1 /Scripts/BPF/NavigationNodeBase.js
// BPF JavaScript library version 0.9
// (c) Nick Polyak 2012 - http://awebpros.com/
// License: Code Project Open License (CPOL) 1.92(http://www.codeproject.com/info/cpol10.aspx)
//
// short overview of copyright rules:
// 1. you can use this framework in any commercial or non-commercial 
//    product as long as you retain this copyright message
// 2. Do not blame the author(s) of this software if something goes wrong. 
// 
// Also as a courtesy, please, mention this software in any documentation for the 
// products that use it.

/// <reference path="underscore.js" />
/// <reference path="../jquery-1.8.3.js" />
/// <reference path="ArrayExtensions.js" />
/// <reference path="SimpleEvent.js" />
/// <reference path="HashStrings.js" />
/// <reference path="Iterator.js" />
/// <reference path="OrderedMap.js" />

/// base class for navigation node
var bpf = bpf || {};

bpf.nav = bpf.nav || {};

bpf.nav.NodeBase = function (parentNode) {
    var _self = this;

    var _onSelectionChanged = new SimpleEvent();

    var _parentNode = parentNode;
    var _childNodes;

    _self.setParentNode = function (parentNode) {
        _parentNode = parentNode;
    }

    _self.getOnSelectionChangedEvent = function () {
        return _onSelectionChanged;
    }

    // fire selection changed and propagate to the
    // parent
    _self.fireSelectionChanged = function () {
        // propagate to the root of the tree
        if (_parentNode) {
            _parentNode.fireSelectionChanged();
        }
        else {
            // only fire onSelectionChanged on the root node
            // to change the whole url hash
            _onSelectionChanged.fire();
        }
    }

    // given a key and a childNode it adds the it to the 
    // child collection
    _self.addChildNode = function (key, childNode) {
        if (!_childNodes) {
            _childNodes = new bpf.utils.OrderedMap();
        }

        // pass _self as parent node
        childNode.setParentNode(_self);

        _childNodes.add(key, childNode);
    };


    // given a key and a data it adds the it to the 
    // child collection
    _self.addAdaptedChild = function (key, adaptedChildData) {
        var childNode = new bpf.nav.Node(adaptedChildData);

        _self.addChildNode(key, childNode);

        return childNode;
    };

    _self.addChild = function (key, data, adaptorFunction) {
        var adaptedChildData = adaptorFunction(data);

        return _self.addAdaptedChild(key, adaptedChildData);
    };

    // give a key, returns the child node corresponding to this key
    _self.getChild = function (key) {
        if (!key)
            return;

        if (!_childNodes)
            return;

        return _childNodes.objByKey(key);
    };

    // returns all child nodes
    _self.getChildren = function () {
        return _childNodes;
    };

    // returns the total hash based on the selected segments
    _self.getTotalHash = function () {
        var totalHash = '#' + _self.getUrlRecursive() + bpf.utils.segmentSeparationCharacter;

        return totalHash;
    }

    _self.setSelectedKeySegments = function (urlHash) {
        // remove the leading '#' and trailing '.'
        urlHash = bpf.utils.stripFirstPound(urlHash);
        urlHash = bpf.utils.stripTrailingDot(urlHash);

        _self.setSelectedKeySegmentsRecursive(urlHash);
    }
};

// sets window hash from the node's segments
bpf.nav.setTotalHash = function (topLevelNode) {
    window.location.hash = topLevelNode.getTotalHash();
};

// sets the selected segments based on the window hash
bpf.nav.setKeySegmentToHash = function (topLevelNode) {
    var totalHash = topLevelNode.getTotalHash();

    if (totalHash === window.location.hash)
        return false; // return false if hash did not change

    topLevelNode.setSelectedKeySegments(window.location.hash);
    bpf.nav.setTotalHash(topLevelNode);

    return true; // return true if hash changed
};

bpf.nav.connectToUrlHash = function (topLevelNode) {
    $(window).bind('hashchange', function (event) {
        bpf.nav.setKeySegmentToHash(topLevelNode);
    });

    topLevelNode.getOnSelectionChangedEvent().addSimpleEventHandler(function () {
        bpf.nav.setTotalHash(topLevelNode);
    });
};

///#source 1 1 /Scripts/BPF/ObjUtils.js
// BPF JavaScript library version 0.9
// (c) Nick Polyak 2012 - http://awebpros.com/
// License: Code Project Open License (CPOL) 1.92(http://www.codeproject.com/info/cpol10.aspx)
//
// short overview of copyright rules:
// 1. you can use this framework in any commercial or non-commercial 
//    product as long as you retain this copyright message
// 2. Do not blame the author(s) of this software if something goes wrong. 
// 
// Also as a courtesy, please, mention this software in any documentation for the 
// products that use it.

/// <reference path="../qunit.js" />
/// <reference path="ArrayExtensions.js" />
"use strict";

var bpf = bpf || {};

bpf.utils = bpf.utils || {};

// since we should not extend object's prototype (see http://erik.eae.net/archives/2005/06/06/22.13.54/)
// we extend the object in a different way - by using objExtend function
// extend the object to have some utility methods, 
// especially those that are related to maps/dictionaries
bpf.utils.extendObj = function (obj) {
    // creates a shallow copy of an (not array) object
    var cloneObj = function (anObj) {
        var result = {};

        for (var key in anObj) {
            result[key] = anObj[key];
        }

        return result;
    };

    var cloneUnextendedOnly = function (anObj) {
        var result = {};

        for (var key in anObj) {
            var val = anObj[key];

            if (typeof (val) === 'function')
                continue;

            result[key] = val;
        }

        return result;
    };


    var _extendedObj = cloneObj(obj);

    _extendedObj.cloneObj = function () {
        return cloneObj(_extendedObj);
    };

    _extendedObj.unextendObj = function () {
        return cloneUnextendedOnly(_extendedObj);
    }

    _extendedObj.removeKey = function (key) {
        delete _extendedObj[key];
    };

    _extendedObj.containsKey = function (key) {
        return _extendedObj.hasOwnProperty(key);
    };

    _extendedObj.printKeys = function () {
        for (var key in _extendedObj) {
            console.log(key + " ");
        }
    };

    return _extendedObj;
};

//test("testRemoveKeys", function () {
//    var myObj = bpf.utils.objExtend({
//        key1: "val1",
//        key2: "val2"
//    });

//    equal(myObj.containsKey("key1"), true);
//    equal(myObj.containsKey("key3"), false);

//    myObj.removeKey("key1");

//    equal(myObj.containsKey("key1"), false);
//});

///#source 1 1 /Scripts/BPF/OrderedMap.js
// BPF JavaScript library version 0.9
// (c) Nick Polyak 2012 - http://awebpros.com/
// License: Code Project Open License (CPOL) 1.92(http://www.codeproject.com/info/cpol10.aspx)
//
// short overview of copyright rules:
// 1. you can use this framework in any commercial or non-commercial 
//    product as long as you retain this copyright message
// 2. Do not blame the author(s) of this software if something goes wrong. 
// 
// Also as a courtesy, please, mention this software in any documentation for the 
// products that use it.

/// <reference path="ObjUtils.js" />
/// <reference path="ArrayExtensions.js" />
/// <reference path="Iterator.js" />
"use strict";

var bpf = bpf || {};

bpf.utils = bpf.utils || {};

bpf.utils.OrderedMap = function () {
    var _self = this;

    // map of keys into values
    var _map = {};

    // ordered keys
    var _orderedKeys = [];

    // add a key-value pair to the map and
    // push the key into orderedKeys array
    _self.add = function (key, obj) {
        _orderedKeys.push(key);
        _map[key] = obj;
    };

    _self.objByKey = function (key) {
        return _map[key];
    };

    _self.getIterator = function () {
        var iterator = new bpf.utils.Iterator(_orderedKeys);

        iterator.currentKey = function () {
            var currentKey = _orderedKeys[iterator.getCurrentIdx()];

            return currentKey;
        };

        // replace 'current' function to return the object from the 'map'
        iterator.current = function () {
            var currentKey = iterator.currentKey();

            return _self.objByKey(currentKey);
        };

        return iterator;
    };

    _self.getMapClone = function () {
        return bpf.utils.extendObj(_map);
    };

    _self.containsKey = function (key) {
        return _map.hasOwnProperty(key);
    };

    _self.reset = _self.clear = function () {
        _orderedKeys = [];

        _map = {};
    };
};


///#source 1 1 /Scripts/BPF/ProductNavigationNode.js
// BPF JavaScript library version 0.9
// (c) Nick Polyak 2012 - http://awebpros.com/
// License: Code Project Open License (CPOL) 1.92(http://www.codeproject.com/info/cpol10.aspx)
//
// short overview of copyright rules:
// 1. you can use this framework in any commercial or non-commercial 
//    product as long as you retain this copyright message
// 2. Do not blame the author(s) of this software if something goes wrong. 
// 
// Also as a courtesy, please, mention this software in any documentation for the 
// products that use it.

/// <reference path="ObjUtils.js" />
/// <reference path="underscore.js" />
/// <reference path="ArrayExtensions.js" />
/// <reference path="StringExtensions.js" />
/// <reference path="OrderedMap.js" />
/// <reference path="SimpleEvent.js" />
/// <reference path="HashStrings.js" />
/// <reference path="NavigationNodeBase.js" />

var bpf = bpf || {};

bpf.nav = bpf.nav || {};

bpf.nav.ProductNode = function (parentNode) {
    var _self = this;

    bpf.nav.NodeBase.call(_self, parentNode); // extend the class

    _self.chainUnselect = function () {
        var childNodes = _self.getChildren();

        for (var childNodeIterator = childNodes.getIterator() ;
             childNodeIterator.isCurrentValid() ;
             childNodeIterator.moveToNext())
        {
            var currentChildNode = childNodeIterator.current();

            currentChildNode.chainUnselect();
        }
    }

    _self.setSelectedKeySegmentsRecursive = function (urlRemainder) {
        var remainder = urlRemainder;

        var childNodes = _self.getChildren();
        var nonprocessedKeys = childNodes.getMapClone();

        while (remainder.length > 0) {
            // should always start with '(' or ')'
            if (remainder.startsWith(')'))
                return remainder.removePrefix(')'); // we ended this node

            if (!remainder.startsWith('('))
                return; // return in case of error

            remainder = remainder.removePrefix('(');

            var keyToSubNodes = remainder.getStrUpTo('/', function (chosenEnding, remainderAfterKey) {
                remainder = remainderAfterKey;
            });

            nonprocessedKeys.removeKey(keyToSubNodes); // remove the processed key from the non-processedKeys

            var childNode = childNodes.objByKey(keyToSubNodes);

            remainder = childNode.setSelectedKeySegmentsRecursive(remainder);
        }

        // for all the keys that were not part of the url, do chain chain unselect 

        var nonprocessedKeysUnextended = nonprocessedKeys.unextendObj();
        for (var unprocessedKey in nonprocessedKeysUnextended) {
            var childNode = childNodes.objByKey(unprocessedKey);
            childNode.chainUnselect();
        }

        return remainder;
    }

    _self.getUrlRecursive = function () {
        var result = "";

        var childNodes = _self.getChildren();

        for (var childNodeIterator = childNodes.getIterator() ;
             childNodeIterator.isCurrentValid() ;
             childNodeIterator.moveToNext())
        {
            var currentKey = childNodeIterator.currentKey();

            var currentChildNode = childNodeIterator.current();

            var childResult = currentChildNode.getUrlRecursive();

            if (!childResult)
                continue;

            result += '(' + currentKey + '/' + childResult + ')';
        }

        return result;
    }
}

///#source 1 1 /Scripts/BPF/SimpleEvent.js
// BPF JavaScript library version 0.9
// (c) Nick Polyak 2012 - http://awebpros.com/
// License: Code Project Open License (CPOL) 1.92(http://www.codeproject.com/info/cpol10.aspx)
//
// short overview of copyright rules:
// 1. you can use this framework in any commercial or non-commercial 
//    product as long as you retain this copyright message
// 2. Do not blame the author(s) of this software if something goes wrong. 
// 
// Also as a courtesy, please, mention this software in any documentation for the 
// products that use it.

/// <reference path="ArrayExtensions.js" />

function SimpleEvent() {
    this.eventHandlers = new Array();
};

SimpleEvent.prototype = {
    addSimpleEventHandler: function (eventHandler) {
        this.eventHandlers.push(eventHandler);
    },

    removeSimpleEventHandler: function (eventHandler) {
        this.eventhandlers.remove(eventHandler);
    },

    clearSimpleEventHandlers: function () {
        this.eventHandlers.clear();
    },

    setSimpleEventHandler: function (eventHandler) {
        this.clearEventHandlers();
        this.eventHandlers.addEventHandler(eventHandler);
    },

    fire: function (context, anyOtherArguments) {
        var result;

        var context;

        if (arguments.length > 0) {
            context = arguments[0];
        }

        var argsToPassToEvents = Array.prototype.copy.call(arguments, 1);

        for (var i = 0; i < this.eventHandlers.length; i++) {
            // apply() function will execute the event handler, passing to 
            // it the context and an the arguments
            result = this.eventHandlers[i].apply(context, argsToPassToEvents);
        }

        return result;
    }
};

///#source 1 1 /Scripts/BPF/StringExtensions.js
// BPF JavaScript library version 0.9
// (c) Nick Polyak 2012 - http://awebpros.com/
// License: Code Project Open License (CPOL) 1.92(http://www.codeproject.com/info/cpol10.aspx)
//
// short overview of copyright rules:
// 1. you can use this framework in any commercial or non-commercial 
//    product as long as you retain this copyright message
// 2. Do not blame the author(s) of this software if something goes wrong. 
// 
// Also as a courtesy, please, mention this software in any documentation for the 
// products that use it.

/// <reference path="../qunit.js" />
/// <reference path="ArrayExtensions.js" />

String.prototype.startsWith = function (prefix) {
    if (prefix.length > this.length)
        return false;

    var subStr = this.substr(0, prefix.length);

    if (subStr === prefix)
        return true;

    return false;
}

String.prototype.removePrefix = function (prefix) {
    if (!this.startsWith(prefix))
        return this;

    return this.substr(prefix.length, this.length - prefix.length);
}

// will allow multiple to specify string delimiters and 
// will return the shortest substring up to one of these delimiters
// the callback will specify the remainder of the string and the chosen delimiter
// if no ending is found within the string, it will return the whole string
// and the remainder will be undefined or empty
String.prototype.getStrUpTo = function (strEndings, endCallback) {
    
    var endIdx = this.length;
    var foundStringEnding;

    var strEndingsArray = [];

    if (isObjectArray(strEndings))
        strEndingsArray = strEndings;
    else
        strEndingsArray.push(strEndings);

    for (var strEndingIdx = 0; strEndingIdx < strEndings.length; strEndingIdx++) {
        var currentStringEnding = strEndings[strEndingIdx];
        var foundIdx = this.indexOf(currentStringEnding);

        if (foundIdx < 0)
            continue;

        if (foundIdx <= endIdx) {
            endIdx = foundIdx;
            foundStringEnding = currentStringEnding;
        }
    }

    var result = this.substr(0, endIdx);

    if (foundStringEnding) {
        endIdx += foundStringEnding.length;
    }

    if (endCallback) {
        var stringRemainder = this.substr(endIdx, this.length - endIdx);

        endCallback(foundStringEnding, stringRemainder);
    }

    return result;
}


//test("testGetStrUpTo", function () {
//    var myStr = "Hello World";

//    var remainder;
//    var ending;
//    var result = myStr.getStrUpTo(/*["lo", "e", 'H']*/"q", function (chosenEnding, strRemainder) {
//        remainder = strRemainder;

//        ending = chosenEnding;
//    });
//    equal(result, "a");
//    equal(ending, "e");
//    equal(remainder, "llo World");
//    //console.log(ending + "     |    " + remainder);
//});

String.prototype.getStrBetween = function(beginStr, endStr, startIdx, endIdxCallback)  {
    if (!startIdx)
        startIdx = 0;

    var startResultIdx = this.indexOf(beginStr, startIdx);

    if (startResultIdx < 0)
    {
        if (endIdxCallback) {
            endIdxCallback(-1);
        }

        return;
    }

    startResultIdx += beginStr.length;

    var endResultIdx = this.indexOf(endStr, startResultIdx + 1);

    if (endResultIdx < 0) {
        endResultIdx = s.length;
    }

    var endIdx = endResultIdx + 1;

    var result = this.substr(startResultIdx, endResultIdx - startResultIdx);

    if (endIdxCallback) {
        endIdxCallback(endIdx);
    }

    return result;
}


//test("testGetStrBetween", function () {

//    var myStr = "hello world hello world";

//    //if (typeof myStr === 'string')
//    //    console.log("This is a string");

//    var endIdx = 0;

//    var endIdxCallBack = function (_endIdx) {
//        endIdx = _endIdx;
//    };

//    var result = myStr.getStrBetween("llo ", " he");

//    equal(result, "world");
//});

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Architect AWebPros
United States United States
I am a software architect and a developer with great passion for new engineering solutions and finding and applying design patterns.

I am passionate about learning new ways of building software and sharing my knowledge with others.

I worked with many various languages including C#, Java and C++.

I fell in love with WPF (and later Silverlight) at first sight. After Microsoft killed Silverlight, I was distraught until I found Avalonia - a great multiplatform package for building UI on Windows, Linux, Mac as well as within browsers (using WASM) and for mobile platforms.

I have my Ph.D. from RPI.

here is my linkedin profile

Comments and Discussions