<commit_before>steal('jquery/lang/observe', 'jquery/event/hashchange', 'jquery/lang/string/deparam', 'jquery/lang/observe/delegate',
function( $ ) {

    // Helper methods used for matching routes.
    //
	var // RegEx used to match route variables of the type ':name'.
        // Any word character or a period is matched.
        matcher = /\:([\w\.]+)/g,
        // Converts a JS Object into a list of parameters that can be
        // inserted into an html element tag.
		makeProps = function( props ) {
			var html = [],
				name, val;
			for ( name in props ) {
				val = props[name]
				if ( name === 'className' ) {
					name = 'class'
				}
				val && html.push(name, "=\"", escapeHTML(val), "\" ");
			}
			return html.join("")
		},
        // Escapes ' and " for safe insertion into html tag parameters.
		escapeHTML = function( content ) {
			return content.replace(/"/g, '"').replace(/'/g, "'");
		},
		// Checks if a route matches the data provided. If any route variable
        // is not present in the data the route does not match. If all route
        // variables are present in the data the number of matches is returned
        // to allow discerning between general and more specific routes.
		matchesData = function(route, data) {
			var count = 0;
			for ( var i = 0; i < route.names.length; i++ ) {
				if (!data.hasOwnProperty(route.names[i]) ) {
					return 0;
				}
				count++;
			}
			return count;
		},
        //
		onready = true;

	/**
	 * @class jQuery.route
	 * @inherits jQuery.Observe
	 * @plugin jquery/dom/route
	 * @parent dom
	 * @tag 3.2
	 *
	 * jQuery.route helps manage browser history (and
	 * client state) by
	 * synchronizing the window.location.hash with
	 * an [jQuery.Observe].
	 *
	 * ## Background Information
	 *
	 * To support the browser's back button and bookmarking
	 * in an Ajax application, most applications use
	 * the window.location.hash.  By
	 * changing the hash (via a link or JavaScript),
	 * one is able to add to the browser's history
	 * without changing the page.  The [jQuery.event.special.hashchange event] allows
	 * you to listen to when the hash is changed.
	 *
	 * Combined, this provides the basics needed to
	 * create history enabled Ajax websites.  However,
	 * jQuery.Route addresses several other needs such as:
	 *
	 *   - Pretty Routes
	 *   - Keeping routes independent of application code
	 *   - Listening to specific parts of the history changing
	 *   - Setup / Teardown of widgets.
	 *
	 * ## How it works
	 *
	 * $.route is a [jQuery.Observe $.Observe] that represents the
	 * window.location.hash as an
	 * object.  For example, if the hash looks like:
	 *
	 *     #!type=videos&id=5
	 *
	 * the data in $.route would look like:
	 *
	 *     { type: 'videos', id: 5 }
	 *
	 *
	 * $.route keeps the state of the hash in-sync with the data in
	 * $.route.
	 *
	 * ## $.Observe
	 *
	 * $.route is a [jQuery.Observe $.Observe]. Understanding
	 * $.Observe is essential for using $.route correctly.
	 *
	 * You can
	 * listen to changes in an Observe with bind and
	 * delegate and change $.route's properties with
	 * attr and attrs.
	 *
	 * ### Listening to changes in an Observable
	 *
	 * Listen to changes in history
	 * by [jQuery.Observe.prototype.bind bind]ing to
	 * changes in $.route like:
	 *
	 *     $.route.bind('change', function(ev, attr, how, newVal, oldVal) {
	 *
	 *     })
	 *
     *  - attr - the name of the changed attribute
     *  - how - the type of Observe change event (add, set or remove)
     *  - newVal/oldVal - the new and old values of the attribute
     *
	 * You can also listen to specific changes
	 * with [jQuery.Observe.prototype.delegate delegate]:
	 *
	 *     $.route.delegate('id','change', function(){ ... })
	 *
	 * Observe lets you listen to the following events:
	 *
	 *  - change - any change to the object
	 *  - add - a property is added
	 *  - set - a property value is added or changed
	 *  - remove - a property is removed
	 *
	 * Listening for add is useful for widget setup
	 * behavior, remove is useful for teardown.
	 *
	 * ### Updating an observable
	 *
	 * Create changes in the route data like:
	 *
	 *     $.route.attr('type','images');
	 *
	 * Or change multiple properties at once with
	 * [jQuery.Observe.prototype.attrs attrs]:
	 *
	 *     $.route.attrs({type: 'pages', id: 5}, true)
	 *
	 * When you make changes to $.route, they will automatically
	 * change the hash.
	 *
	 * ## Creating a Route
	 *
	 * Use $.route(url, defaults) to create a
	 * route. A route is a mapping from a url to
	 * an object (that is the $.route's state).
	 *
	 * If no routes are added, or no route is matched,
	 * $.route's data is updated with the [jQuery.String.deparam deparamed]
	 * hash.
	 *
	 *     location.hash = "#!type=videos";
	 *     // $.route -> {type : "videos"}
	 *
	 * Once routes are added and the hash changes,
	 * $.route looks for matching routes and uses them
	 * to update $.route's data.
	 *
	 *     $.route( "content/:type" );
	 *     location.hash = "#!content/images";
	 *     // $.route -> {type : "images"}
	 *
	 * Default values can also be added:
	 *
	 *     $.route("content/:type",{type: "videos" });
	 *     location.hash = "#!content/"
	 *     // $.route -> {type : "videos"}
	 *
	 * ## Delay setting $.route
	 *
	 * By default, $.route sets its initial data
	 * on document ready.  Sometimes, you want to wait to set
	 * this data.  To wait, call:
	 *
	 *     $.route.ready(false);
	 *
	 * and when ready, call:
	 *
	 *     $.route.ready(true);
	 *
	 * ## Changing the route.
	 *
	 * Typically, you never want to set location.hash
	 * directly.  Instead, you can change properties on $.route
	 * like:
	 *
	 *     $.route.attr('type', 'videos')
	 *
	 * This will automatically look up the appropriate
	 * route and update the hash.
	 *
	 * Often, you want to create links.  $.route provides
	 * the [jQuery.route.link] and [jQuery.route.url] helpers to make this
	 * easy:
	 *
	 *     $.route.link("Videos", {type: 'videos'})
	 *
	 * @param {String} url the fragment identifier to match.
	 * @param {Object} [defaults] an object of default values
	 * @return {jQuery.route}
	 */
	var $route = $.route = function( url, defaults ) {
        // Extract the variable names and replace with regEx that will match an atual URL with values.
		var names = [],
			test = url.replace(matcher, function( whole, name ) {
				names.push(name)
				return "([\\w\\.]*)"  // The '\\' is for string-escaping giving single '\' for regEx escaping
			});

		// Add route in a form that can be easily figured out
		$route.routes[url] = {
            // A regular expression that will match the route when variable values
            // are present; i.e. for :page/:type the regEx is /([\w\.]*)/([\w\.]*)/ which
            // will match for any value of :page and :type (word chars or period).
			test: new RegExp("^" + test),
            // The original URL, same as the index for this entry in routes.
			route: url,
            // An array of all the variable names in this route
			names: names,
            // Default values provided for the variables.
			defaults: defaults || {},
            // The number of parts in the URL separated by '/'.
			length: url.split('/').length
		}
		return $route;
	};

	$.extend($route, {
		/**
		 * Parameterizes the raw JS object representation provided in data.
		 * If a route matching the provided data is found that URL is built
         * from the data. Any remaining data is added at the end of the
         * URL as & separated key/value parameters.
		 *
		 * @param {Object} data
         * @return {String} The route URL and & separated parameters.
		 */
		param: function( data ) {
			// Check if the provided data keys matches the names in any routes;
			// get the one with the most matches.
			var route,
				matches = -1,
				temp,
				matchCount;
			for ( var name in $route.routes ) {
                temp = $route.routes[name],
                matchCount = matchesData(temp, data);
				if ( matchCount > matches ) {
					route = temp;
					matches = matchCount
				}
			}
			if ( route ) {
				var cpy = $.extend({}, data),
                    // Create the url by replacing the var names with the provided data.
                    // If the default value is found an empty string is inserted.
				    res = route.route.replace(matcher, function( whole, name ) {
                        delete cpy[name];
                        return data[name] === route.defaults[name] ? "" : data[name];
                    }),
                    // The remaining elements of data are added as $amp; separated parameters to the url.
				    after = $.param(cpy);
				return res + (after ? "&" + after : "")
			}
            // If no route was found there is no hash URL, only paramters.
			return $.param(data);
		},
		/**
		 * Populate the JS data object from a given URL.
		 *
		 * @param {Object} url
		 */
		deparam: function( url ) {
			// See if the url matches any routes by testing it against the route.test regEx.
            // By comparing the URL length the most specialized route that matches is used.
			var route = {
				length: -1
			};
			for ( var name in $route.routes ) {
				var temp = $route.routes[name]
				if ( temp.test.test(url) && temp.length > route.length ) {
					route = temp;
				}
			}
            // If a route was matched
			if ( route.length > -1 ) {
				var // Since RegEx backreferences are used in route.test (round brackets)
                    // the parts will contain the full matched string and each variable (backreferenced) value.
                    parts = url.match(route.test),
                    // start will contain the full mathced string; parts contain the variable values.
					start = parts.shift(),
                    // The remainder will be the & separated key/value pairs at the end of the URL.
					remainder = url.substr(start.length + 1),
                    // obj becomes the deparameterized JS object with values in remainder extended with the defaults.
					obj = $.extend(true, remainder ? $.String.deparam(remainder) : {}, route.defaults);

                // Overwrite each of the default values in obj with those in parts if that part is not empty.
				for ( var p = 0; p < parts.length; p++ ) {
					if ( parts[p] ) {
						obj[route.names[p]] = parts[p]
					}
				}
				return obj;
			}
            // If no route was matched it is parsed as a & separated list of key/value pairs.
			return $.String.deparam(url);
		},
		/**
		 * @hide
		 * A $.Observe that represents the state of the history.
		 */
		data: new $.Observe({}),
        /**
         * @attribute
         * @type Object
         * A list of routes recognized by the router indixed by the url used to add it.
         * Each route is an object with these members:
         *  - test - A regular expression that will match the route when variable values
         *      are present; i.e. for :page/:type the regEx is /([\w\.]*)/([\w\.]*)/ which
         *      will match for any value of :page and :type (word chars or period).
         *  - route - The original URL, same as the index for this entry in routes.
         *  - names - An array of all the variable names in this route
         *  - defaults - Default values provided for the variables or an empty object.
         *  - length - The number of parts in the URL separated by '/'.
         */
		routes: {},
		/**
		 * Indicates that all routes have been added and sets $.route.data
		 * based upon the routes and the current hash.
		 * @param {Boolean} [start]
		 * @return
		 */
		ready: function(val) {
			if( val === false ) {
				onready = false;
			}
			if( val === true || onready === true ) {
				setState();
			}
			return $route;
		},
		/**
		 * Returns a url from the options
		 * @param {Object} options
		 * @param {Boolean} merge true if the options should be merged with the current options
		 * @return {String}
		 */
		url: function( options, merge ) {
			//merges
			if (!merge ) {
				return "#!" + $route.param(options)
			} else {
				return "#!" + $route.param($.extend({}, curParams, options))
			}
		},
		/**
		 * Returns a link
		 * @param {Object} name The text of the link.
		 * @param {Object} options The route options (variables)
		 * @param {Object} props Properties of the <a> other than href.
		 */
		link: function( name, options, props ) {
			return "" + name + "";
		},
		/**
		 * Returns if the options represent the current page.
		 * @param {Object} options
		 */
		current: function( options ) {
			return window.location.hash == "#!" + $route.param(options)
		}
	});
	// onready
	$(function() {
		$.route.ready();
	});

    // The functions in the following list applied to $.route (e.g. $.route.attr('...')) will
    // instead act on the $.route.data Observe.
	$.each(['bind','unbind','delegate','undelegate','attr','attrs','removeAttr'], function(i, name){
		$route[name] = function(){
			return $route.data[name].apply($route.data, arguments)
		}
	})

	var throttle = function( func, time ) {
		var timer;
		return function() {
			clearTimeout(timer);
			timer = setTimeout(func, time || 1);
		}
	},
		curParams,
		setState = function() {

			var hash = window.location.hash.substr(2); // everything after #!
			//deparam it
			var props = $route.deparam(hash);
			curParams = props;
			$route.attrs(props, true);

		};

	// If the hash changes, update the $.route.data
	$(window).bind('hashchange', setState);

	// If the $.route.data changes, update the hash.
    // Using .serialize() retrieves the raw data contained in the observable.
    // This function is throttled.
	$route.data.bind("change", throttle(function() {
		window.location.hash = "#!" + $route.param($route.data.serialize())
	}));
})<commit_msg>dom/route: escapeHTML 'name' in makeProps as well.<commit_after>steal('jquery/lang/observe', 'jquery/event/hashchange', 'jquery/lang/string/deparam', 'jquery/lang/observe/delegate',
function( $ ) {

    // Helper methods used for matching routes.
    //
	var // RegEx used to match route variables of the type ':name'.
        // Any word character or a period is matched.
        matcher = /\:([\w\.]+)/g,
        // Converts a JS Object into a list of parameters that can be
        // inserted into an html element tag.
		makeProps = function( props ) {
			var html = [],
				name, val;
			for ( name in props ) {
				val = props[name]
				if ( name === 'className' ) {
					name = 'class'
				}
				val && html.push(escapeHTML(name), "=\"", escapeHTML(val), "\" ");
			}
			return html.join("")
		},
        // Escapes ' and " for safe insertion into html tag parameters.
		escapeHTML = function( content ) {
			return content.replace(/"/g, '"').replace(/'/g, "'");
		},
		// Checks if a route matches the data provided. If any route variable
        // is not present in the data the route does not match. If all route
        // variables are present in the data the number of matches is returned
        // to allow discerning between general and more specific routes.
		matchesData = function(route, data) {
			var count = 0;
			for ( var i = 0; i < route.names.length; i++ ) {
				if (!data.hasOwnProperty(route.names[i]) ) {
					return 0;
				}
				count++;
			}
			return count;
		},
        //
		onready = true;

	/**
	 * @class jQuery.route
	 * @inherits jQuery.Observe
	 * @plugin jquery/dom/route
	 * @parent dom
	 * @tag 3.2
	 *
	 * jQuery.route helps manage browser history (and
	 * client state) by
	 * synchronizing the window.location.hash with
	 * an [jQuery.Observe].
	 *
	 * ## Background Information
	 *
	 * To support the browser's back button and bookmarking
	 * in an Ajax application, most applications use
	 * the window.location.hash.  By
	 * changing the hash (via a link or JavaScript),
	 * one is able to add to the browser's history
	 * without changing the page.  The [jQuery.event.special.hashchange event] allows
	 * you to listen to when the hash is changed.
	 *
	 * Combined, this provides the basics needed to
	 * create history enabled Ajax websites.  However,
	 * jQuery.Route addresses several other needs such as:
	 *
	 *   - Pretty Routes
	 *   - Keeping routes independent of application code
	 *   - Listening to specific parts of the history changing
	 *   - Setup / Teardown of widgets.
	 *
	 * ## How it works
	 *
	 * $.route is a [jQuery.Observe $.Observe] that represents the
	 * window.location.hash as an
	 * object.  For example, if the hash looks like:
	 *
	 *     #!type=videos&id=5
	 *
	 * the data in $.route would look like:
	 *
	 *     { type: 'videos', id: 5 }
	 *
	 *
	 * $.route keeps the state of the hash in-sync with the data in
	 * $.route.
	 *
	 * ## $.Observe
	 *
	 * $.route is a [jQuery.Observe $.Observe]. Understanding
	 * $.Observe is essential for using $.route correctly.
	 *
	 * You can
	 * listen to changes in an Observe with bind and
	 * delegate and change $.route's properties with
	 * attr and attrs.
	 *
	 * ### Listening to changes in an Observable
	 *
	 * Listen to changes in history
	 * by [jQuery.Observe.prototype.bind bind]ing to
	 * changes in $.route like:
	 *
	 *     $.route.bind('change', function(ev, attr, how, newVal, oldVal) {
	 *
	 *     })
	 *
     *  - attr - the name of the changed attribute
     *  - how - the type of Observe change event (add, set or remove)
     *  - newVal/oldVal - the new and old values of the attribute
     *
	 * You can also listen to specific changes
	 * with [jQuery.Observe.prototype.delegate delegate]:
	 *
	 *     $.route.delegate('id','change', function(){ ... })
	 *
	 * Observe lets you listen to the following events:
	 *
	 *  - change - any change to the object
	 *  - add - a property is added
	 *  - set - a property value is added or changed
	 *  - remove - a property is removed
	 *
	 * Listening for add is useful for widget setup
	 * behavior, remove is useful for teardown.
	 *
	 * ### Updating an observable
	 *
	 * Create changes in the route data like:
	 *
	 *     $.route.attr('type','images');
	 *
	 * Or change multiple properties at once with
	 * [jQuery.Observe.prototype.attrs attrs]:
	 *
	 *     $.route.attrs({type: 'pages', id: 5}, true)
	 *
	 * When you make changes to $.route, they will automatically
	 * change the hash.
	 *
	 * ## Creating a Route
	 *
	 * Use $.route(url, defaults) to create a
	 * route. A route is a mapping from a url to
	 * an object (that is the $.route's state).
	 *
	 * If no routes are added, or no route is matched,
	 * $.route's data is updated with the [jQuery.String.deparam deparamed]
	 * hash.
	 *
	 *     location.hash = "#!type=videos";
	 *     // $.route -> {type : "videos"}
	 *
	 * Once routes are added and the hash changes,
	 * $.route looks for matching routes and uses them
	 * to update $.route's data.
	 *
	 *     $.route( "content/:type" );
	 *     location.hash = "#!content/images";
	 *     // $.route -> {type : "images"}
	 *
	 * Default values can also be added:
	 *
	 *     $.route("content/:type",{type: "videos" });
	 *     location.hash = "#!content/"
	 *     // $.route -> {type : "videos"}
	 *
	 * ## Delay setting $.route
	 *
	 * By default, $.route sets its initial data
	 * on document ready.  Sometimes, you want to wait to set
	 * this data.  To wait, call:
	 *
	 *     $.route.ready(false);
	 *
	 * and when ready, call:
	 *
	 *     $.route.ready(true);
	 *
	 * ## Changing the route.
	 *
	 * Typically, you never want to set location.hash
	 * directly.  Instead, you can change properties on $.route
	 * like:
	 *
	 *     $.route.attr('type', 'videos')
	 *
	 * This will automatically look up the appropriate
	 * route and update the hash.
	 *
	 * Often, you want to create links.  $.route provides
	 * the [jQuery.route.link] and [jQuery.route.url] helpers to make this
	 * easy:
	 *
	 *     $.route.link("Videos", {type: 'videos'})
	 *
	 * @param {String} url the fragment identifier to match.
	 * @param {Object} [defaults] an object of default values
	 * @return {jQuery.route}
	 */
	var $route = $.route = function( url, defaults ) {
        // Extract the variable names and replace with regEx that will match an atual URL with values.
		var names = [],
			test = url.replace(matcher, function( whole, name ) {
				names.push(name)
				return "([\\w\\.]*)"  // The '\\' is for string-escaping giving single '\' for regEx escaping
			});

		// Add route in a form that can be easily figured out
		$route.routes[url] = {
            // A regular expression that will match the route when variable values
            // are present; i.e. for :page/:type the regEx is /([\w\.]*)/([\w\.]*)/ which
            // will match for any value of :page and :type (word chars or period).
			test: new RegExp("^" + test),
            // The original URL, same as the index for this entry in routes.
			route: url,
            // An array of all the variable names in this route
			names: names,
            // Default values provided for the variables.
			defaults: defaults || {},
            // The number of parts in the URL separated by '/'.
			length: url.split('/').length
		}
		return $route;
	};

	$.extend($route, {
		/**
		 * Parameterizes the raw JS object representation provided in data.
		 * If a route matching the provided data is found that URL is built
         * from the data. Any remaining data is added at the end of the
         * URL as & separated key/value parameters.
		 *
		 * @param {Object} data
         * @return {String} The route URL and & separated parameters.
		 */
		param: function( data ) {
			// Check if the provided data keys matches the names in any routes;
			// get the one with the most matches.
			var route,
				matches = -1,
				temp,
				matchCount;
			for ( var name in $route.routes ) {
                temp = $route.routes[name],
                matchCount = matchesData(temp, data);
				if ( matchCount > matches ) {
					route = temp;
					matches = matchCount
				}
			}
			if ( route ) {
				var cpy = $.extend({}, data),
                    // Create the url by replacing the var names with the provided data.
                    // If the default value is found an empty string is inserted.
				    res = route.route.replace(matcher, function( whole, name ) {
                        delete cpy[name];
                        return data[name] === route.defaults[name] ? "" : data[name];
                    }),
                    // The remaining elements of data are added as $amp; separated parameters to the url.
				    after = $.param(cpy);
				return res + (after ? "&" + after : "")
			}
            // If no route was found there is no hash URL, only paramters.
			return $.param(data);
		},
		/**
		 * Populate the JS data object from a given URL.
		 *
		 * @param {Object} url
		 */
		deparam: function( url ) {
			// See if the url matches any routes by testing it against the route.test regEx.
            // By comparing the URL length the most specialized route that matches is used.
			var route = {
				length: -1
			};
			for ( var name in $route.routes ) {
				var temp = $route.routes[name]
				if ( temp.test.test(url) && temp.length > route.length ) {
					route = temp;
				}
			}
            // If a route was matched
			if ( route.length > -1 ) {
				var // Since RegEx backreferences are used in route.test (round brackets)
                    // the parts will contain the full matched string and each variable (backreferenced) value.
                    parts = url.match(route.test),
                    // start will contain the full mathced string; parts contain the variable values.
					start = parts.shift(),
                    // The remainder will be the & separated key/value pairs at the end of the URL.
					remainder = url.substr(start.length + 1),
                    // obj becomes the deparameterized JS object with values in remainder extended with the defaults.
					obj = $.extend(true, remainder ? $.String.deparam(remainder) : {}, route.defaults);

                // Overwrite each of the default values in obj with those in parts if that part is not empty.
				for ( var p = 0; p < parts.length; p++ ) {
					if ( parts[p] ) {
						obj[route.names[p]] = parts[p]
					}
				}
				return obj;
			}
            // If no route was matched it is parsed as a & separated list of key/value pairs.
			return $.String.deparam(url);
		},
		/**
		 * @hide
		 * A $.Observe that represents the state of the history.
		 */
		data: new $.Observe({}),
        /**
         * @attribute
         * @type Object
         * A list of routes recognized by the router indixed by the url used to add it.
         * Each route is an object with these members:
         *  - test - A regular expression that will match the route when variable values
         *      are present; i.e. for :page/:type the regEx is /([\w\.]*)/([\w\.]*)/ which
         *      will match for any value of :page and :type (word chars or period).
         *  - route - The original URL, same as the index for this entry in routes.
         *  - names - An array of all the variable names in this route
         *  - defaults - Default values provided for the variables or an empty object.
         *  - length - The number of parts in the URL separated by '/'.
         */
		routes: {},
		/**
		 * Indicates that all routes have been added and sets $.route.data
		 * based upon the routes and the current hash.
		 * @param {Boolean} [start]
		 * @return
		 */
		ready: function(val) {
			if( val === false ) {
				onready = false;
			}
			if( val === true || onready === true ) {
				setState();
			}
			return $route;
		},
		/**
		 * Returns a url from the options
		 * @param {Object} options
		 * @param {Boolean} merge true if the options should be merged with the current options
		 * @return {String}
		 */
		url: function( options, merge ) {
			//merges
			if (!merge ) {
				return "#!" + $route.param(options)
			} else {
				return "#!" + $route.param($.extend({}, curParams, options))
			}
		},
		/**
		 * Returns a link
		 * @param {Object} name The text of the link.
		 * @param {Object} options The route options (variables)
		 * @param {Object} props Properties of the <a> other than href.
		 */
		link: function( name, options, props ) {
			return "" + name + "";
		},
		/**
		 * Returns if the options represent the current page.
		 * @param {Object} options
		 */
		current: function( options ) {
			return window.location.hash == "#!" + $route.param(options)
		}
	});
	// onready
	$(function() {
		$.route.ready();
	});

    // The functions in the following list applied to $.route (e.g. $.route.attr('...')) will
    // instead act on the $.route.data Observe.
	$.each(['bind','unbind','delegate','undelegate','attr','attrs','removeAttr'], function(i, name){
		$route[name] = function(){
			return $route.data[name].apply($route.data, arguments)
		}
	})

	var throttle = function( func, time ) {
		var timer;
		return function() {
			clearTimeout(timer);
			timer = setTimeout(func, time || 1);
		}
	},
		curParams,
		setState = function() {

			var hash = window.location.hash.substr(2); // everything after #!
			//deparam it
			var props = $route.deparam(hash);
			curParams = props;
			$route.attrs(props, true);

		};

	// If the hash changes, update the $.route.data
	$(window).bind('hashchange', setState);

	// If the $.route.data changes, update the hash.
    // Using .serialize() retrieves the raw data contained in the observable.
    // This function is throttled.
	$route.data.bind("change", throttle(function() {
		window.location.hash = "#!" + $route.param($route.data.serialize())
	}));
})<|endoftext|>


<commit_before>var url = "https://github.com/toggl-open-source/toggldesktop/releases/download/v",
apiUrl = "https://api.github.com/repos/toggl-open-source/toggldesktop/releases/tags/v",
releases = {},
ghReleases = {},
defaultPath = "/toggldesktop/installers",
downloadPath = "/toggldesktop/download",
releasesPath = "/assets/releases/releases.json",
osUrl = "https://toggl-open-source.github.io/toggldesktop/",
links = {
  "windows": "win",
  "windows_enterprise": "win-enterprise",
  "linux_tar.gz": "linux",
  "linux_deb64": "linux-deb64"
};

//We need a function which handles requests and send response
function load(){
  var location = window.location.pathname.replace(defaultPath, ""),
    hash = window.location.hash;
  try {
    if (location == "/") {
      fillHtml();
      if (!!hash) {
        hash = hash.replace("#", "/");
        findRedirect(hash);
      }
    } else {
      msg = "
Unknown path url: " + window.location.href + "
";
      showMessage(msg);
    }
  } catch (e) {
    msg = "
Error occurred on url: " + window.location.href + "

Error message:
" + e + "
";
    showMessage(msg);
  }
}

function findRedirect(location) {
  var channel, ver, ok, msg;
  if (location.match("/win/")) {
      channel = location.match(/win\/(.*)/)[1].replace("/", "");
      ver = releases["win"][channel];
      ver = ver[ver.length-1];
      redirect(url + ver.version + "/" + ver.filename[0].name);
    } else if (location.match("/linux/")) {
      channel = location.match(/linux\/(.*)/)[1].replace("/");
      ver = releases["linux"][channel];
      ver = ver[ver.length-1];
      redirect(url + ver.version + "/" + ver.filename[0].name);
    } else if (location.match("/macos")) {
      channel = location.match(/macos\/(.*)/)[1].replace("/", "");
      ver = releases["osx"][channel];
      ver = ver[ver.length-1];
      redirect(url + ver.version + "/" + ver.filename[0].name);
    } else if (location.match("/win-enterprise")) {
      channel = location.match(/win-enterprise\/(.*)/)[1].replace("/", "");
      ver = releases["win"][channel];
      ver = ver[ver.length-1];
      redirect(url + ver.version + "/" + ver.filename[1].name);
    } else if (location.match("/linux-deb64")) {
      channel = location.match(/linux-deb64\/(.*)/)[1].replace("/", "");
      ver = releases["linux"][channel];
      ver = ver[ver.length-1];
      redirect(url + ver.version + "/" + ver.filename[1].name);
    } else if (location.match("/status")) {
      ok = !!releases;
      if (ok) {
        console.log("OK");
        msg = "OK";
        showMessage(msg);
      } else {
        msg = "
Error: Something is wrong (releases data not loaded)
";
        showMessage(msg);
      }
    }
}

function redirect(url) {
  window.location.assign(url);
}

function fillHtml() {
  var lines = [], os, ch, cur, prev, tmp, link;
  for (var k in releases) {
    // win, linux, mac
    os = releases[k];
    for (var ka in os) {
      // dev, beta, stable
      ch = os[ka];
      if (!ch.length) {
        continue;
      }
      // 7.3.0 ...
      cur = ch[ch.length-1];
      if (!ghReleases[cur.version]) ghReleases[cur.version] = {};

      for (var kb in cur.filename) {
        prev = ch[ch.length-2];
        if (!prev || !prev.filename[kb]) {
          prev = cur;
        }
        if (!ghReleases[prev.version]) ghReleases[prev.version] = {};

        link = downloadPath + "/" + cur.filename[kb].type + "-" + ka + "/";
        ghReleases[cur.version][cur.filename[kb].name] = 1;
        ghReleases[prev.version][prev.filename[kb].name] = 1;
        tmp = "
" +
          "" + cur.filename[kb].type + "	" +
          "" + ka + "	" +
          "" + cur.version + "	" +
          "	" +
          "" + prev.version + "	" +
          "	" +
          "link
";

        lines.push(tmp);
      }
    }
  }

  document.querySelector("tbody").innerHTML = lines.join("");
  for (let version in ghReleases) {
    let files = ghReleases[version];
    let request = new XMLHttpRequest();
    request.open('GET', apiUrl + version, true);
    request.responseType = 'json';
    request.onload = onReqLoad(version, files);
    request.send();
  }
  return;
}
function onReqLoad(ver, fls){
  return function(){
    if (this.status == 200){
      let release = this.response;
      for (let file in fls) {
        let asset = release.assets.find(a => a.name == file);
        setDownloadCount(ver, file, !!asset ? asset.download_count : '-');
      }
      loadSorttable();
    }
  }
}
function setDownloadCount(ver, file, downloadCount){
  var els = document.getElementsByClassName(ver+"_"+file);
  for (var i = 0; i < els.length; i++){
    els[i].innerHTML = downloadCount;
  }
}

function showMessage(msg) {
  document.querySelector(".message").innerHTML = msg;
}

function loadSorttable() {
  let fileref = document.createElement('script');
  fileref.setAttribute("type","text/javascript");
  fileref.setAttribute("src", "sorttable.js");
  document.getElementsByTagName("head")[0].appendChild(fileref)
}

function readJSON(path) {
    var xhr = new XMLHttpRequest();
    xhr.open('GET', osUrl + path, true);
    xhr.responseType = 'blob';
    xhr.onload = function(e) {
      if (this.status == 200) {
          var file = new File([this.response], 'temp');
          var fileReader = new FileReader();
          fileReader.addEventListener('load', function(){
               releases = JSON.parse(fileReader.result);
               load();
          });
          fileReader.readAsText(file);
      } else {
        showError()
      }
    }
    xhr.send();
}

readJSON(releasesPath);
<commit_msg>improved the error message<commit_after>var url = "https://github.com/toggl-open-source/toggldesktop/releases/download/v",
apiUrl = "https://api.github.com/repos/toggl-open-source/toggldesktop/releases/tags/v",
releases = {},
ghReleases = {},
defaultPath = "/toggldesktop/installers",
downloadPath = "/toggldesktop/download",
releasesPath = "/assets/releases/releases.json",
osUrl = "https://toggl-open-source.github.io/toggldesktop/",
links = {
  "windows": "win",
  "windows_enterprise": "win-enterprise",
  "linux_tar.gz": "linux",
  "linux_deb64": "linux-deb64"
};

//We need a function which handles requests and send response
function load(){
  var location = window.location.pathname.replace(defaultPath, ""),
    hash = window.location.hash;
  try {
    if (location == "/") {
      fillHtml();
      if (!!hash) {
        hash = hash.replace("#", "/");
        findRedirect(hash);
      }
    } else {
      msg = "
Unknown path
Url: " + window.location.href + "
Path: " + location + "
";
      showMessage(msg);
    }
  } catch (e) {
    msg = "
Error occurred on url: " + window.location.href + "

Error message:
" + e + "
";
    showMessage(msg);
  }
}

function findRedirect(location) {
  var channel, ver, ok, msg;
  if (location.match("/win/")) {
      channel = location.match(/win\/(.*)/)[1].replace("/", "");
      ver = releases["win"][channel];
      ver = ver[ver.length-1];
      redirect(url + ver.version + "/" + ver.filename[0].name);
    } else if (location.match("/linux/")) {
      channel = location.match(/linux\/(.*)/)[1].replace("/");
      ver = releases["linux"][channel];
      ver = ver[ver.length-1];
      redirect(url + ver.version + "/" + ver.filename[0].name);
    } else if (location.match("/macos")) {
      channel = location.match(/macos\/(.*)/)[1].replace("/", "");
      ver = releases["osx"][channel];
      ver = ver[ver.length-1];
      redirect(url + ver.version + "/" + ver.filename[0].name);
    } else if (location.match("/win-enterprise")) {
      channel = location.match(/win-enterprise\/(.*)/)[1].replace("/", "");
      ver = releases["win"][channel];
      ver = ver[ver.length-1];
      redirect(url + ver.version + "/" + ver.filename[1].name);
    } else if (location.match("/linux-deb64")) {
      channel = location.match(/linux-deb64\/(.*)/)[1].replace("/", "");
      ver = releases["linux"][channel];
      ver = ver[ver.length-1];
      redirect(url + ver.version + "/" + ver.filename[1].name);
    } else if (location.match("/status")) {
      ok = !!releases;
      if (ok) {
        console.log("OK");
        msg = "OK";
        showMessage(msg);
      } else {
        msg = "
Error: Something is wrong (releases data not loaded)
";
        showMessage(msg);
      }
    }
}

function redirect(url) {
  window.location.assign(url);
}

function fillHtml() {
  var lines = [], os, ch, cur, prev, tmp, link;
  for (var k in releases) {
    // win, linux, mac
    os = releases[k];
    for (var ka in os) {
      // dev, beta, stable
      ch = os[ka];
      if (!ch.length) {
        continue;
      }
      // 7.3.0 ...
      cur = ch[ch.length-1];
      if (!ghReleases[cur.version]) ghReleases[cur.version] = {};

      for (var kb in cur.filename) {
        prev = ch[ch.length-2];
        if (!prev || !prev.filename[kb]) {
          prev = cur;
        }
        if (!ghReleases[prev.version]) ghReleases[prev.version] = {};

        link = downloadPath + "/" + cur.filename[kb].type + "-" + ka + "/";
        ghReleases[cur.version][cur.filename[kb].name] = 1;
        ghReleases[prev.version][prev.filename[kb].name] = 1;
        tmp = "
" +
          "" + cur.filename[kb].type + "	" +
          "" + ka + "	" +
          "" + cur.version + "	" +
          "	" +
          "" + prev.version + "	" +
          "	" +
          "link
";

        lines.push(tmp);
      }
    }
  }

  document.querySelector("tbody").innerHTML = lines.join("");
  for (let version in ghReleases) {
    let files = ghReleases[version];
    let request = new XMLHttpRequest();
    request.open('GET', apiUrl + version, true);
    request.responseType = 'json';
    request.onload = onReqLoad(version, files);
    request.send();
  }
  return;
}
function onReqLoad(ver, fls){
  return function(){
    if (this.status == 200){
      let release = this.response;
      for (let file in fls) {
        let asset = release.assets.find(a => a.name == file);
        setDownloadCount(ver, file, !!asset ? asset.download_count : '-');
      }
      loadSorttable();
    }
  }
}
function setDownloadCount(ver, file, downloadCount){
  var els = document.getElementsByClassName(ver+"_"+file);
  for (var i = 0; i < els.length; i++){
    els[i].innerHTML = downloadCount;
  }
}

function showMessage(msg) {
  document.querySelector(".message").innerHTML = msg;
}

function loadSorttable() {
  let fileref = document.createElement('script');
  fileref.setAttribute("type","text/javascript");
  fileref.setAttribute("src", "sorttable.js");
  document.getElementsByTagName("head")[0].appendChild(fileref)
}

function readJSON(path) {
    var xhr = new XMLHttpRequest();
    xhr.open('GET', osUrl + path, true);
    xhr.responseType = 'blob';
    xhr.onload = function(e) {
      if (this.status == 200) {
          var file = new File([this.response], 'temp');
          var fileReader = new FileReader();
          fileReader.addEventListener('load', function(){
               releases = JSON.parse(fileReader.result);
               load();
          });
          fileReader.readAsText(file);
      } else {
        showError()
      }
    }
    xhr.send();
}

readJSON(releasesPath);
<|endoftext|>


<commit_before>var Store;
(function (Store) {
    var underscore = angular.module('Common', []).factory('underscore', [
        '$window',
        function ($window) { return $window._; }
    ]);
})(Store || (Store = {}));
//# sourceMappingURL=UnderscoreModule.js.map<commit_msg>delete<commit_after><|endoftext|>


<commit_before>'use strict';

var eIndexOf           = require('es5-ext/array/#/e-index-of')
  , i                  = require('es5-ext/function/i')
  , invoke             = require('es5-ext/function/invoke')
  , d                  = require('d/d')
  , memoize            = require('memoizee/lib/regular')
  , memMethods         = require('memoizee/lib/d')(memoize)
  , ReadOnly           = require('observable-set/create-read-only')(
	require('observable-set')
)
  , validObservableMap = require('./valid-observable-map')

  , defineProperties = Object.defineProperties
  , invokeDispose = invoke('_dispose');

require('memoizee/lib/ext/ref-counter');
require('memoizee/lib/ext/resolvers');
require('memoizee/lib/ext/dispose');

module.exports = memoize(function (prototype) {
	validObservableMap(prototype);

	return defineProperties(prototype, memMethods({
		toSet: d(function (kind) {
			var result, disposed, listener, registry, inClear;
			result = new ReadOnly((kind === 'value') ? this.values() : this.keys());
			if (kind === 'value') {
				registry = memoize(i, { refCounter: true, dispose: function (val) {
					if (inClear) return;
					result._delete(val);
				} });
				result.forEach(registry);
				this.on('change', listener = function (event) {
					var type = event.type, valid;
					if (type === 'set') {
						if (event.hasOwnProperty('oldValue')) {
							registry.clearRef(event.oldValue);
						}
						result._add(registry(event.value));
						return;
					}
					if (type === 'delete') {
						registry.clearRef(event.value);
						return;
					}
					if (type === 'clear') {
						inClear = true;
						registry.clearAll();
						inClear = false;
						result._clear();
						return;
					}
					result._postponed_ += 1;
					if (type === 'batch') {
						if (event.set) {
							event.set.forEach(function (value) {
								result._add(registry(value));
							});
						}
						if (event.deleted) {
							event.deleted.forEach(function (value) {
								registry.clearRef(value);
							});
						}
					} else {
						inClear = true;
						registry.clearAll();
						inClear = false;
						valid = [];
						this.forEach(function (value, key) {
							valid.push(registry(value));
							if (result.has(value)) return;
							result._add(value);
						});
						result.forEach(function (value) {
							if (eIndexOf.call(valid, value)) return;
							result._delete(value);
						});
					}
					result._postponed_ -= 1;
				});
			} else {
				this.on('change', listener = function (event) {
					var type = event.type;
					if (type === 'set') {
						result._add(event.key);
						return;
					}
					if (type === 'delete') {
						result._delete(event.key);
						return;
					}
					if (type === 'clear') {
						result._clear();
						return;
					}
					result._postponed_ += 1;
					if (type === 'batch') {
						if (event.set) {
							event.set.forEach(function (value, key) {
								result._add(key);
							});
						}
						if (event.deleted) {
							event.deleted.forEach(function (value, key) {
								if (event.set && event.set.has(key)) return;
								result._delete(key);
							});
						}
					} else {
						this.forEach(function (value, key) {
							if (result.has(key)) return;
							result._add(key);
						});
						result.forEach(function (value) {
							if (this.has(value)) return;
							result._delete(value);
						}, this);
					}
					result._postponed_ -= 1;
				});
			}
			defineProperties(result, {
				unref: d(function () {
					if (disposed) return;
					this.toSet.clearRef(kind);
				}.bind(this)),
				_dispose: d(function () {
					this.off('change', listener);
					if (registry) registry.clearAll();
					disposed = true;
				}.bind(this))
			});
			return result;
		}, { resolvers: [function (kind) {
			return (String(kind) === 'key') ? 'key' : 'value';
		}], refCounter: true, dispose: invokeDispose })
	}));
});
<commit_msg>Update up to changes in es5-ext<commit_after>'use strict';

var eIndexOf           = require('es5-ext/array/#/e-index-of')
  , identity           = require('es5-ext/function/identity')
  , invoke             = require('es5-ext/function/invoke')
  , d                  = require('d/d')
  , memoize            = require('memoizee/lib/regular')
  , memMethods         = require('memoizee/lib/d')(memoize)
  , ReadOnly           = require('observable-set/create-read-only')(
	require('observable-set')
)
  , validObservableMap = require('./valid-observable-map')

  , defineProperties = Object.defineProperties
  , invokeDispose = invoke('_dispose');

require('memoizee/lib/ext/ref-counter');
require('memoizee/lib/ext/resolvers');
require('memoizee/lib/ext/dispose');

module.exports = memoize(function (prototype) {
	validObservableMap(prototype);

	return defineProperties(prototype, memMethods({
		toSet: d(function (kind) {
			var result, disposed, listener, registry, inClear;
			result = new ReadOnly((kind === 'value') ? this.values() : this.keys());
			if (kind === 'value') {
				registry = memoize(identity, {
					refCounter: true,
					dispose: function (val) {
						if (inClear) return;
						result._delete(val);
					}
				});
				result.forEach(registry);
				this.on('change', listener = function (event) {
					var type = event.type, valid;
					if (type === 'set') {
						if (event.hasOwnProperty('oldValue')) {
							registry.clearRef(event.oldValue);
						}
						result._add(registry(event.value));
						return;
					}
					if (type === 'delete') {
						registry.clearRef(event.value);
						return;
					}
					if (type === 'clear') {
						inClear = true;
						registry.clearAll();
						inClear = false;
						result._clear();
						return;
					}
					result._postponed_ += 1;
					if (type === 'batch') {
						if (event.set) {
							event.set.forEach(function (value) {
								result._add(registry(value));
							});
						}
						if (event.deleted) {
							event.deleted.forEach(function (value) {
								registry.clearRef(value);
							});
						}
					} else {
						inClear = true;
						registry.clearAll();
						inClear = false;
						valid = [];
						this.forEach(function (value, key) {
							valid.push(registry(value));
							if (result.has(value)) return;
							result._add(value);
						});
						result.forEach(function (value) {
							if (eIndexOf.call(valid, value)) return;
							result._delete(value);
						});
					}
					result._postponed_ -= 1;
				});
			} else {
				this.on('change', listener = function (event) {
					var type = event.type;
					if (type === 'set') {
						result._add(event.key);
						return;
					}
					if (type === 'delete') {
						result._delete(event.key);
						return;
					}
					if (type === 'clear') {
						result._clear();
						return;
					}
					result._postponed_ += 1;
					if (type === 'batch') {
						if (event.set) {
							event.set.forEach(function (value, key) {
								result._add(key);
							});
						}
						if (event.deleted) {
							event.deleted.forEach(function (value, key) {
								if (event.set && event.set.has(key)) return;
								result._delete(key);
							});
						}
					} else {
						this.forEach(function (value, key) {
							if (result.has(key)) return;
							result._add(key);
						});
						result.forEach(function (value) {
							if (this.has(value)) return;
							result._delete(value);
						}, this);
					}
					result._postponed_ -= 1;
				});
			}
			defineProperties(result, {
				unref: d(function () {
					if (disposed) return;
					this.toSet.clearRef(kind);
				}.bind(this)),
				_dispose: d(function () {
					this.off('change', listener);
					if (registry) registry.clearAll();
					disposed = true;
				}.bind(this))
			});
			return result;
		}, { resolvers: [function (kind) {
			return (String(kind) === 'key') ? 'key' : 'value';
		}], refCounter: true, dispose: invokeDispose })
	}));
});
<|endoftext|>


<commit_before>import { NODE_FIELDS } from "../definitions";

const has = Function.call.bind(Object.prototype.hasOwnProperty);

function cloneIfNode(obj, deep) {
  if (
    obj &&
    typeof obj.type === "string" &&
    // CommentLine and CommentBlock are used in File#comments, but they are
    // not defined in babel-types
    obj.type !== "CommentLine" &&
    obj.type !== "CommentBlock"
  ) {
    return cloneNode(obj, deep);
  }

  return obj;
}

function cloneIfNodeOrArray(obj, deep) {
  if (Array.isArray(obj)) {
    return obj.map(node => cloneIfNode(node, deep));
  }
  return cloneIfNode(obj, deep);
}

/**
 * Create a clone of a `node` including only properties belonging to the node.
 * If the second parameter is `false`, cloneNode performs a shallow clone.
 */
export default function cloneNode<T: Object>(node: T, deep: boolean = true): T {
  if (!node) return node;

  const { type } = node;
  const newNode = (({ type }: any): T);

  // Special-case identifiers since they are the most cloned nodes.
  if (type === "Identifier") {
    newNode.name = node.name;

    if (has(node, "typeAnnotation")) {
      newNode.typeAnnotation = deep
        ? cloneIfNodeOrArray(node.typeAnnotation, true)
        : node.typeAnnotation;
    }
  } else if (!has(NODE_FIELDS, type)) {
    throw new Error(`Unknown node type: "${type}"`);
  } else {
    for (const field of Object.keys(NODE_FIELDS[type])) {
      if (has(node, field)) {
        newNode[field] = deep
          ? cloneIfNodeOrArray(node[field], true)
          : node[field];
      }
    }
  }

  if (has(node, "loc")) {
    newNode.loc = node.loc;
  }
  if (has(node, "leadingComments")) {
    newNode.leadingComments = node.leadingComments;
  }
  if (has(node, "innerComments")) {
    newNode.innerComments = node.innerCmments;
  }
  if (has(node, "trailingComments")) {
    newNode.trailingComments = node.trailingComments;
  }
  if (has(node, "extra")) {
    newNode.extra = {
      ...node.extra,
    };
  }

  return newNode;
}
<commit_msg>Copy "optional" property when cloning Identifier node (#9333) <commit_after>import { NODE_FIELDS } from "../definitions";

const has = Function.call.bind(Object.prototype.hasOwnProperty);

function cloneIfNode(obj, deep) {
  if (
    obj &&
    typeof obj.type === "string" &&
    // CommentLine and CommentBlock are used in File#comments, but they are
    // not defined in babel-types
    obj.type !== "CommentLine" &&
    obj.type !== "CommentBlock"
  ) {
    return cloneNode(obj, deep);
  }

  return obj;
}

function cloneIfNodeOrArray(obj, deep) {
  if (Array.isArray(obj)) {
    return obj.map(node => cloneIfNode(node, deep));
  }
  return cloneIfNode(obj, deep);
}

/**
 * Create a clone of a `node` including only properties belonging to the node.
 * If the second parameter is `false`, cloneNode performs a shallow clone.
 */
export default function cloneNode<T: Object>(node: T, deep: boolean = true): T {
  if (!node) return node;

  const { type } = node;
  const newNode = (({ type }: any): T);

  // Special-case identifiers since they are the most cloned nodes.
  if (type === "Identifier") {
    newNode.name = node.name;

    if (has(node, "optional") && typeof node.optional === "boolean") {
      newNode.optional = node.optional;
    }

    if (has(node, "typeAnnotation")) {
      newNode.typeAnnotation = deep
        ? cloneIfNodeOrArray(node.typeAnnotation, true)
        : node.typeAnnotation;
    }
  } else if (!has(NODE_FIELDS, type)) {
    throw new Error(`Unknown node type: "${type}"`);
  } else {
    for (const field of Object.keys(NODE_FIELDS[type])) {
      if (has(node, field)) {
        newNode[field] = deep
          ? cloneIfNodeOrArray(node[field], true)
          : node[field];
      }
    }
  }

  if (has(node, "loc")) {
    newNode.loc = node.loc;
  }
  if (has(node, "leadingComments")) {
    newNode.leadingComments = node.leadingComments;
  }
  if (has(node, "innerComments")) {
    newNode.innerComments = node.innerCmments;
  }
  if (has(node, "trailingComments")) {
    newNode.trailingComments = node.trailingComments;
  }
  if (has(node, "extra")) {
    newNode.extra = {
      ...node.extra,
    };
  }

  return newNode;
}
<|endoftext|>


<commit_before>import { run } from '@ember/runloop';
import { capitalize } from '@ember/string';
import { getOwner } from '@ember/application';
import { tryInvoke, isEqual } from '@ember/utils';
import EmberObject, { set, get } from '@ember/object';
import ConnectionMonitor from 'ember-cable/core/connection_monitor';

export default EmberObject.extend({
  consumer: null,
  connected: false,
  _reopenTimer: null,

  init() {
    this._super(...arguments);
    this.open();
    this.monitor = ConnectionMonitor.create(getOwner(this).ownerInjection(), {
      connection: this
    });
  },

  send(data) {
    if (this.isOpen()) {
      get(this, 'webSocket').send(JSON.stringify(data));
    }
  },

  open() {
    let ws = new WebSocket(get(this, 'consumer.url'));

    ['open', 'close', 'error', 'message'].forEach(eventName => {
      ws[`on${eventName}`] = event => {
        run(() => tryInvoke(this, `on${capitalize(eventName)}`, [event]));
      };
    });

    set(this, 'webSocket', ws);
  },

  close() {
    let ws = this.get('webSocket');

    ['open', 'close', 'error', 'message'].forEach(eventName => {
      ws[`on${eventName}`] = null;
    });

    tryInvoke(get(this, 'webSocket'), 'close');
    this.disconnect();
  },

  reopen() {
    if (this.isClose()) {
      this.open();
    } else {
      this.close();
      this._reopenTimer = setTimeout(run.bind(this, 'reopen'), 500);
    }
  },

  willDestroy() {
    this._super();
    clearTimeout(this._reopenTimer);
    this.monitor.destroy();
  },

  isClose() {
    return !this.isOpen();
  },

  isOpen() {
    return (
      isEqual(get(this, 'connected'), true) &&
      isEqual(get(this, 'webSocket').readyState, get(this, 'webSocket').OPEN)
    );
  },

  isConnecting() {
    return isEqual(
      get(this, 'webSocket').readyState,
      get(this, 'webSocket').CONNECTING
    );
  },

  disconnect() {
    set(this, 'connected', false);
    get(this, 'consumer.subscriptions').notifyAll('disconnected');
  },

  onMessage(event) {
    let data = JSON.parse(event.data);
    switch (data.type) {
      case 'welcome':
        get(this, 'monitor').connected();
        break;
      case 'ping':
        get(this, 'monitor').ping();
        break;
      case 'confirm_subscription':
        get(this, 'consumer.subscriptions').notify(
          data.identifier,
          'connected'
        );
        break;
      case 'reject_subscription':
        get(this, 'consumer.subscriptions').reject(data.identifier);
        break;
      default:
        get(this, 'consumer.subscriptions').notify(
          data.identifier,
          'received',
          data.message
        );
    }
  },

  onOpen() {
    set(this, 'connected', true);
    get(this, 'consumer.subscriptions').reload();
  },

  onClose() {
    this.disconnect();
  },

  onError() {
    this.disconnect();
  }
});
<commit_msg>Add method to destroy consumer<commit_after>import { run } from '@ember/runloop';
import { capitalize } from '@ember/string';
import { getOwner } from '@ember/application';
import { tryInvoke, isEqual } from '@ember/utils';
import EmberObject, { set, get } from '@ember/object';
import ConnectionMonitor from 'ember-cable/core/connection_monitor';

export default EmberObject.extend({
  consumer: null,
  connected: false,
  _reopenTimer: null,

  init() {
    this._super(...arguments);
    this.open();
    this.monitor = ConnectionMonitor.create(getOwner(this).ownerInjection(), {
      connection: this
    });
  },

  send(data) {
    if (this.isOpen()) {
      get(this, 'webSocket').send(JSON.stringify(data));
    }
  },

  open() {
    let ws = new WebSocket(get(this, 'consumer.url'));

    ['open', 'close', 'error', 'message'].forEach(eventName => {
      ws[`on${eventName}`] = event => {
        run(() => tryInvoke(this, `on${capitalize(eventName)}`, [event]));
      };
    });

    set(this, 'webSocket', ws);
  },

  close() {
    let ws = this.get('webSocket');

    ['open', 'close', 'error', 'message'].forEach(eventName => {
      ws[`on${eventName}`] = null;
    });

    tryInvoke(get(this, 'webSocket'), 'close');
    this.disconnect();
  },

  reopen() {
    if (this.isClose()) {
      this.open();
    } else {
      this.close();
      this._reopenTimer = setTimeout(run.bind(this, 'reopen'), 500);
    }
  },

  willDestroy() {
    this._super();
    clearTimeout(this._reopenTimer);
    this.monitor.destroy();
    this.close();
  },

  isClose() {
    return !this.isOpen();
  },

  isOpen() {
    return (
      isEqual(get(this, 'connected'), true) &&
      isEqual(get(this, 'webSocket').readyState, get(this, 'webSocket').OPEN)
    );
  },

  isConnecting() {
    return isEqual(
      get(this, 'webSocket').readyState,
      get(this, 'webSocket').CONNECTING
    );
  },

  disconnect() {
    set(this, 'connected', false);
    get(this, 'consumer.subscriptions').notifyAll('disconnected');
  },

  onMessage(event) {
    let data = JSON.parse(event.data);
    switch (data.type) {
      case 'welcome':
        get(this, 'monitor').connected();
        break;
      case 'ping':
        get(this, 'monitor').ping();
        break;
      case 'confirm_subscription':
        get(this, 'consumer.subscriptions').notify(
          data.identifier,
          'connected'
        );
        break;
      case 'reject_subscription':
        get(this, 'consumer.subscriptions').reject(data.identifier);
        break;
      default:
        get(this, 'consumer.subscriptions').notify(
          data.identifier,
          'received',
          data.message
        );
    }
  },

  onOpen() {
    set(this, 'connected', true);
    get(this, 'consumer.subscriptions').reload();
  },

  onClose() {
    this.disconnect();
  },

  onError() {
    this.disconnect();
  }
});
<|endoftext|>


<commit_before>angular.module("umbraco")
    .controller("Umbraco.Editors.RTEController",
    function($rootScope, $scope, dialogService, $log, imageHelper, assetsService, $timeout){

    assetsService.loadJs("lib/tinymce/tinymce.min.js", $scope).then(function(){
        tinymce.DOM.events.domLoaded = true;

        tinymce.init({
            selector: "#" + $scope.model.alias + "_rte",
            skin: "umbraco",
            menubar : false,
            statusbar: false,
            height: 340,
            toolbar: "bold italic | styleselect | alignleft aligncenter alignright | bullist numlist | outdent indent | link image mediapicker iconpicker embeddialog",
            setup : function(editor) {

                    editor.on('blur', function(e) {
                        $scope.$apply(function() {
                            $scope.model.value = editor.getContent();
                        });
                    });

                    editor.addButton('mediapicker', {
                        icon: 'media',
                        tooltip: 'Media Picker',
                        onclick: function(){
                            dialogService.mediaPicker({scope: $scope, callback: function(img){

                                if(img){
                                    var imagePropVal = imageHelper.getImagePropertyValue({imageModel: img, scope: $scope});
                                    var data = {
                                        src: (imagePropVal != null && imagePropVal != "") ? imagePropVal: "nothing.jpg",
                                        id: '__mcenew'
                                    };

                                    editor.insertContent(editor.dom.createHTML('img', data));

                                    $timeout(function(){
                                         var imgElm = editor.dom.get('__mcenew');
                                         var size = editor.dom.getSize(imgElm);
                                         $log.log(size);

                                         var newSize = imageHelper.scaleToMaxSize(500, size.w, size.h);
                                         var s = "width: " + newSize.width + "px; height:" + newSize.height + "px;";
                                         editor.dom.setAttrib(imgElm, 'style', s);
                                         editor.dom.setAttrib(imgElm, 'rel', newSize.width + "," + newSize.height);
                                         editor.dom.setAttrib(imgElm, 'id', null);

                                    }, 500);
                                }
                            }});
                        }
                    });

                    editor.addButton('iconpicker', {
                        icon: 'media',
                        tooltip: 'Icon Picker',
                        onclick: function(){
                            dialogService.open({show: true, template: "views/common/dialogs/iconpicker.html", scope: $scope, callback: function(c){

                                var data = {
                                    style: 'font-size: 60px'
                                };

                                var i = editor.dom.createHTML('i', data);
                                tinyMCE.activeEditor.dom.addClass(i, c);
                                editor.insertContent(i);
                            }});
                        }
                    });

                    editor.addButton('embeddialog', {
                        icon: 'media',
                        tooltip: 'Embed',
                        onclick: function () {
                            dialogService.embedDialog({
                                scope: $scope, callback: function (data) {
                                    editor.insertContent(data);
                                }
                            });
                        }
                    });
              }
        });

        $scope.openMediaPicker =function(value){
                var d = dialog.mediaPicker({scope: $scope, callback: populate});
        };

        function populate(data){
            $scope.model.value = data.selection;
        }

    });
});<commit_msg>rte.controller<commit_after>angular.module("umbraco")
    .controller("Umbraco.Editors.RTEController",
    function($rootScope, $scope, dialogService, $log, imageHelper, assetsService, $timeout){

    assetsService.loadJs("lib/tinymce/tinymce.min.js", $scope).then(function(){
        tinymce.DOM.events.domLoaded = true;

        tinymce.init({
            selector: "#" + $scope.model.alias + "_rte",
            skin: "umbraco",
            menubar : false,
            statusbar: false,
            height: 340,
            toolbar: "bold italic | styleselect | alignleft aligncenter alignright | bullist numlist | outdent indent | link image mediapicker iconpicker embeddialog",
            setup : function(editor) {

                    editor.on('blur', function(e) {
                        $scope.$apply(function() {
                            $scope.model.value = editor.getContent();
                        });
                    });

                    editor.addButton('mediapicker', {
                        icon: 'media',
                        tooltip: 'Media Picker',
                        onclick: function(){
                            dialogService.mediaPicker({scope: $scope, callback: function(img){

                                if(img){
                                    var imagePropVal = imageHelper.getImagePropertyValue({imageModel: img, scope: $scope});
                                    var data = {
                                        src: (imagePropVal != null && imagePropVal != "") ? imagePropVal: "nothing.jpg",
                                        id: '__mcenew'
                                    };

                                    editor.insertContent(editor.dom.createHTML('img', data));

                                    $timeout(function(){
                                         var imgElm = editor.dom.get('__mcenew');
                                         var size = editor.dom.getSize(imgElm);
                                         $log.log(size);

                                         var newSize = imageHelper.scaleToMaxSize(500, size.w, size.h);
                                         var s = "width: " + newSize.width + "px; height:" + newSize.height + "px;";
                                         editor.dom.setAttrib(imgElm, 'style', s);
                                         editor.dom.setAttrib(imgElm, 'rel', newSize.width + "," + newSize.height);
                                         editor.dom.setAttrib(imgElm, 'id', null);

                                    }, 500);
                                }
                            }});
                        }
                    });

                    editor.addButton('iconpicker', {
                        icon: 'media',
                        tooltip: 'Icon Picker',
                        onclick: function(){
                            dialogService.open({show: true, template: "views/common/dialogs/iconpicker.html", scope: $scope, callback: function(c){

                                var data = {
                                    style: 'font-size: 60px'
                                };

                                var i = editor.dom.createHTML('i', data);
                                tinyMCE.activeEditor.dom.addClass(i, c);
                                editor.insertContent(i);
                            }});
                        }
                    });

                    editor.addButton('embeddialog', {
                        icon: 'media',
                        tooltip: 'Embed',
                        onclick: function () {
                            dialogService.embedDialog({
                                scope: $scope, callback: function (data) {
                                    editor.insertContent(data);
                                }
                            });
                        }
                    });
              }
        });

        $scope.openMediaPicker =function(value){
                var d = dialog.mediaPicker({scope: $scope, callback: populate});
        };

        function populate(data){
            $scope.model.value = data.selection;
        }

    });
});<|endoftext|>


<commit_before>/*
 * Copyright 2013 SimplifyOps Inc, <http://simplifyops.com>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/**
 * State of workflow
 */
var FlowState = Class.create({
    model:{},
    executionId:null,
    targetElement:null,
    loadUrl:null,
    outputUrl:null,
    shouldUpdate:false,
    timer:null,
    selectedElem:null,
    reloadInterval:3000,
    initialize: function (eid, elem, params) {
        this.executionId = eid;
        this.targetElement = elem;
        Object.extend(this, params);
    },
    withOrWithoutMatch: function (root, selector, func, wofunc) {
        var elem = $(root).down(selector);
        if (elem && typeof(func)=='function') {
            func(elem);
        } else if(typeof(wofunc=='function')){
            wofunc();
        }
    },
    withoutMatch: function (root, selector, func) {
        this.withOrWithoutMatch(root, selector, null,func);
    },
    withMatch: function (root, selector, func) {
        this.withOrWithoutMatch(root,selector,func,
            this.logWarn.bind(this).curry("No match " + selector)
        );
    },
    updateStepState: function (root,stepctx, step) {
        this.withMatch(root, '.execstate.step[data-stepctx=' + stepctx + ']', function (elem) {
            $(elem).setAttribute('data-execstate', step.executionState);
        });
        this.withMatch(root, '.stepident[data-stepctx=' + stepctx + ']', function (elem) {
            var type= this.workflow.contextType(stepctx);
            $(elem).innerHTML=' '+this.workflow.renderContextString(stepctx);
        });
        if (step.errorMessage) {
            this.withMatch(root, '.errmsg.step[data-stepctx=' + stepctx + ']', function (elem) {
                $(elem).innerHTML = step.errorMessage;
                $(elem).show();
            });
        }
    },
    updateOutput:function(elem,data){
        $(elem).innerHTML='';
        for(var i=0;i<data.entries.length;i++){
            $(elem).innerHTML+= data.entries[i].log+'\n';
        }
    },
    showOutput:function(stepctx, node,evt){
        if($(this.targetElement + '_output')){
            $(evt.target).addClassName('selected');
            if(this.selectedElem){
                $(this.selectedElem).removeClassName('selected');
            }
            this.selectedElem= evt.target;
            var state=this;
            new Ajax.Request(this.outputUrl,
                {
                    parameters:{nodename:node,stepctx:stepctx},
                    onSuccess: function (transport) {
                        var data = transport.responseJSON;
                        state.updateOutput(state.targetElement+'_output',data);
                    }
                }
            )
        }
    },
    bindNodeOutput: function(elem,stepctx,node){
        Event.observe(elem, 'click', this.showOutput.bind(this).curry(stepctx, node));
    },
    newNodeState: function(stepctx, node, nstate){
        var div = new Element('div');
        var nspan = new Element('span');
        nspan.addClassName('execstate isnode');
        nspan.setAttribute('data-node',node);
        nspan.setAttribute('data-stepctx',stepctx);
        nspan.setAttribute('data-execstate', nstate.executionState);
        var errspan = new Element('span');
        errspan.addClassName('errmsg isnode');
        errspan.setAttribute('data-node', node);
        errspan.setAttribute('data-stepctx', stepctx);
        errspan.style.display='none';
        div.appendChild(nspan);
        div.appendChild(errspan);
        this.bindNodeOutput(div,stepctx,node);
        return div;
    },
    setNodeState:function(stepctx,node,nstate, elem){
        $(elem).innerHTML = node;
        $(elem).setAttribute('data-execstate', nstate.executionState);
        if($(elem).getAttribute('data-bound')!='true'){
            this.bindNodeOutput($(elem).parentNode, stepctx, node);
            $(elem).setAttribute('data-bound', 'true');
        }
    },
    addNodeState: function(root,stepctx,node,nstate){
        var nstates= $(root).down('.wfstepstate[data-stepctx=' + stepctx + '] .nodestates');
        var newstate= this.newNodeState(stepctx, node, nstate);
        nstates.appendChild(newstate);
        var execstate=newstate.down('.execstate');
        this.setNodeState(stepctx, node, nstate, execstate);
    },
    updateNodeState: function (root, stepctx,node,nstate) {
        this.withOrWithoutMatch(root, '.execstate.isnode[data-stepctx=' + stepctx + '][data-node=' + node + ']',
            this.setNodeState.bind(this).curry(stepctx,node,nstate),
            this.addNodeState.bind(this).curry(root, stepctx, node, nstate)
        );

        if (nstate.errorMessage) {
            this.withMatch(root, '.errmsg.isnode[data-stepctx=' + stepctx + '][data-node=' + node + ']', function (elem) {
                $(elem).innerHTML = nstate.errorMessage;
                $(elem).show();
            });
        }
    },
    logWarn: function (text) {
        if ($(this.targetElement + '_log')) {
            $(this.targetElement + '_log').innerHTML += "
" + text;
        }
    },
    showError: function(text){
        this.logWarn(text);
    },
    updateWorkflow: function (currentwf,ctx) {
        var count = parseInt(currentwf.stepCount);
        if (!currentwf.steps) {
            return;
        }
        for (var i = 1; i <= count; i++) {
            var step = currentwf.steps[i - 1];
            var stepctx = ctx + (ctx ? '/' : '') + i;
            this.updateStepState(this.targetElement,stepctx,step);

            if (step.hasSubworkflow) {
                this.updateWorkflow(step.workflow, stepctx);
            }else if (step.nodeStep){
                var nodeset=step.stepTargetNodes? step.stepTargetNodes :currentwf.targetNodes;
                for(var n=0;n<nodeset.length;n++){
                    var node = nodeset[n];
                    var nstate= step.nodeStates?step.nodeStates[node]:null;
                    if(!nstate){
                        nstate={ executionState: 'WAITING' };
                    }
                    this.updateNodeState(this.targetElement, stepctx, node, nstate);
                }
            }
        }
    },
    update: function (data) {
        //compare
        if(data.error){
            this.showError(data.error);
            return;
        }
        this.model = data;
        if($(this.targetElement + '_json')){
            $(this.targetElement + '_json').innerHTML = Object.toJSON(this.model);
        }
        this.updateWorkflow(this.model,'');
        if (!this.model.completed && this.shouldUpdate) {
            this.timer = setTimeout(this.callUpdate.bind(this), this.reloadInterval);
        } else {
            this.stopFollowing();
        }
    },
    callUpdate: function(){
        var state=this;
        new Ajax.Request(this.loadUrl,{
            evalScripts: true,
            evalJSON: true,
            onSuccess: function (transport) {
                var data = transport.responseJSON;
                state.update(data);
            }
        });
    },
    beginFollowing: function(){
        this.shouldUpdate=true;
        this.callUpdate();
    },
    stopFollowing: function(){
        this.shouldUpdate=false;
        clearTimeout(this.timer);
        this.timer = null;
    }
});
<commit_msg>collapsable state for step view<commit_after>/*
 * Copyright 2013 SimplifyOps Inc, <http://simplifyops.com>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/**
 * State of workflow, step oriented
 */
var FlowState = Class.create({
    model:{},
    executionId:null,
    targetElement:null,
    loadUrl:null,
    outputUrl:null,
    shouldUpdate:false,
    timer:null,
    selectedElem:null,
    reloadInterval:3000,
    initialize: function (eid, elem, params) {
        this.executionId = eid;
        this.targetElement = elem;
        Object.extend(this, params);
    },
    withOrWithoutMatch: function (root, selector, func, wofunc) {
        var elem = $(root).down(selector);
        if (elem && typeof(func)=='function') {
            func(elem);
        } else if(typeof(wofunc=='function')){
            wofunc();
        }
    },
    withoutMatch: function (root, selector, func) {
        this.withOrWithoutMatch(root, selector, null,func);
    },
    withMatch: function (root, selector, func) {
        this.withOrWithoutMatch(root,selector,func,
            this.logWarn.bind(this).curry("No match " + selector)
        );
    },
    showStepOutput: function (elem, stepctx, evt) {
        var wfstep = $(elem).up('.wfstepstate');
        if (wfstep) {
            $(wfstep).toggleClassName('collapsed');
        }
    },
    bindStepOutput: function (elem, stepctx) {
        Event.observe(elem, 'click', this.showStepOutput.bind(this).curry(elem, stepctx));
    },
    updateStepState: function (root,stepctx, step) {
        this.withMatch(root, '.execstate.step[data-stepctx=' + stepctx + ']', function (elem) {
            $(elem).setAttribute('data-execstate', step.executionState);
        });
        this.withMatch(root, '.stepident[data-stepctx=' + stepctx + ']', function (elem) {
            var type= this.workflow.contextType(stepctx);
            $(elem).innerHTML=' '+this.workflow.renderContextString(stepctx);
        });
        var me=this;
        this.withMatch(root, '.stepaction[data-stepctx=' + stepctx + ']', function (elem) {
            if ($(elem).getAttribute('data-bound') != 'true') {
                me.bindStepOutput($(elem), stepctx);
                $(elem).setAttribute('data-bound', 'true');
            }
        });
        if (step.errorMessage) {
            this.withMatch(root, '.errmsg.step[data-stepctx=' + stepctx + ']', function (elem) {
                $(elem).innerHTML = step.errorMessage;
                $(elem).show();
            });
        }

    },
    updateOutput:function(elem,data){
        $(elem).innerHTML='';
        for(var i=0;i<data.entries.length;i++){
            $(elem).innerHTML+= data.entries[i].log+'\n';
        }
    },
    showOutput:function(elem,stepctx, node,evt){
        if($(this.targetElement + '_output')){
            if (this.selectedElem) {
                $(this.selectedElem).removeClassName('active');
            }
            $(elem).addClassName('active');
            this.selectedElem= elem;
            var state=this;
            new Ajax.Request(this.outputUrl,
                {
                    parameters:{nodename:node,stepctx:stepctx},
                    onSuccess: function (transport) {
                        var data = transport.responseJSON;
                        state.updateOutput(state.targetElement+'_output',data);
                    }
                }
            )
        }
    },
    bindNodeOutput: function(elem,stepctx,node){
        Event.observe(elem, 'click', this.showOutput.bind(this).curry(elem,stepctx, node));
    },
    newNodeState: function(stepctx, node, nstate){
        var div = new Element('div');
        div.addClassName('textbtn textbtn-default');
        var nspan = new Element('span');
        nspan.addClassName('execstate isnode');
        nspan.setAttribute('data-node',node);
        nspan.setAttribute('data-stepctx',stepctx);
        nspan.setAttribute('data-execstate', nstate.executionState);
        var errspan = new Element('span');
        errspan.addClassName('errmsg isnode');
        errspan.setAttribute('data-node', node);
        errspan.setAttribute('data-stepctx', stepctx);
        errspan.style.display='none';
        div.appendChild(nspan);
        div.appendChild(errspan);
        this.bindNodeOutput(div,stepctx,node);
        return div;
    },
    setNodeState:function(stepctx,node,nstate, elem){
        $(elem).innerHTML = node;
        $(elem).setAttribute('data-execstate', nstate.executionState);
        if($(elem).getAttribute('data-bound')!='true'){
            this.bindNodeOutput($(elem).parentNode, stepctx, node);
            $(elem).setAttribute('data-bound', 'true');
        }
    },
    addNodeState: function(root,stepctx,node,nstate){
        var nstates= $(root).down('.wfstepstate[data-stepctx=' + stepctx + '] .nodestates');
        var newstate= this.newNodeState(stepctx, node, nstate);
        nstates.appendChild(newstate);
        var execstate=newstate.down('.execstate');
        this.setNodeState(stepctx, node, nstate, execstate);
    },
    updateNodeState: function (root, stepctx,node,nstate) {
        this.withOrWithoutMatch(root, '.execstate.isnode[data-stepctx=' + stepctx + '][data-node=' + node + ']',
            this.setNodeState.bind(this).curry(stepctx,node,nstate),
            this.addNodeState.bind(this).curry(root, stepctx, node, nstate)
        );

        if (nstate.errorMessage) {
            this.withMatch(root, '.errmsg.isnode[data-stepctx=' + stepctx + '][data-node=' + node + ']', function (elem) {
                $(elem).innerHTML = nstate.errorMessage;
                $(elem).show();
            });
        }
    },
    logWarn: function (text) {
        if ($(this.targetElement + '_log')) {
            $(this.targetElement + '_log').innerHTML += "
" + text;
        }
    },
    showError: function(text){
        this.logWarn(text);
    },
    updateWorkflow: function (currentwf,ctx) {
        var count = parseInt(currentwf.stepCount);
        if (!currentwf.steps) {
            return;
        }
        for (var i = 1; i <= count; i++) {
            var step = currentwf.steps[i - 1];
            var stepctx = ctx + (ctx ? '/' : '') + i;
            this.updateStepState(this.targetElement,stepctx,step);

            if (step.hasSubworkflow) {
                this.updateWorkflow(step.workflow, stepctx);
            }else if (step.nodeStep){
                var nodeset=step.stepTargetNodes? step.stepTargetNodes :currentwf.targetNodes;
                for(var n=0;n<nodeset.length;n++){
                    var node = nodeset[n];
                    var nstate= step.nodeStates?step.nodeStates[node]:null;
                    if(!nstate){
                        nstate={ executionState: 'WAITING' };
                    }
                    this.updateNodeState(this.targetElement, stepctx, node, nstate);
                }
            }
        }
    },
    update: function (data) {
        //compare
        if(data.error){
            this.showError(data.error);
            return;
        }
        this.model = data;
        if($(this.targetElement + '_json')){
            $(this.targetElement + '_json').innerHTML = Object.toJSON(this.model);
        }
        this.updateWorkflow(this.model,'');
        if (!this.model.completed && this.shouldUpdate) {
            this.timer = setTimeout(this.callUpdate.bind(this), this.reloadInterval);
        } else {
            this.stopFollowing();
        }
    },
    callUpdate: function(){
        var state=this;
        new Ajax.Request(this.loadUrl,{
            evalScripts: true,
            evalJSON: true,
            onSuccess: function (transport) {
                var data = transport.responseJSON;
                state.update(data);
            }
        });
    },
    beginFollowing: function(){
        this.shouldUpdate=true;
        this.callUpdate();
    },
    stopFollowing: function(){
        this.shouldUpdate=false;
        clearTimeout(this.timer);
        this.timer = null;
    }
});
<|endoftext|>