1 ko.utils = new (function () {
2 var stringTrimRegex = /^(\s|\u00A0)+|(\s|\u00A0)+$/g;
4 // Represent the known event types in a compact way, then at runtime transform it into a hash with event name as key (for fast lookup)
5 var knownEvents = {}, knownEventTypesByEventName = {};
6 var keyEventTypeName = /Firefox\/2/i.test(navigator.userAgent) ? 'KeyboardEvent' : 'UIEvents';
7 knownEvents[keyEventTypeName] = ['keyup', 'keydown', 'keypress'];
8 knownEvents['MouseEvents'] = ['click', 'dblclick', 'mousedown', 'mouseup', 'mousemove', 'mouseover', 'mouseout', 'mouseenter', 'mouseleave'];
9 for (var eventType in knownEvents) {
10 var knownEventsForType = knownEvents[eventType];
11 if (knownEventsForType.length) {
12 for (var i = 0, j = knownEventsForType.length; i < j; i++)
13 knownEventTypesByEventName[knownEventsForType[i]] = eventType;
16 var eventsThatMustBeRegisteredUsingAttachEvent = { 'propertychange': true }; // Workaround for an IE9 issue - https://github.com/SteveSanderson/knockout/issues/406
18 // Detect IE versions for bug workarounds (uses IE conditionals, not UA string, for robustness)
19 // Note that, since IE 10 does not support conditional comments, the following logic only detects IE < 10.
20 // Currently this is by design, since IE 10+ behaves correctly when treated as a standard browser.
21 // If there is a future need to detect specific versions of IE10+, we will amend this.
22 var ieVersion = (function() {
23 var version = 3, div = document.createElement('div'), iElems = div.getElementsByTagName('i');
25 // Keep constructing conditional HTML blocks until we hit one that resolves to an empty fragment
27 div.innerHTML = '<!--[if gt IE ' + (++version) + ']><i></i><![endif]-->',
30 return version > 4 ? version : undefined;
32 var isIe6 = ieVersion === 6,
33 isIe7 = ieVersion === 7;
35 function isClickOnCheckableElement(element, eventType) {
36 if ((ko.utils.tagNameLower(element) !== "input") || !element.type) return false;
37 if (eventType.toLowerCase() != "click") return false;
38 var inputType = element.type;
39 return (inputType == "checkbox") || (inputType == "radio");
43 fieldsIncludedWithJsonPost: ['authenticity_token', /^__RequestVerificationToken(_.*)?$/],
45 arrayForEach: function (array, action) {
46 for (var i = 0, j = array.length; i < j; i++)
50 arrayIndexOf: function (array, item) {
51 if (typeof Array.prototype.indexOf == "function")
52 return Array.prototype.indexOf.call(array, item);
53 for (var i = 0, j = array.length; i < j; i++)
54 if (array[i] === item)
59 arrayFirst: function (array, predicate, predicateOwner) {
60 for (var i = 0, j = array.length; i < j; i++)
61 if (predicate.call(predicateOwner, array[i]))
66 arrayRemoveItem: function (array, itemToRemove) {
67 var index = ko.utils.arrayIndexOf(array, itemToRemove);
69 array.splice(index, 1);
72 arrayGetDistinctValues: function (array) {
75 for (var i = 0, j = array.length; i < j; i++) {
76 if (ko.utils.arrayIndexOf(result, array[i]) < 0)
77 result.push(array[i]);
82 arrayMap: function (array, mapping) {
85 for (var i = 0, j = array.length; i < j; i++)
86 result.push(mapping(array[i]));
90 arrayFilter: function (array, predicate) {
93 for (var i = 0, j = array.length; i < j; i++)
94 if (predicate(array[i]))
95 result.push(array[i]);
99 arrayPushAll: function (array, valuesToPush) {
100 if (valuesToPush instanceof Array)
101 array.push.apply(array, valuesToPush);
103 for (var i = 0, j = valuesToPush.length; i < j; i++)
104 array.push(valuesToPush[i]);
108 extend: function (target, source) {
110 for(var prop in source) {
111 if(source.hasOwnProperty(prop)) {
112 target[prop] = source[prop];
119 emptyDomNode: function (domNode) {
120 while (domNode.firstChild) {
121 ko.removeNode(domNode.firstChild);
125 moveCleanedNodesToContainerElement: function(nodes) {
126 // Ensure it's a real array, as we're about to reparent the nodes and
127 // we don't want the underlying collection to change while we're doing that.
128 var nodesArray = ko.utils.makeArray(nodes);
130 var container = document.createElement('div');
131 for (var i = 0, j = nodesArray.length; i < j; i++) {
132 container.appendChild(ko.cleanNode(nodesArray[i]));
137 cloneNodes: function (nodesArray, shouldCleanNodes) {
138 for (var i = 0, j = nodesArray.length, newNodesArray = []; i < j; i++) {
139 var clonedNode = nodesArray[i].cloneNode(true);
140 newNodesArray.push(shouldCleanNodes ? ko.cleanNode(clonedNode) : clonedNode);
142 return newNodesArray;
145 setDomNodeChildren: function (domNode, childNodes) {
146 ko.utils.emptyDomNode(domNode);
148 for (var i = 0, j = childNodes.length; i < j; i++)
149 domNode.appendChild(childNodes[i]);
153 replaceDomNodes: function (nodeToReplaceOrNodeArray, newNodesArray) {
154 var nodesToReplaceArray = nodeToReplaceOrNodeArray.nodeType ? [nodeToReplaceOrNodeArray] : nodeToReplaceOrNodeArray;
155 if (nodesToReplaceArray.length > 0) {
156 var insertionPoint = nodesToReplaceArray[0];
157 var parent = insertionPoint.parentNode;
158 for (var i = 0, j = newNodesArray.length; i < j; i++)
159 parent.insertBefore(newNodesArray[i], insertionPoint);
160 for (var i = 0, j = nodesToReplaceArray.length; i < j; i++) {
161 ko.removeNode(nodesToReplaceArray[i]);
166 setOptionNodeSelectionState: function (optionNode, isSelected) {
167 // IE6 sometimes throws "unknown error" if you try to write to .selected directly, whereas Firefox struggles with setAttribute. Pick one based on browser.
169 optionNode.setAttribute("selected", isSelected);
171 optionNode.selected = isSelected;
174 stringTrim: function (string) {
175 return (string || "").replace(stringTrimRegex, "");
178 stringTokenize: function (string, delimiter) {
180 var tokens = (string || "").split(delimiter);
181 for (var i = 0, j = tokens.length; i < j; i++) {
182 var trimmed = ko.utils.stringTrim(tokens[i]);
184 result.push(trimmed);
189 stringStartsWith: function (string, startsWith) {
190 string = string || "";
191 if (startsWith.length > string.length)
193 return string.substring(0, startsWith.length) === startsWith;
196 domNodeIsContainedBy: function (node, containedByNode) {
197 if (containedByNode.compareDocumentPosition)
198 return (containedByNode.compareDocumentPosition(node) & 16) == 16;
199 while (node != null) {
200 if (node == containedByNode)
202 node = node.parentNode;
207 domNodeIsAttachedToDocument: function (node) {
208 return ko.utils.domNodeIsContainedBy(node, node.ownerDocument);
211 tagNameLower: function(element) {
212 // For HTML elements, tagName will always be upper case; for XHTML elements, it'll be lower case.
213 // Possible future optimization: If we know it's an element from an XHTML document (not HTML),
214 // we don't need to do the .toLowerCase() as it will always be lower case anyway.
215 return element && element.tagName && element.tagName.toLowerCase();
218 registerEventHandler: function (element, eventType, handler) {
219 var mustUseAttachEvent = ieVersion && eventsThatMustBeRegisteredUsingAttachEvent[eventType];
220 if (!mustUseAttachEvent && typeof jQuery != "undefined") {
221 if (isClickOnCheckableElement(element, eventType)) {
222 // For click events on checkboxes, jQuery interferes with the event handling in an awkward way:
223 // it toggles the element checked state *after* the click event handlers run, whereas native
224 // click events toggle the checked state *before* the event handler.
225 // Fix this by intecepting the handler and applying the correct checkedness before it runs.
226 var originalHandler = handler;
227 handler = function(event, eventData) {
228 var jQuerySuppliedCheckedState = this.checked;
230 this.checked = eventData.checkedStateBeforeEvent !== true;
231 originalHandler.call(this, event);
232 this.checked = jQuerySuppliedCheckedState; // Restore the state jQuery applied
235 jQuery(element)['bind'](eventType, handler);
236 } else if (!mustUseAttachEvent && typeof element.addEventListener == "function")
237 element.addEventListener(eventType, handler, false);
238 else if (typeof element.attachEvent != "undefined")
239 element.attachEvent("on" + eventType, function (event) {
240 handler.call(element, event);
243 throw new Error("Browser doesn't support addEventListener or attachEvent");
246 triggerEvent: function (element, eventType) {
247 if (!(element && element.nodeType))
248 throw new Error("element must be a DOM node when calling triggerEvent");
250 if (typeof jQuery != "undefined") {
252 if (isClickOnCheckableElement(element, eventType)) {
253 // Work around the jQuery "click events on checkboxes" issue described above by storing the original checked state before triggering the handler
254 eventData.push({ checkedStateBeforeEvent: element.checked });
256 jQuery(element)['trigger'](eventType, eventData);
257 } else if (typeof document.createEvent == "function") {
258 if (typeof element.dispatchEvent == "function") {
259 var eventCategory = knownEventTypesByEventName[eventType] || "HTMLEvents";
260 var event = document.createEvent(eventCategory);
261 event.initEvent(eventType, true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, element);
262 element.dispatchEvent(event);
265 throw new Error("The supplied element doesn't support dispatchEvent");
266 } else if (typeof element.fireEvent != "undefined") {
267 // Unlike other browsers, IE doesn't change the checked state of checkboxes/radiobuttons when you trigger their "click" event
268 // so to make it consistent, we'll do it manually here
269 if (isClickOnCheckableElement(element, eventType))
270 element.checked = element.checked !== true;
271 element.fireEvent("on" + eventType);
274 throw new Error("Browser doesn't support triggering events");
277 unwrapObservable: function (value) {
278 return ko.isObservable(value) ? value() : value;
281 peekObservable: function (value) {
282 return ko.isObservable(value) ? value.peek() : value;
285 toggleDomNodeCssClass: function (node, classNames, shouldHaveClass) {
287 var cssClassNameRegex = /[\w-]+/g,
288 currentClassNames = node.className.match(cssClassNameRegex) || [];
289 ko.utils.arrayForEach(classNames.match(cssClassNameRegex), function(className) {
290 var indexOfClass = ko.utils.arrayIndexOf(currentClassNames, className);
291 if (indexOfClass >= 0) {
292 if (!shouldHaveClass)
293 currentClassNames.splice(indexOfClass, 1);
296 currentClassNames.push(className);
299 node.className = currentClassNames.join(" ");
303 setTextContent: function(element, textContent) {
304 var value = ko.utils.unwrapObservable(textContent);
305 if ((value === null) || (value === undefined))
308 if (element.nodeType === 3) {
309 element.data = value;
311 // We need there to be exactly one child: a text node.
312 // If there are no children, more than one, or if it's not a text node,
313 // we'll clear everything and create a single text node.
314 var innerTextNode = ko.virtualElements.firstChild(element);
315 if (!innerTextNode || innerTextNode.nodeType != 3 || ko.virtualElements.nextSibling(innerTextNode)) {
316 ko.virtualElements.setDomNodeChildren(element, [document.createTextNode(value)]);
318 innerTextNode.data = value;
321 ko.utils.forceRefresh(element);
325 setElementName: function(element, name) {
328 // Workaround IE 6/7 issue
329 // - https://github.com/SteveSanderson/knockout/issues/197
330 // - http://www.matts411.com/post/setting_the_name_attribute_in_ie_dom/
331 if (ieVersion <= 7) {
333 element.mergeAttributes(document.createElement("<input name='" + element.name + "'/>"), false);
335 catch(e) {} // For IE9 with doc mode "IE9 Standards" and browser mode "IE9 Compatibility View"
339 forceRefresh: function(node) {
340 // Workaround for an IE9 rendering bug - https://github.com/SteveSanderson/knockout/issues/209
341 if (ieVersion >= 9) {
342 // For text nodes and comment nodes (most likely virtual elements), we will have to refresh the container
343 var elem = node.nodeType == 1 ? node : node.parentNode;
345 elem.style.zoom = elem.style.zoom;
349 ensureSelectElementIsRenderedCorrectly: function(selectElement) {
350 // Workaround for IE9 rendering bug - it doesn't reliably display all the text in dynamically-added select boxes unless you force it to re-render by updating the width.
351 // (See https://github.com/SteveSanderson/knockout/issues/312, http://stackoverflow.com/questions/5908494/select-only-shows-first-char-of-selected-option)
352 if (ieVersion >= 9) {
353 var originalWidth = selectElement.style.width;
354 selectElement.style.width = 0;
355 selectElement.style.width = originalWidth;
359 range: function (min, max) {
360 min = ko.utils.unwrapObservable(min);
361 max = ko.utils.unwrapObservable(max);
363 for (var i = min; i <= max; i++)
368 makeArray: function(arrayLikeObject) {
370 for (var i = 0, j = arrayLikeObject.length; i < j; i++) {
371 result.push(arrayLikeObject[i]);
378 ieVersion : ieVersion,
380 getFormFields: function(form, fieldName) {
381 var fields = ko.utils.makeArray(form.getElementsByTagName("input")).concat(ko.utils.makeArray(form.getElementsByTagName("textarea")));
382 var isMatchingField = (typeof fieldName == 'string')
383 ? function(field) { return field.name === fieldName }
384 : function(field) { return fieldName.test(field.name) }; // Treat fieldName as regex or object containing predicate
386 for (var i = fields.length - 1; i >= 0; i--) {
387 if (isMatchingField(fields[i]))
388 matches.push(fields[i]);
393 parseJson: function (jsonString) {
394 if (typeof jsonString == "string") {
395 jsonString = ko.utils.stringTrim(jsonString);
397 if (window.JSON && window.JSON.parse) // Use native parsing where available
398 return window.JSON.parse(jsonString);
399 return (new Function("return " + jsonString))(); // Fallback on less safe parsing for older browsers
405 stringifyJson: function (data, replacer, space) { // replacer and space are optional
406 if ((typeof JSON == "undefined") || (typeof JSON.stringify == "undefined"))
407 throw new Error("Cannot find JSON.stringify(). Some browsers (e.g., IE < 8) don't support it natively, but you can overcome this by adding a script reference to json2.js, downloadable from http://www.json.org/json2.js");
408 return JSON.stringify(ko.utils.unwrapObservable(data), replacer, space);
411 postJson: function (urlOrForm, data, options) {
412 options = options || {};
413 var params = options['params'] || {};
414 var includeFields = options['includeFields'] || this.fieldsIncludedWithJsonPost;
417 // If we were given a form, use its 'action' URL and pick out any requested field values
418 if((typeof urlOrForm == 'object') && (ko.utils.tagNameLower(urlOrForm) === "form")) {
419 var originalForm = urlOrForm;
420 url = originalForm.action;
421 for (var i = includeFields.length - 1; i >= 0; i--) {
422 var fields = ko.utils.getFormFields(originalForm, includeFields[i]);
423 for (var j = fields.length - 1; j >= 0; j--)
424 params[fields[j].name] = fields[j].value;
428 data = ko.utils.unwrapObservable(data);
429 var form = document.createElement("form");
430 form.style.display = "none";
432 form.method = "post";
433 for (var key in data) {
434 var input = document.createElement("input");
436 input.value = ko.utils.stringifyJson(ko.utils.unwrapObservable(data[key]));
437 form.appendChild(input);
439 for (var key in params) {
440 var input = document.createElement("input");
442 input.value = params[key];
443 form.appendChild(input);
445 document.body.appendChild(form);
446 options['submitter'] ? options['submitter'](form) : form.submit();
447 setTimeout(function () { form.parentNode.removeChild(form); }, 0);
452 ko.exportSymbol('utils', ko.utils);
453 ko.exportSymbol('utils.arrayForEach', ko.utils.arrayForEach);
454 ko.exportSymbol('utils.arrayFirst', ko.utils.arrayFirst);
455 ko.exportSymbol('utils.arrayFilter', ko.utils.arrayFilter);
456 ko.exportSymbol('utils.arrayGetDistinctValues', ko.utils.arrayGetDistinctValues);
457 ko.exportSymbol('utils.arrayIndexOf', ko.utils.arrayIndexOf);
458 ko.exportSymbol('utils.arrayMap', ko.utils.arrayMap);
459 ko.exportSymbol('utils.arrayPushAll', ko.utils.arrayPushAll);
460 ko.exportSymbol('utils.arrayRemoveItem', ko.utils.arrayRemoveItem);
461 ko.exportSymbol('utils.extend', ko.utils.extend);
462 ko.exportSymbol('utils.fieldsIncludedWithJsonPost', ko.utils.fieldsIncludedWithJsonPost);
463 ko.exportSymbol('utils.getFormFields', ko.utils.getFormFields);
464 ko.exportSymbol('utils.peekObservable', ko.utils.peekObservable);
465 ko.exportSymbol('utils.postJson', ko.utils.postJson);
466 ko.exportSymbol('utils.parseJson', ko.utils.parseJson);
467 ko.exportSymbol('utils.registerEventHandler', ko.utils.registerEventHandler);
468 ko.exportSymbol('utils.stringifyJson', ko.utils.stringifyJson);
469 ko.exportSymbol('utils.range', ko.utils.range);
470 ko.exportSymbol('utils.toggleDomNodeCssClass', ko.utils.toggleDomNodeCssClass);
471 ko.exportSymbol('utils.triggerEvent', ko.utils.triggerEvent);
472 ko.exportSymbol('utils.unwrapObservable', ko.utils.unwrapObservable);
474 if (!Function.prototype['bind']) {
475 // Function.prototype.bind is a standard part of ECMAScript 5th Edition (December 2009, http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-262.pdf)
476 // In case the browser doesn't implement it natively, provide a JavaScript implementation. This implementation is based on the one in prototype.js
477 Function.prototype['bind'] = function (object) {
478 var originalFunction = this, args = Array.prototype.slice.call(arguments), object = args.shift();
480 return originalFunction.apply(object, args.concat(Array.prototype.slice.call(arguments)));