<!DOCTYPE html>
<html>

<head>
    <script data-require="jquery@*" data-semver="2.1.1" src="//cdnjs.cloudflare.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
    <link rel="stylesheet" href="style.css" />
    <script src="setImmediate.js"></script>
    <script src="script.js"></script>
</head>

<body>
    <h1>JavaScript Array Processing Test</h1>
    <p>Both chunk methods perform the same operations, but the timedChunk method takes "breaks" from processing to allow the browser to repaint.</p>
    <ul style="list-style: none; ">
        <li>CSS spinner: <div class="loader animated"></div></li>
        <li>GIF spinner: <div class="k-loading-image"></div></li>
    </ul>
    
    <h2>chunk() Function</h2>
    <p>This method will cause the GIF spinner to freeze, but the CSS spinner will continue.</p>
    <p>
        <button id="start-chunk-btn">Start chunk()</button>
        <button id="reset-chunk-btn">Reset</button>
    </p>
    <div style="width: 500px; border: 1px solid black">
        <div id="orig-progress" style="height: 16px; color: white; width: 100%;">
            <div id="orig-label"></div>
        </div>
    </div>
    <h2>timedChunk() Function</h2>
    <p>This method will allow both spinners to keep spinning.</p>
    <p>
        <button id="start-timed-btn">Start timedChunk()</button>
        <button id="reset-timed-btn">Reset</button>
    </p>
    <div style="width: 500px; border: 1px solid black">
        <div id="new-progress" style="height: 16px; color: white; width: 100%;">
            <div id="new-label"></div>
        </div>
    </div>
</body>

</html>
// Code goes here

$(function() {
    function chunk(array, process, context, callback) {
        var items = array.concat(); //clone the array
        
        var start = +new Date();
        var execute = function() {
            process.call(context, items.shift(), start);
            
            if (items.length <= 0) {
                callback(array);
            } else {
                execute();
            }
        };
        
        execute();
    }

    function timedChunk(items, process, context, callback) {
        var todo = items.concat(); //create a clone of the original

        var outerStart = +new Date();
        var execute = function() {
            var start = +new Date();

            do {
                process.call(context, todo.shift(), outerStart);
            } while (todo.length > 0 && (+new Date() - start < 25));
            
            if (todo.length > 0) {
                setImmediate(execute);
            } else {
                callback(items);
            }
        };
        
        execute();
    }

    var count = 10000;
    var items = new Array(count);
    for (var i = 0; i < count; i++) {
        items[i] = i;
    }
    
    $('body').append('<style>.unit { width: 1px; }</style>');
    
    function processOrig(item, start) {
        var progress = $('#orig-progress');
        $('#orig-label').text(+new Date() - start);
        
        var units = progress.children('.unit').length;
        var itemsPerUnit = Math.floor(count / progress.parent().width());
        if (item % itemsPerUnit === 0) {
            progress.append('<div class="unit"></div>');
        }
    }

    function processNew(item, start) {
        var progress = $('#new-progress');
        $('#new-label').text(+new Date() - start);
        
        var units = progress.children('.unit').length;
        var itemsPerUnit = Math.floor(count / progress.parent().width());
        if (item % itemsPerUnit === 0) {
            progress.append('<div class="unit"></div>');
        }
    }

    function startProcessing(button) {
        resetTest();
        
        var startTimedButton = $('#start-timed-btn');
        var startButton = $('#start-chunk-btn');
        var resetButton = $('#reset-chunk-btn');
        
        startTimedButton[0].disabled = true;
        startButton[0].disabled = true;
        resetButton[0].disabled = true;

        var origStart = +new Date();
        chunk(items, processOrig, null, function() {
            $('#orig-label').text(new Date() - origStart);
            resetButton[0].disabled = false;
            startTimedButton[0].disabled = false;
            startButton[0].disabled = false;
        });
    }

    function startTimedProcessing(button) {
        resetTimedTest();
        
        var startChunkButton = $('#start-chunk-btn');
        var startButton = $('#start-timed-btn');
        var resetButton = $('#reset-timed-btn');

        startChunkButton[0].disabled = true;
        startButton[0].disabled = true;
        resetButton[0].disabled = true;

        var newStart = +new Date();
        timedChunk(items, processNew, null, function() {
            $('#new-label').text(new Date() - newStart);
            resetButton[0].disabled = false;
            startChunkButton[0].disabled = false;
            startButton[0].disabled = false;
        });
    }

    function resetTest() {
        var origprogress = $('#orig-progress .unit').remove();

        $('#orig-label').text('');
        $('#start-chunk-btn')[0].disabled = false;
    }

    function resetTimedTest() {
        var newprogress = $('#new-progress .unit').remove();

        $('#new-label').text('');
        $('#start-timed-btn')[0].disabled = false;
    }

    $('body').on('click', '#start-chunk-btn', startProcessing);
    $('body').on('click', '#start-timed-btn', startTimedProcessing);
    $('body').on('click', '#reset-chunk-btn', resetTest);
    $('body').on('click', '#reset-timed-btn', resetTimedTest);
});
/* Styles go here */
body, html {
    font-family: arial, sans-serif;
    font-size: 13px;
}


#new-progress,
#orig-progress {
    position: relative;
}

#new-label,
#orig-label {
    position: absolute;
    top: 0;
    bottom: 0;
    left: 0;
    width: 100px;
    color: white;
}

.k-loading-image {
    background-image: url('http://cdn.kendostatic.com/2014.2.903/styles/Bootstrap/loading-image.gif');
    background-repeat: no-repeat;
    background-position: center center;
    width: 60px;
    height: 50px;
}

li {
    margin-bottom: 20px;
}

@-webkit-keyframes rotate {
  from {
    -webkit-transform: rotate(0deg);
  }

  to {
    -webkit-transform: rotate(360deg);
  }
}
@-moz-keyframes rotate {
  from {
    -moz-transform: rotate(0deg);
  }

  to {
    -moz-transform: rotate(360deg);
  }
}
@-o-keyframes rotate {
  from {
    -o-transform: rotate(0deg);
  }

  to {
    -o-transform: rotate(360deg);
  }
}
@keyframes rotate {
  from {
    transform: rotate(0deg);
  }

  to {
    transform: rotate(360deg);
  }
}

.unit {
    height: 16px; 
    background: navy; 
    color: white;
    display: inline-block;
}

.loader {
  font-size: 10px;
  position: relative;
  text-indent: -9999em;
  background: none;
  -webkit-transform-style: preserve-3d;
  -webkit-backface-visibility: hidden;
  border-top: 8px solid rgba(0, 0, 0, 0.2);
  border-right: 8px solid rgba(0, 0, 0, 0.2);
  border-bottom: 8px solid rgba(0, 0, 0, 0.2);
  border-left: 8px solid #333333;
}
.loader, .loader:after {
  border-radius: 50%;
  width: 40px;
  height: 40px;
}
.loader.animated {
  -moz-animation: rotate 1.1s infinite linear;
  -webkit-animation: rotate 1.1s infinite linear;
  animation: rotate 1.1s infinite linear;
}
(function (global, undefined) {
	"use strict";

	var tasks = (function () {
		function Task(handler, args) {
			this.handler = handler;
			this.args = args;
		}
		Task.prototype.run = function () {
			// See steps in section 5 of the spec.
			if (typeof this.handler === "function") {
				// Choice of `thisArg` is not in the setImmediate spec; `undefined` is in the setTimeout spec though:
				// http://www.whatwg.org/specs/web-apps/current-work/multipage/timers.html
				this.handler.apply(undefined, this.args);
			} else {
				var scriptSource = "" + this.handler;
				/*jshint evil: true */
				eval(scriptSource);
			}
		};

		var nextHandle = 1; // Spec says greater than zero
		var tasksByHandle = {};
		var currentlyRunningATask = false;

		return {
			addFromSetImmediateArguments: function (args) {
				var handler = args[0];
				var argsToHandle = Array.prototype.slice.call(args, 1);
				var task = new Task(handler, argsToHandle);

				var thisHandle = nextHandle++;
				tasksByHandle[thisHandle] = task;
				return thisHandle;
			},
			runIfPresent: function (handle) {
				// From the spec: "Wait until any invocations of this algorithm started before this one have completed."
				// So if we're currently running a task, we'll need to delay this invocation.
				if (!currentlyRunningATask) {
					var task = tasksByHandle[handle];
					if (task) {
						currentlyRunningATask = true;
						try {
							task.run();
						} finally {
							delete tasksByHandle[handle];
							currentlyRunningATask = false;
						}
					}
				} else {
					// Delay by doing a setTimeout. setImmediate was tried instead, but in Firefox 7 it generated a
					// "too much recursion" error.
					global.setTimeout(function () {
						tasks.runIfPresent(handle);
					}, 0);
				}
			},
			remove: function (handle) {
				delete tasksByHandle[handle];
			}
		};
	}());

	function canUseNextTick() {
		// Don't get fooled by e.g. browserify environments.
		return typeof process === "object" &&
			   Object.prototype.toString.call(process) === "[object process]";
	}

	function canUseMessageChannel() {
		return !!global.MessageChannel;
	}

	function canUsePostMessage() {
		// The test against `importScripts` prevents this implementation from being installed inside a web worker,
		// where `global.postMessage` means something completely different and can't be used for this purpose.

		if (!global.postMessage || global.importScripts) {
			return false;
		}

		var postMessageIsAsynchronous = true;
		var oldOnMessage = global.onmessage;
		global.onmessage = function () {
			postMessageIsAsynchronous = false;
		};
		global.postMessage("", "*");
		global.onmessage = oldOnMessage;

		return postMessageIsAsynchronous;
	}

	function canUseReadyStateChange() {
		return "document" in global && "onreadystatechange" in global.document.createElement("script");
	}

	function installNextTickImplementation(attachTo) {
		attachTo.setImmediate = function () {
			var handle = tasks.addFromSetImmediateArguments(arguments);

			process.nextTick(function () {
				tasks.runIfPresent(handle);
			});

			return handle;
		};
	}

	function installMessageChannelImplementation(attachTo) {
		var channel = new global.MessageChannel();
		channel.port1.onmessage = function (event) {
			var handle = event.data;
			tasks.runIfPresent(handle);
		};
		attachTo.setImmediate = function () {
			var handle = tasks.addFromSetImmediateArguments(arguments);

			channel.port2.postMessage(handle);

			return handle;
		};
	}

	function installPostMessageImplementation(attachTo) {
		// Installs an event handler on `global` for the `message` event: see
		// * https://developer.mozilla.org/en/DOM/window.postMessage
		// * http://www.whatwg.org/specs/web-apps/current-work/multipage/comms.html#crossDocumentMessages

		var MESSAGE_PREFIX = "com.bn.NobleJS.setImmediate" + Math.random();

		function isStringAndStartsWith(string, putativeStart) {
			return typeof string === "string" && string.substring(0, putativeStart.length) === putativeStart;
		}

		function onGlobalMessage(event) {
			// This will catch all incoming messages (even from other windows!), so we need to try reasonably hard to
			// avoid letting anyone else trick us into firing off. We test the origin is still this window, and that a
			// (randomly generated) unpredictable identifying prefix is present.
			if (event.source === global && isStringAndStartsWith(event.data, MESSAGE_PREFIX)) {
				var handle = event.data.substring(MESSAGE_PREFIX.length);
				tasks.runIfPresent(handle);
			}
		}
		if (global.addEventListener) {
			global.addEventListener("message", onGlobalMessage, false);
		} else {
			global.attachEvent("onmessage", onGlobalMessage);
		}

		attachTo.setImmediate = function () {
			var handle = tasks.addFromSetImmediateArguments(arguments);

			// Make `global` post a message to itself with the handle and identifying prefix, thus asynchronously
			// invoking our onGlobalMessage listener above.
			global.postMessage(MESSAGE_PREFIX + handle, "*");

			return handle;
		};
	}

	function installReadyStateChangeImplementation(attachTo) {
		attachTo.setImmediate = function () {
			var handle = tasks.addFromSetImmediateArguments(arguments);

			// Create a <script> element; its readystatechange event will be fired asynchronously once it is inserted
			// into the document. Do so, thus queuing up the task. Remember to clean up once it's been called.
			var scriptEl = global.document.createElement("script");
			scriptEl.onreadystatechange = function () {
				tasks.runIfPresent(handle);

				scriptEl.onreadystatechange = null;
				scriptEl.parentNode.removeChild(scriptEl);
				scriptEl = null;
			};
			global.document.documentElement.appendChild(scriptEl);

			return handle;
		};
	}

	function installSetTimeoutImplementation(attachTo) {
		attachTo.setImmediate = function () {
			var handle = tasks.addFromSetImmediateArguments(arguments);

			global.setTimeout(function () {
				tasks.runIfPresent(handle);
			}, 0);

			return handle;
		};
	}

	var setImmediate = global.setImmediate;
	if (!setImmediate) {
		// If supported, we should attach to the prototype of global, since that is where setTimeout et al. live.
		var attachTo = typeof Object.getPrototypeOf === "function" && "setTimeout" in Object.getPrototypeOf(global) ?
			Object.getPrototypeOf(global)
			: global;

		if (canUseNextTick()) {
			// For Node.js before 0.9
			installNextTickImplementation(attachTo);
		} else if (canUsePostMessage()) {
			// For non-IE10 modern browsers
			installPostMessageImplementation(attachTo);
		} else if (canUseMessageChannel()) {
			// For web workers, where supported
			installMessageChannelImplementation(attachTo);
		} else if (canUseReadyStateChange()) {
			// For IE 6–8
			installReadyStateChangeImplementation(attachTo);
		} else {
			// For older browsers
			installSetTimeoutImplementation(attachTo);
		}

		attachTo.clearImmediate = tasks.remove;
	} else if (typeof setImmediate === 'function') {
		global.setImmediate = function() {
			return setImmediate.apply(null, arguments);
		};

		var clearImmediate = global.clearImmediate;
		global.clearImmediate = function(id) {
			clearImmediate(id);
		};
	}
}(typeof global === "object" && global ? global : this));