├── .bowerrc ├── .gitignore ├── .jshintrc ├── .travis.yml ├── LICENSE ├── README.md ├── ajax-form.html ├── ajax-form.js ├── bower.json ├── demo.html ├── demo_resources ├── alertify.core.css ├── alertify.default.css ├── alertify.min.js └── sinon-server-1.10.2.js ├── grunt_tasks ├── jshint.js └── karma.js ├── gruntfile.js ├── index.html ├── package.json ├── test ├── index.html └── typical-form-tests.html └── wct.conf.js /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "../" 3 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bower_components 2 | .c9 3 | coverage 4 | node_modules 5 | .idea 6 | *.iml 7 | *.log 8 | dist -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "bitwise": true, 3 | "camelcase": false, 4 | "curly": true, 5 | "eqeqeq": true, 6 | "forin": true, 7 | "freeze": true, 8 | "immed": true, 9 | "indent": 4, 10 | "latedef": true, 11 | "newcap": true, 12 | "noarg": true, 13 | "noempty": false, 14 | "nonew": true, 15 | "plusplus": false, 16 | "quotmark": "single", 17 | "undef": true, 18 | "unused": false, 19 | "strict": false, 20 | "trailing": false, 21 | "maxparams": 3, 22 | "maxdepth": 3, 23 | "asi": false, 24 | "boss": false, 25 | "eqnull": true, 26 | "evil": false, 27 | "expr": true, 28 | "funcscope": false, 29 | "globalstrict": false, 30 | "iterator": false, 31 | "lastsemic": false, 32 | "laxbreak": false, 33 | "laxcomma": false, 34 | "loopfunc": false, 35 | "multistr": false, 36 | "notypeof": false, 37 | "proto": false, 38 | "scripturl": false, 39 | "smarttabs": false, 40 | "shadow": false, 41 | "sub": true, 42 | "supernew": false, 43 | "predef": [ 44 | "afterEach", 45 | "beforeEach", 46 | "Blob", 47 | "clearTimeout", 48 | "console", 49 | "CustomEvent", 50 | "describe", 51 | "document", 52 | "expect", 53 | "FormData", 54 | "HTMLFormElement", 55 | "it", 56 | "setTimeout", 57 | "window", 58 | "module", 59 | "XMLHttpRequest" 60 | ] 61 | } -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '0.10' 4 | before_install: 5 | - npm install 6 | script: 7 | - $(npm bin)/grunt travis 8 | env: 9 | global: 10 | - secure: rvRcNshNze/8zhMJd3Rhiv+N9u46ljNwAuE6MtpNSaNz+jLC5e7LOXV8dyPdb3ROzDql8SztpMscaEInDvqq+JvC3W0hCYd34WAQt0ZkW0udKs0tbnkNN9dFx1gTfQFsDceTiy1D7d8fQepbeSjOn9N5M1uSUCPdTw5vMFhGFEs= 11 | - secure: ed/OB9OuOsagOo3mGd39wuE8jfHv2w09olfWMeyvmYkSoGXg0EcvYdggXAkwBAlzYbm7Tm3Mwa51BmI7O8lB5iq7X/m8ILYiKJ9nksFWEANaNWV7Px6j7EvWvjppQY1H8g7x3P6VBXIJ06+LHO0LThLIDEteAae0CVuq4vap1Vg= 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Ray Nicholus 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ajax-form 2 | ========= 3 | 4 | HTML forms on performance-enhancing drugs. 5 | 6 | [![Build Status](https://travis-ci.org/rnicholus/ajax-form.svg?branch=master)](https://travis-ci.org/rnicholus/ajax-form) 7 | [![npm](https://img.shields.io/npm/v/ajax-form.svg)](https://www.npmjs.com/package/ajax-form) 8 | [![license](https://img.shields.io/badge/license-MIT-brightgreen.svg)](LICENSE) 9 | 10 | **Note: Ajax-form is not tied to Polymer. In fact, it has no dependencies at all, but should work just fine with Polymer or any other custom elements library. If you prefer to use a simple custom elements polyfill, ajax-form is also your best choice.** 11 | 12 | ## What's wrong with a traditional `
`? 13 | 1. Form submission changes/reloads the page, and it's not trivial to properly prevent this. 14 | 2. You can't send custom headers with a submitted form. 15 | 3. You can't (easily) parse the server response after a form is submitted. 16 | 4. Programmatically tracking invalid forms/fields is frustrating. 17 | 5. You can't send form data as JSON. 18 | 6. You have no opportunity to programmatically augment user-entered data before it is sent to the server. 19 | 7. Custom form elements (such as those created using the web components spec) cannot be submitted using a traditional unadulterated ``. 20 | 21 | The `ajax-form` custom element augments a traditional `` to provide additional features and solve the problems listed above. See the [API documentation page](http://ajax-form.raynicholus.com) for complete documentation and demos. 22 | 23 | ## Installation 24 | 25 | `npm install ajax-form` 26 | 27 | ## Use 28 | 29 | Use ajax-form just like you would use a traditional form, with the exception of the required `is="ajax-form"` attribute that you _must_ include in your `` element markup. Since ajax-form is a web component, you may need to include a web component polyfill, such as [webcomponents.js](http://webcomponents.org/) to ensure browsers 30 | that do not implement the WC spec are able to make use of ajax-form. Ajax-form has *no* hard 31 | dependencies. 32 | 33 | A very simple use of `ajax-form` looks just like a normal ``, with the addition of an `is` attribute: 34 | 35 | ```html 36 | 37 | 38 | ... 39 |
40 | ``` 41 | 42 | See the [API documentation page](http://ajax-form.raynicholus.com) for complete documentation and demos. 43 | 44 | 45 | ## Integration 46 | Are you developing a form field web component? Read the instructions below to ensure 47 | your field integrates properly with ajax-form. 48 | 49 | ### Submitting 50 | Your component will integrate nicely into ajax form provided your custom element 51 | exposes a `value` property that contains the current value of the field. If this 52 | is not true, then your custom field must ensure a native HTML form field is part of 53 | the light DOM. In either case, the element with the `value` property must also 54 | contain a `name` attribute. Your user/integrator will need to include an 55 | appropriate value for this field. 56 | 57 | ### Validation 58 | If your custom field exposes a native HTML form field in the light DOM, then there 59 | is nothing more to do - ajax-form will respect any validation that your user/integrator 60 | adds to the field. The constrain attribute(s) MUST be placed on the native HTML form 61 | field. 62 | 63 | If your custom field does NOT expose a native HTML form field in the light DOM by 64 | default, and you want ajax-form to respect validation constraints, then you will 65 | need to include a little code to account for this. Here are the steps to follow: 66 | 67 | 1. Add an opaque, 0x0 `` field to the light DOM, just before your field. 68 | 2. Add a `customElementRef` property to the input, with a value equal to your field. 69 | 3. Ensure the validity of the input always matches the validity of your field. You can 70 | do this via the `setCustomValidity` method present on an `HTMLInputElement`. 71 | 72 | See the [`setValidationTarget` method in the `` custom element source code](https://github.com/rnicholus/file-input/blob/1.1.4/file-input.js#L104) 73 | for an example. 74 | 75 | 76 | ## Testing 77 | ``` 78 | npm install 79 | npm install -g grunt-cli 80 | grunt 81 | ``` 82 | 83 | - Running `grunt` without any parameters will test against a few locally installed browsers (see the codebase for details). 84 | 85 | - Running `grunt shell:wctSauce` will run tests against a number of browsers in SauceLabs. Ensure you have your SauceLabs username and key attached to the proper environment variables first. 86 | -------------------------------------------------------------------------------- /ajax-form.html: -------------------------------------------------------------------------------- 1 | 89 | 265 | 266 | 267 | -------------------------------------------------------------------------------- /ajax-form.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var arrayOf = function(pseudoArray) { 3 | return (pseudoArray && [].slice.call(pseudoArray)) || []; 4 | }, 5 | 6 | // Note that _currentScript is a polyfill-specific convention 7 | currentScript = document._currentScript || document.currentScript, 8 | 9 | fire = function (node, type, _detail_) { 10 | var detail = _detail_ === null || _detail_ === undefined ? {} : _detail_, 11 | event = new CustomEvent(type, { 12 | bubbles: true, 13 | cancelable: true, 14 | detail: detail 15 | }); 16 | 17 | // hack to ensure preventDefault() in IE10+ actually sets the defaultPrevented property 18 | if (customPreventDefaultIgnored) { 19 | event.preventDefault = function () { 20 | Object.defineProperty(this, 'defaultPrevented', { 21 | get: function () { 22 | return true; 23 | } 24 | }); 25 | }; 26 | } 27 | 28 | node.dispatchEvent(event); 29 | return event; 30 | }, 31 | 32 | customPreventDefaultIgnored = (function () { 33 | var tempElement = document.createElement('div'), 34 | event = fire(tempElement, 'foobar'); 35 | 36 | event.preventDefault(); 37 | return !event.defaultPrevented; 38 | }()), 39 | 40 | getEnctype = function(ajaxForm) { 41 | var enctype = ajaxForm.getAttribute('enctype'); 42 | 43 | return enctype || 'application/x-www-form-urlencoded'; 44 | }, 45 | 46 | getValidMethod = function(method) { 47 | if (method) { 48 | var proposedMethod = method.toUpperCase(); 49 | 50 | if (['GET', 'POST', 'PUT', 'PATCH', 'DELETE'].indexOf(proposedMethod) >= 0) { 51 | return proposedMethod; 52 | } 53 | } 54 | }, 55 | 56 | importDoc = currentScript.ownerDocument, 57 | 58 | // NOTE: Safari doesn't have any visual indications when submit is blocked 59 | interceptSubmit = function(ajaxForm) { 60 | // Intercept submit event 61 | ajaxForm.addEventListener('submit', function(event) { 62 | // Stop form submission. Believe it or not, 63 | // both of these are required for some reason, 64 | // and returning false doesn't seem to reliably work. 65 | event.preventDefault(); 66 | event.stopPropagation(); 67 | 68 | // respect any field validation attributes 69 | if (ajaxForm.checkValidity()) { 70 | sendFormData(ajaxForm); 71 | } 72 | }); 73 | 74 | // Intercept native form submit function. 75 | // In order to force the browser to highlight the invalid fields, 76 | // we need to create a hidden submit button and click it if the form is invalid. 77 | var fakeSubmitEl = document.createElement('input'); 78 | fakeSubmitEl.setAttribute('type', 'submit'); 79 | fakeSubmitEl.style.display = 'none'; 80 | ajaxForm.appendChild(fakeSubmitEl); 81 | ajaxForm.submit = function() { 82 | ajaxForm._preventValidityCheck = false; 83 | if (ajaxForm.checkValidity()) { 84 | fire(ajaxForm, 'submit'); 85 | } 86 | else { 87 | ajaxForm._preventValidityCheck = true; 88 | fakeSubmitEl.click(); 89 | } 90 | }; 91 | }, 92 | 93 | isCheckboxOrRadioButton = function(element) { 94 | var elementType = element.type, 95 | role = element.getAttribute('role'); 96 | 97 | return elementType === 'checkbox' || 98 | role === 'checkbox' || 99 | elementType === 'radio' || 100 | role === 'radio'; 101 | }, 102 | 103 | maybeParseCoreDropdownMenu = function(customElement, data) { 104 | if (customElement.tagName.toLowerCase() === 'core-dropdown-menu' || 105 | customElement.tagName.toLowerCase() === 'paper-dropdown-menu') { 106 | var coreMenu = customElement.querySelector('core-menu'), 107 | selectedItem = coreMenu && coreMenu.selectedItem; 108 | 109 | if (selectedItem) { 110 | processFormValue(customElement.getAttribute('name'), selectedItem.label || selectedItem.textContent, data); 111 | } 112 | 113 | return true; 114 | } 115 | }, 116 | 117 | maybeParseCustomElementOrFileInput = function(spec) { 118 | var name = spec.customElement.getAttribute('name'); 119 | spec.name = name; 120 | 121 | if (spec.customElement.tagName.indexOf('-') >= 0 ) { 122 | if (isCheckboxOrRadioButton(spec.customElement)) { 123 | var radioValue = parseRadioElementValue(spec.customElement); 124 | if (radioValue) { 125 | processFormValue(name, radioValue, spec.data); 126 | return true; 127 | } 128 | } 129 | else if (spec.customElement.files) { 130 | maybeParseFileInput(spec); 131 | return true; 132 | } 133 | else if (!spec.customElement.files) { 134 | processFormValue(name, spec.customElement.value, spec.data); 135 | return true; 136 | } 137 | } 138 | else if (spec.customElement.files) { 139 | maybeParseFileInput(spec); 140 | return true; 141 | } 142 | }, 143 | 144 | maybeParseFileInput = function(spec) { 145 | if (spec.parseFileInputs) { 146 | processFormValue(spec.name, arrayOf(spec.customElement.files), spec.data); 147 | spec.form._fileInputFieldNames.push(spec.name); 148 | } 149 | }, 150 | 151 | parseCustomElements = function(form, parseFileInputs) { 152 | var data = {}; 153 | 154 | form._fileInputFieldNames = []; 155 | 156 | arrayOf(form.querySelectorAll('*[name]')).forEach(function(el) { 157 | if (maybeParseCoreDropdownMenu(el, data) || 158 | maybeParseCustomElementOrFileInput({ 159 | customElement: el, 160 | data: data, 161 | form: form, 162 | parseFileInputs: parseFileInputs 163 | }) 164 | ) { 165 | arrayOf(el.querySelectorAll('[name]')).forEach(function(el) { 166 | el.setAttribute('data-ajaxform-ignore', ''); 167 | }); 168 | } 169 | }); 170 | 171 | return data; 172 | }, 173 | 174 | /** 175 | * Return the value of some `HTMLElement`s value attribute if possible. 176 | * @param HTMLElement element 177 | * @return mixed The element's value attribute 178 | */ 179 | parseElementValue = function(element){ 180 | var elementValue, 181 | elementTag = element.tagName.toLowerCase(); 182 | 183 | if (elementTag === 'input' && element.type !== 'file') { 184 | elementValue = parseInputElementValue(element); 185 | } 186 | else if (elementTag === 'textarea') { 187 | elementValue = element.value || ''; 188 | } 189 | else if (elementTag === 'select') { 190 | elementValue = parseSelectElementValues(element); 191 | } 192 | 193 | return elementValue; 194 | }, 195 | 196 | parseForm = function(form, parseFileInputs) { 197 | var formObj = {}, 198 | customElementsData = parseCustomElements(form, parseFileInputs), 199 | formElements = arrayOf(form.elements).filter(function(el) { 200 | return !el.hasAttribute('data-ajaxform-ignore'); 201 | }); 202 | 203 | 204 | formElements.forEach(function(formElement) { 205 | var key = formElement.name, 206 | val = parseElementValue(formElement); 207 | 208 | if (key && val != null) { 209 | processFormValue(key, val, formObj); 210 | } 211 | }); 212 | 213 | Object.keys(customElementsData).forEach(function(fieldName) { 214 | processFormValue(fieldName, customElementsData[fieldName], formObj); 215 | }); 216 | 217 | return formObj; 218 | }, 219 | 220 | parseInputElementValue = function(element){ 221 | var elementValue, 222 | elementType = element.type; 223 | 224 | if (element.disabled === true || 225 | ['submit', 'reset', 'button', 'image'].indexOf(elementType) !== -1) { 226 | // do nothing for these button types 227 | } 228 | // support checkboxes, radio buttons or elements that behave as such 229 | else if (isCheckboxOrRadioButton(element)) { 230 | elementValue = parseRadioElementValue(element); 231 | } 232 | else { 233 | elementValue = element.value || ''; 234 | } 235 | 236 | return elementValue; 237 | }, 238 | 239 | parseRadioElementValue = function(element) { 240 | var value; 241 | if (element.checked === true) { 242 | value = element.value; 243 | } 244 | return value; 245 | }, 246 | 247 | parseSelectOptionElementValue = function(element) { 248 | var elementValue; 249 | if (element.selected === true){ 250 | elementValue = element.value; 251 | } 252 | return elementValue; 253 | }, 254 | 255 | parseSelectElementValues = function(element) { 256 | var elementValues = []; 257 | 258 | arrayOf(element.options).forEach(function(optionElement){ 259 | var tempElementValue = parseSelectOptionElementValue(optionElement); 260 | tempElementValue && elementValues.push(tempElementValue); 261 | }); 262 | 263 | return elementValues; 264 | }, 265 | 266 | processFormValue = function(key, value, store) { 267 | if (store[key]) { 268 | if (Array.isArray(store[key]) && 269 | store[key].length > 1 && 270 | Array.isArray(store[key][1])) { 271 | 272 | store[key].push([value]); 273 | } 274 | else { 275 | store[key] = [[store[key]]]; 276 | store[key].push([value]); 277 | } 278 | } 279 | else { 280 | store[key] = value; 281 | } 282 | }, 283 | 284 | sendFormData = function(ajaxForm) { 285 | var enctype = getEnctype(ajaxForm), 286 | formData = parseForm(ajaxForm, enctype === 'multipart/form-data'), 287 | submittingEvent = fire(ajaxForm, 'submitting', {formData: formData}); 288 | 289 | if (!submittingEvent.defaultPrevented) { 290 | formData = submittingEvent.detail.formData; 291 | 292 | if ('multipart/form-data' !== enctype && 293 | 'application/json' !== enctype) { 294 | 295 | sendUrlencodedForm(ajaxForm, formData); 296 | } 297 | else { 298 | if ('GET' === ajaxForm.acceptableMethod) { 299 | sendUrlencodedForm(ajaxForm, formData); 300 | } 301 | else if ('multipart/form-data' === enctype) { 302 | sendMultipartForm(ajaxForm, formData); 303 | } 304 | else if ('application/json' === enctype) { 305 | sendJsonEncodedForm(ajaxForm, formData); 306 | } 307 | } 308 | } 309 | }, 310 | 311 | sendJsonEncodedForm = function(ajaxForm, data) { 312 | sendRequest({ 313 | body: JSON.stringify(data), 314 | contentType: getEnctype(ajaxForm), 315 | form: ajaxForm 316 | }); 317 | }, 318 | 319 | sendMultipartForm = function(ajaxForm, data) { 320 | var formData = new FormData(); 321 | 322 | Object.keys(data).forEach(function(fieldName) { 323 | var fieldValue = data[fieldName]; 324 | 325 | if (Array.isArray(fieldValue)) { 326 | // If this is a file input field value, and there are no 327 | // selected files, ensure this is accounted for in the 328 | // request as an empty filename w/ an empty application/octet-stream 329 | // boundary body. This is how a native form submit accounts for an 330 | // empty file input. 331 | if (fieldValue.length === 0 && 332 | ajaxForm._fileInputFieldNames.indexOf(fieldName) >= 0) { 333 | 334 | formData.append(fieldName, 335 | new Blob([], {type : 'application/octet-stream'}), ''); 336 | } 337 | else { 338 | fieldValue.forEach(function(file) { 339 | formData.append(fieldName, file); 340 | }); 341 | } 342 | } 343 | else { 344 | formData.append(fieldName, data[fieldName]); 345 | } 346 | }); 347 | 348 | sendRequest({ 349 | body: formData, 350 | form: ajaxForm 351 | }); 352 | }, 353 | 354 | sendRequest = function(options) { 355 | var xhr = new XMLHttpRequest(), 356 | customHeaders = options.form.getAttribute('headers'); 357 | 358 | xhr.open(options.form.acceptableMethod, options.url || options.form.action); 359 | 360 | xhr.withCredentials = !!options.form.cookies; 361 | 362 | if (customHeaders) { 363 | if (typeof(customHeaders) === 'string') { 364 | customHeaders = JSON.parse(customHeaders); 365 | } 366 | 367 | Object.keys(customHeaders).forEach(function(headerName) { 368 | xhr.setRequestHeader(headerName, customHeaders[headerName]); 369 | }); 370 | } 371 | 372 | options.contentType && xhr.setRequestHeader('Content-Type', options.contentType); 373 | 374 | xhr.onreadystatechange = function() { 375 | if (xhr.readyState === 4) { 376 | fire(options.form, 'submitted', xhr); 377 | } 378 | }; 379 | 380 | xhr.send(options.body); 381 | }, 382 | 383 | sendUrlencodedForm = function(ajaxForm, formData) { 384 | var data = toQueryString(formData); 385 | 386 | if (ajaxForm.acceptableMethod === 'POST') { 387 | sendRequest({ 388 | body: data, 389 | contentType: getEnctype(ajaxForm), 390 | form: ajaxForm 391 | }); 392 | } 393 | else { 394 | sendRequest({ 395 | contentType: getEnctype(ajaxForm), 396 | form: ajaxForm, 397 | url: ajaxForm.action + (ajaxForm.action.indexOf('?') > 0 ? '&' : '?') + data 398 | }); 399 | } 400 | }, 401 | 402 | toQueryString = function(params) { 403 | var queryParams = []; 404 | 405 | Object.keys(params).forEach(function(key) { 406 | var val = params[key]; 407 | key = encodeURIComponent(key); 408 | 409 | if (val && Object.prototype.toString.call(val) === '[object Array]') { 410 | val.forEach(function(valInArray) { 411 | queryParams.push(key + '=' + encodeURIComponent(valInArray)); 412 | }); 413 | } 414 | else { 415 | queryParams.push(val == null ? key : (key + '=' + encodeURIComponent(val))); 416 | } 417 | }); 418 | 419 | return queryParams.join('&'); 420 | }, 421 | 422 | watchForInvalidFields = function (ajaxForm) { 423 | var config = {attributes: true, childList: true, characterData: false}, 424 | initialFields = arrayOf(ajaxForm.elements), 425 | invalidFields = [], 426 | 427 | listenForInvalidEvent = function (field) { 428 | field.willValidate && field.addEventListener('invalid', function () { 429 | if (ajaxForm._preventValidityCheck) { 430 | return; 431 | } 432 | 433 | invalidFields.push(field.customElementRef || field); 434 | 435 | // In case another element is invalid and the event 436 | // hasn't been triggered yet, hold off on firing the 437 | // invalid event on the custom el. 438 | clearTimeout(timer); 439 | timer = setTimeout(function () { 440 | fire(ajaxForm, 'invalid', invalidFields); 441 | invalidFields = []; 442 | console.error('Form submission blocked - constraints violation.'); 443 | }, 10); 444 | }); 445 | }, 446 | 447 | // Be sure to observe any validatable form fields added in the future 448 | mutationHandler = new window.MutationObserver(function (records) { 449 | records.forEach(function (record) { 450 | if (record.addedNodes.length) { 451 | arrayOf(record.addedNodes).forEach(function (addedNode) { 452 | addedNode.willValidate && listenForInvalidEvent(addedNode); 453 | }); 454 | } 455 | }); 456 | }), 457 | 458 | timer = null; 459 | 460 | initialFields.forEach(function (field) { 461 | listenForInvalidEvent(field); 462 | }); 463 | 464 | // pass in the target node, as well as the observer options 465 | mutationHandler.observe(ajaxForm, config); 466 | 467 | }; 468 | 469 | document.registerElement('ajax-form', { 470 | extends: 'form', 471 | prototype: Object.create(HTMLFormElement.prototype, { 472 | createdCallback: { 473 | value: function () { 474 | var templates = importDoc.querySelectorAll('.ajax-form-template'), 475 | template = templates[templates.length - 1], 476 | clone = document.importNode(template.content, true); 477 | 478 | this.appendChild(clone); 479 | 480 | var ajaxForm = this; 481 | 482 | // The method attribute set on the light-DOM `
` 483 | // can't seem to be accessed as a property of this element, 484 | // unlike other attributes. Perhaps due to the fact that 485 | // we are extending a form and a "natural" form also has a 486 | // method attr? Perhaps something special about this attr? 487 | // Need to look into this further. 488 | ajaxForm.acceptableMethod = getValidMethod(ajaxForm.getAttribute('method')); 489 | 490 | // default method is GET 491 | ajaxForm.acceptableMethod = ajaxForm.acceptableMethod || 'GET'; 492 | 493 | watchForInvalidFields(ajaxForm); 494 | interceptSubmit(ajaxForm); 495 | fire(ajaxForm, 'ready'); 496 | } 497 | } 498 | }) 499 | }); 500 | }()); 501 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ajax-form", 3 | "description": "HTML forms on performance-enhancing drugs", 4 | "main": "ajax-form.js", 5 | "authors": [ 6 | "Ray Nicholus" 7 | ], 8 | "license": "MIT", 9 | "keywords": [ 10 | "Polymer", 11 | "web-components", 12 | "form", 13 | "html", 14 | "ajax" 15 | ], 16 | "homepage": "https://github.com/rnicholus/ajax-form", 17 | "ignore": [ 18 | "**/.*", 19 | "node_modules", 20 | "bower_components", 21 | "../", 22 | "test", 23 | "tests" 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /demo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | ajax-form demo 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 46 | 47 | 48 | 49 | 50 | 51 |

ajax-form in action:

52 | 53 |

See the code for this demo in the demo.html file 54 | of the ajax-form GitHub repository.

55 | 56 |

Sending your data...

57 | 58 |
59 | 60 |
61 | 62 |
63 | 64 |
65 | 66 |
67 | 68 |
69 | 70 |
71 | 72 |
73 | Receive periodic email updates from us? 74 | yes 75 | no 76 | testcheck 77 |
78 | 79 | 80 | 81 | 82 |

Summary

83 |

The above code includes a simple HTML form that identifies itself as an `ajax-form`. 84 | As you can see, there are some standard form elements included that collect the user's 85 | name, address, favorite color, and ask them if they'd like to be added to a mailing list. 86 | The name and address fields are required for submission.

87 | 88 | 89 |

Server Handling

90 |

This form will result in a POST request with a URL-encoded payload to the "test" endpoint.

91 | 92 | 93 |

Validation

94 |

If required fields are not filled out when the user clicks "submit", an "invalid" event will 95 | be triggered on the form (passing the invalid elements as event detail), and an alert will be 96 | displayed to the user. Some browsers (not Safari) will also outline the offending fields in red.

97 | 98 |

If the form is able to be submitted and passes validation checks, a "submitted" event 99 | will be triggered on the form, the form will be hidden, and a large "Sending your data..." 100 | message will appear and fade in and out continuously until the form has been submitted.

101 | 102 |

Once the server has processed and responded to the form submit, a "submitted" event 103 | will be triggered on the form and the "Sending your data..." message will disappear. 104 | If submission was successful, a message will replace the form. If a problem 105 | occurred, the form will re-appear and an alert will be displayed to the user. 106 | In each case, the underlying `XMLHttpRequest` instance will be passed to the 107 | "submitted" event handler as event detail.

108 |
109 | 110 | 111 | 112 | 141 | 142 | 157 | 158 | 159 | -------------------------------------------------------------------------------- /demo_resources/alertify.core.css: -------------------------------------------------------------------------------- 1 | .alertify, 2 | .alertify-show, 3 | .alertify-log { 4 | -webkit-transition: all 500ms cubic-bezier(0.175, 0.885, 0.320, 1.275); 5 | -moz-transition: all 500ms cubic-bezier(0.175, 0.885, 0.320, 1.275); 6 | -ms-transition: all 500ms cubic-bezier(0.175, 0.885, 0.320, 1.275); 7 | -o-transition: all 500ms cubic-bezier(0.175, 0.885, 0.320, 1.275); 8 | transition: all 500ms cubic-bezier(0.175, 0.885, 0.320, 1.275); /* easeOutBack */ 9 | } 10 | .alertify-hide { 11 | -webkit-transition: all 250ms cubic-bezier(0.600, -0.280, 0.735, 0.045); 12 | -moz-transition: all 250ms cubic-bezier(0.600, -0.280, 0.735, 0.045); 13 | -ms-transition: all 250ms cubic-bezier(0.600, -0.280, 0.735, 0.045); 14 | -o-transition: all 250ms cubic-bezier(0.600, -0.280, 0.735, 0.045); 15 | transition: all 250ms cubic-bezier(0.600, -0.280, 0.735, 0.045); /* easeInBack */ 16 | } 17 | .alertify-log-hide { 18 | -webkit-transition: all 500ms cubic-bezier(0.600, -0.280, 0.735, 0.045); 19 | -moz-transition: all 500ms cubic-bezier(0.600, -0.280, 0.735, 0.045); 20 | -ms-transition: all 500ms cubic-bezier(0.600, -0.280, 0.735, 0.045); 21 | -o-transition: all 500ms cubic-bezier(0.600, -0.280, 0.735, 0.045); 22 | transition: all 500ms cubic-bezier(0.600, -0.280, 0.735, 0.045); /* easeInBack */ 23 | } 24 | .alertify-cover { 25 | position: fixed; z-index: 99999; 26 | top: 0; right: 0; bottom: 0; left: 0; 27 | background-color:white; 28 | filter:alpha(opacity=0); 29 | opacity:0; 30 | } 31 | .alertify-cover-hidden { 32 | display: none; 33 | } 34 | .alertify { 35 | position: fixed; z-index: 99999; 36 | top: 50px; left: 50%; 37 | width: 550px; 38 | margin-left: -275px; 39 | opacity: 1; 40 | } 41 | .alertify-hidden { 42 | -webkit-transform: translate(0,-150px); 43 | -moz-transform: translate(0,-150px); 44 | -ms-transform: translate(0,-150px); 45 | -o-transform: translate(0,-150px); 46 | transform: translate(0,-150px); 47 | opacity: 0; 48 | display: none; 49 | } 50 | /* overwrite display: none; for everything except IE6-8 */ 51 | :root *> .alertify-hidden { 52 | display: block; 53 | visibility: hidden; 54 | } 55 | .alertify-logs { 56 | position: fixed; 57 | z-index: 5000; 58 | bottom: 10px; 59 | right: 10px; 60 | width: 300px; 61 | } 62 | .alertify-logs-hidden { 63 | display: none; 64 | } 65 | .alertify-log { 66 | display: block; 67 | margin-top: 10px; 68 | position: relative; 69 | right: -300px; 70 | opacity: 0; 71 | } 72 | .alertify-log-show { 73 | right: 0; 74 | opacity: 1; 75 | } 76 | .alertify-log-hide { 77 | -webkit-transform: translate(300px, 0); 78 | -moz-transform: translate(300px, 0); 79 | -ms-transform: translate(300px, 0); 80 | -o-transform: translate(300px, 0); 81 | transform: translate(300px, 0); 82 | opacity: 0; 83 | } 84 | .alertify-dialog { 85 | padding: 25px; 86 | } 87 | .alertify-resetFocus { 88 | border: 0; 89 | clip: rect(0 0 0 0); 90 | height: 1px; 91 | margin: -1px; 92 | overflow: hidden; 93 | padding: 0; 94 | position: absolute; 95 | width: 1px; 96 | } 97 | .alertify-inner { 98 | text-align: center; 99 | } 100 | .alertify-text { 101 | margin-bottom: 15px; 102 | width: 100%; 103 | -webkit-box-sizing: border-box; 104 | -moz-box-sizing: border-box; 105 | box-sizing: border-box; 106 | font-size: 100%; 107 | } 108 | .alertify-buttons { 109 | } 110 | .alertify-button, 111 | .alertify-button:hover, 112 | .alertify-button:active, 113 | .alertify-button:visited { 114 | background: none; 115 | text-decoration: none; 116 | border: none; 117 | /* line-height and font-size for input button */ 118 | line-height: 1.5; 119 | font-size: 100%; 120 | display: inline-block; 121 | cursor: pointer; 122 | margin-left: 5px; 123 | } 124 | 125 | @media only screen and (max-width: 680px) { 126 | .alertify, 127 | .alertify-logs { 128 | width: 90%; 129 | -webkit-box-sizing: border-box; 130 | -moz-box-sizing: border-box; 131 | box-sizing: border-box; 132 | } 133 | .alertify { 134 | left: 5%; 135 | margin: 0; 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /demo_resources/alertify.default.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Default Look and Feel 3 | */ 4 | .alertify, 5 | .alertify-log { 6 | font-family: sans-serif; 7 | } 8 | .alertify { 9 | background: #FFF; 10 | border: 10px solid #333; /* browsers that don't support rgba */ 11 | border: 10px solid rgba(0,0,0,.7); 12 | border-radius: 8px; 13 | box-shadow: 0 3px 3px rgba(0,0,0,.3); 14 | -webkit-background-clip: padding; /* Safari 4? Chrome 6? */ 15 | -moz-background-clip: padding; /* Firefox 3.6 */ 16 | background-clip: padding-box; /* Firefox 4, Safari 5, Opera 10, IE 9 */ 17 | } 18 | .alertify-text { 19 | border: 1px solid #CCC; 20 | padding: 10px; 21 | border-radius: 4px; 22 | } 23 | .alertify-button { 24 | border-radius: 4px; 25 | color: #FFF; 26 | font-weight: bold; 27 | padding: 6px 15px; 28 | text-decoration: none; 29 | text-shadow: 1px 1px 0 rgba(0,0,0,.5); 30 | box-shadow: inset 0 1px 0 0 rgba(255,255,255,.5); 31 | background-image: -webkit-linear-gradient(top, rgba(255,255,255,.3), rgba(255,255,255,0)); 32 | background-image: -moz-linear-gradient(top, rgba(255,255,255,.3), rgba(255,255,255,0)); 33 | background-image: -ms-linear-gradient(top, rgba(255,255,255,.3), rgba(255,255,255,0)); 34 | background-image: -o-linear-gradient(top, rgba(255,255,255,.3), rgba(255,255,255,0)); 35 | background-image: linear-gradient(top, rgba(255,255,255,.3), rgba(255,255,255,0)); 36 | } 37 | .alertify-button:hover, 38 | .alertify-button:focus { 39 | outline: none; 40 | background-image: -webkit-linear-gradient(top, rgba(0,0,0,.1), rgba(0,0,0,0)); 41 | background-image: -moz-linear-gradient(top, rgba(0,0,0,.1), rgba(0,0,0,0)); 42 | background-image: -ms-linear-gradient(top, rgba(0,0,0,.1), rgba(0,0,0,0)); 43 | background-image: -o-linear-gradient(top, rgba(0,0,0,.1), rgba(0,0,0,0)); 44 | background-image: linear-gradient(top, rgba(0,0,0,.1), rgba(0,0,0,0)); 45 | } 46 | .alertify-button:focus { 47 | box-shadow: 0 0 15px #2B72D5; 48 | } 49 | .alertify-button:active { 50 | position: relative; 51 | box-shadow: inset 0 2px 4px rgba(0,0,0,.15), 0 1px 2px rgba(0,0,0,.05); 52 | } 53 | .alertify-button-cancel, 54 | .alertify-button-cancel:hover, 55 | .alertify-button-cancel:focus { 56 | background-color: #FE1A00; 57 | border: 1px solid #D83526; 58 | } 59 | .alertify-button-ok, 60 | .alertify-button-ok:hover, 61 | .alertify-button-ok:focus { 62 | background-color: #5CB811; 63 | border: 1px solid #3B7808; 64 | } 65 | 66 | .alertify-log { 67 | background: #1F1F1F; 68 | background: rgba(0,0,0,.9); 69 | padding: 15px; 70 | border-radius: 4px; 71 | color: #FFF; 72 | text-shadow: -1px -1px 0 rgba(0,0,0,.5); 73 | } 74 | .alertify-log-error { 75 | background: #FE1A00; 76 | background: rgba(254,26,0,.9); 77 | } 78 | .alertify-log-success { 79 | background: #5CB811; 80 | background: rgba(92,184,17,.9); 81 | } -------------------------------------------------------------------------------- /demo_resources/alertify.min.js: -------------------------------------------------------------------------------- 1 | /*! alertify - v0.3.11 - 2013-10-08 */ 2 | !function(a,b){"use strict";var c,d=a.document;c=function(){var c,e,f,g,h,i,j,k,l,m,n,o,p,q={},r={},s=!1,t={ENTER:13,ESC:27,SPACE:32},u=[];return r={buttons:{holder:'',submit:'',ok:'',cancel:''},input:'
',message:'

{{message}}

',log:'
{{message}}
'},p=function(){var a,c,e=!1,f=d.createElement("fakeelement"),g={WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"otransitionend",transition:"transitionend"};for(a in g)if(f.style[a]!==b){c=g[a],e=!0;break}return{type:c,supported:e}},c=function(a){return d.getElementById(a)},q={labels:{ok:"OK",cancel:"Cancel"},delay:5e3,buttonReverse:!1,buttonFocus:"ok",transition:b,addListeners:function(a){var b,c,i,j,k,l="undefined"!=typeof f,m="undefined"!=typeof e,n="undefined"!=typeof o,p="",q=this;b=function(b){return"undefined"!=typeof b.preventDefault&&b.preventDefault(),i(b),"undefined"!=typeof o&&(p=o.value),"function"==typeof a&&("undefined"!=typeof o?a(!0,p):a(!0)),!1},c=function(b){return"undefined"!=typeof b.preventDefault&&b.preventDefault(),i(b),"function"==typeof a&&a(!1),!1},i=function(){q.hide(),q.unbind(d.body,"keyup",j),q.unbind(g,"focus",k),l&&q.unbind(f,"click",b),m&&q.unbind(e,"click",c)},j=function(a){var d=a.keyCode;(d===t.SPACE&&!n||n&&d===t.ENTER)&&b(a),d===t.ESC&&m&&c(a)},k=function(){n?o.focus():!m||q.buttonReverse?f.focus():e.focus()},this.bind(g,"focus",k),this.bind(h,"focus",k),l&&this.bind(f,"click",b),m&&this.bind(e,"click",c),this.bind(d.body,"keyup",j),this.transition.supported||this.setFocus()},bind:function(a,b,c){"function"==typeof a.addEventListener?a.addEventListener(b,c,!1):a.attachEvent&&a.attachEvent("on"+b,c)},handleErrors:function(){if("undefined"!=typeof a.onerror){var b=this;return a.onerror=function(a,c,d){b.error("["+a+" on line "+d+" of "+c+"]",0)},!0}return!1},appendButtons:function(a,b){return this.buttonReverse?b+a:a+b},build:function(a){var b="",c=a.type,d=a.message,e=a.cssClass||"";switch(b+='
',b+='Reset Focus',"none"===q.buttonFocus&&(b+=''),"prompt"===c&&(b+='
'),b+='
',b+=r.message.replace("{{message}}",d),"prompt"===c&&(b+=r.input),b+=r.buttons.holder,b+="
","prompt"===c&&(b+="
"),b+='Reset Focus',b+="
",c){case"confirm":b=b.replace("{{buttons}}",this.appendButtons(r.buttons.cancel,r.buttons.ok)),b=b.replace("{{ok}}",this.labels.ok).replace("{{cancel}}",this.labels.cancel);break;case"prompt":b=b.replace("{{buttons}}",this.appendButtons(r.buttons.cancel,r.buttons.submit)),b=b.replace("{{ok}}",this.labels.ok).replace("{{cancel}}",this.labels.cancel);break;case"alert":b=b.replace("{{buttons}}",r.buttons.ok),b=b.replace("{{ok}}",this.labels.ok)}return l.className="alertify alertify-"+c+" "+e,k.className="alertify-cover",b},close:function(a,b){var c,d,e=b&&!isNaN(b)?+b:this.delay,f=this;this.bind(a,"click",function(){c(a)}),d=function(a){a.stopPropagation(),f.unbind(this,f.transition.type,d),m.removeChild(this),m.hasChildNodes()||(m.className+=" alertify-logs-hidden")},c=function(a){"undefined"!=typeof a&&a.parentNode===m&&(f.transition.supported?(f.bind(a,f.transition.type,d),a.className+=" alertify-log-hide"):(m.removeChild(a),m.hasChildNodes()||(m.className+=" alertify-logs-hidden")))},0!==b&&setTimeout(function(){c(a)},e)},dialog:function(a,b,c,e,f){j=d.activeElement;var g=function(){m&&null!==m.scrollTop&&k&&null!==k.scrollTop||g()};if("string"!=typeof a)throw new Error("message must be a string");if("string"!=typeof b)throw new Error("type must be a string");if("undefined"!=typeof c&&"function"!=typeof c)throw new Error("fn must be a function");return this.init(),g(),u.push({type:b,message:a,callback:c,placeholder:e,cssClass:f}),s||this.setup(),this},extend:function(a){if("string"!=typeof a)throw new Error("extend method must have exactly one paramter");return function(b,c){return this.log(b,a,c),this}},hide:function(){var a,b=this;u.splice(0,1),u.length>0?this.setup(!0):(s=!1,a=function(c){c.stopPropagation(),b.unbind(l,b.transition.type,a)},this.transition.supported?(this.bind(l,this.transition.type,a),l.className="alertify alertify-hide alertify-hidden"):l.className="alertify alertify-hide alertify-hidden alertify-isHidden",k.className="alertify-cover alertify-cover-hidden",j.focus())},init:function(){d.createElement("nav"),d.createElement("article"),d.createElement("section"),null==c("alertify-cover")&&(k=d.createElement("div"),k.setAttribute("id","alertify-cover"),k.className="alertify-cover alertify-cover-hidden",d.body.appendChild(k)),null==c("alertify")&&(s=!1,u=[],l=d.createElement("section"),l.setAttribute("id","alertify"),l.className="alertify alertify-hidden",d.body.appendChild(l)),null==c("alertify-logs")&&(m=d.createElement("section"),m.setAttribute("id","alertify-logs"),m.className="alertify-logs alertify-logs-hidden",d.body.appendChild(m)),d.body.setAttribute("tabindex","0"),this.transition=p()},log:function(a,b,c){var d=function(){m&&null!==m.scrollTop||d()};return this.init(),d(),m.className="alertify-logs",this.notify(a,b,c),this},notify:function(a,b,c){var e=d.createElement("article");e.className="alertify-log"+("string"==typeof b&&""!==b?" alertify-log-"+b:""),e.innerHTML=a,m.appendChild(e),setTimeout(function(){e.className=e.className+" alertify-log-show"},50),this.close(e,c)},set:function(a){var b;if("object"!=typeof a&&a instanceof Array)throw new Error("args must be an object");for(b in a)a.hasOwnProperty(b)&&(this[b]=a[b])},setFocus:function(){o?(o.focus(),o.select()):i.focus()},setup:function(a){var d,j=u[0],k=this;s=!0,d=function(a){a.stopPropagation(),k.setFocus(),k.unbind(l,k.transition.type,d)},this.transition.supported&&!a&&this.bind(l,this.transition.type,d),l.innerHTML=this.build(j),g=c("alertify-resetFocus"),h=c("alertify-resetFocusBack"),f=c("alertify-ok")||b,e=c("alertify-cancel")||b,i="cancel"===q.buttonFocus?e:"none"===q.buttonFocus?c("alertify-noneFocus"):f,o=c("alertify-text")||b,n=c("alertify-form")||b,"string"==typeof j.placeholder&&""!==j.placeholder&&(o.value=j.placeholder),a&&this.setFocus(),this.addListeners(j.callback)},unbind:function(a,b,c){"function"==typeof a.removeEventListener?a.removeEventListener(b,c,!1):a.detachEvent&&a.detachEvent("on"+b,c)}},{alert:function(a,b,c){return q.dialog(a,"alert",b,"",c),this},confirm:function(a,b,c){return q.dialog(a,"confirm",b,"",c),this},extend:q.extend,init:q.init,log:function(a,b,c){return q.log(a,b,c),this},prompt:function(a,b,c,d){return q.dialog(a,"prompt",b,c,d),this},success:function(a,b){return q.log(a,"success",b),this},error:function(a,b){return q.log(a,"error",b),this},set:function(a){q.set(a)},labels:q.labels,debug:q.handleErrors}},"function"==typeof define?define([],function(){return new c}):"undefined"==typeof a.alertify&&(a.alertify=new c)}(this); -------------------------------------------------------------------------------- /demo_resources/sinon-server-1.10.2.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Sinon.JS 1.10.2, 2014/06/02 3 | * 4 | * @author Christian Johansen (christian@cjohansen.no) 5 | * @author Contributors: https://github.com/cjohansen/Sinon.JS/blob/master/AUTHORS 6 | * 7 | * (The BSD License) 8 | * 9 | * Copyright (c) 2010-2014, Christian Johansen, christian@cjohansen.no 10 | * All rights reserved. 11 | * 12 | * Redistribution and use in source and binary forms, with or without modification, 13 | * are permitted provided that the following conditions are met: 14 | * 15 | * * Redistributions of source code must retain the above copyright notice, 16 | * this list of conditions and the following disclaimer. 17 | * * Redistributions in binary form must reproduce the above copyright notice, 18 | * this list of conditions and the following disclaimer in the documentation 19 | * and/or other materials provided with the distribution. 20 | * * Neither the name of Christian Johansen nor the names of his contributors 21 | * may be used to endorse or promote products derived from this software 22 | * without specific prior written permission. 23 | * 24 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 25 | * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 26 | * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 27 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 28 | * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 29 | * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 30 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 31 | * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 32 | * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 33 | * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 34 | */ 35 | 36 | /*jslint eqeqeq: false, onevar: false, forin: true, nomen: false, regexp: false, plusplus: false*/ 37 | /*global module, require, __dirname, document*/ 38 | /** 39 | * Sinon core utilities. For internal use only. 40 | * 41 | * @author Christian Johansen (christian@cjohansen.no) 42 | * @license BSD 43 | * 44 | * Copyright (c) 2010-2013 Christian Johansen 45 | */ 46 | 47 | var sinon = (function (formatio) { 48 | var div = typeof document != "undefined" && document.createElement("div"); 49 | var hasOwn = Object.prototype.hasOwnProperty; 50 | 51 | function isDOMNode(obj) { 52 | var success = false; 53 | 54 | try { 55 | obj.appendChild(div); 56 | success = div.parentNode == obj; 57 | } catch (e) { 58 | return false; 59 | } finally { 60 | try { 61 | obj.removeChild(div); 62 | } catch (e) { 63 | // Remove failed, not much we can do about that 64 | } 65 | } 66 | 67 | return success; 68 | } 69 | 70 | function isElement(obj) { 71 | return div && obj && obj.nodeType === 1 && isDOMNode(obj); 72 | } 73 | 74 | function isFunction(obj) { 75 | return typeof obj === "function" || !!(obj && obj.constructor && obj.call && obj.apply); 76 | } 77 | 78 | function isReallyNaN(val) { 79 | return typeof val === 'number' && isNaN(val); 80 | } 81 | 82 | function mirrorProperties(target, source) { 83 | for (var prop in source) { 84 | if (!hasOwn.call(target, prop)) { 85 | target[prop] = source[prop]; 86 | } 87 | } 88 | } 89 | 90 | function isRestorable (obj) { 91 | return typeof obj === "function" && typeof obj.restore === "function" && obj.restore.sinon; 92 | } 93 | 94 | var sinon = { 95 | wrapMethod: function wrapMethod(object, property, method) { 96 | if (!object) { 97 | throw new TypeError("Should wrap property of object"); 98 | } 99 | 100 | if (typeof method != "function") { 101 | throw new TypeError("Method wrapper should be function"); 102 | } 103 | 104 | var wrappedMethod = object[property], 105 | error; 106 | 107 | if (!isFunction(wrappedMethod)) { 108 | error = new TypeError("Attempted to wrap " + (typeof wrappedMethod) + " property " + 109 | property + " as function"); 110 | } else if (wrappedMethod.restore && wrappedMethod.restore.sinon) { 111 | error = new TypeError("Attempted to wrap " + property + " which is already wrapped"); 112 | } else if (wrappedMethod.calledBefore) { 113 | var verb = !!wrappedMethod.returns ? "stubbed" : "spied on"; 114 | error = new TypeError("Attempted to wrap " + property + " which is already " + verb); 115 | } 116 | 117 | if (error) { 118 | if (wrappedMethod && wrappedMethod._stack) { 119 | error.stack += '\n--------------\n' + wrappedMethod._stack; 120 | } 121 | throw error; 122 | } 123 | 124 | // IE 8 does not support hasOwnProperty on the window object and Firefox has a problem 125 | // when using hasOwn.call on objects from other frames. 126 | var owned = object.hasOwnProperty ? object.hasOwnProperty(property) : hasOwn.call(object, property); 127 | object[property] = method; 128 | method.displayName = property; 129 | // Set up a stack trace which can be used later to find what line of 130 | // code the original method was created on. 131 | method._stack = (new Error('Stack Trace for original')).stack; 132 | 133 | method.restore = function () { 134 | // For prototype properties try to reset by delete first. 135 | // If this fails (ex: localStorage on mobile safari) then force a reset 136 | // via direct assignment. 137 | if (!owned) { 138 | delete object[property]; 139 | } 140 | if (object[property] === method) { 141 | object[property] = wrappedMethod; 142 | } 143 | }; 144 | 145 | method.restore.sinon = true; 146 | mirrorProperties(method, wrappedMethod); 147 | 148 | return method; 149 | }, 150 | 151 | extend: function extend(target) { 152 | for (var i = 1, l = arguments.length; i < l; i += 1) { 153 | for (var prop in arguments[i]) { 154 | if (arguments[i].hasOwnProperty(prop)) { 155 | target[prop] = arguments[i][prop]; 156 | } 157 | 158 | // DONT ENUM bug, only care about toString 159 | if (arguments[i].hasOwnProperty("toString") && 160 | arguments[i].toString != target.toString) { 161 | target.toString = arguments[i].toString; 162 | } 163 | } 164 | } 165 | 166 | return target; 167 | }, 168 | 169 | create: function create(proto) { 170 | var F = function () {}; 171 | F.prototype = proto; 172 | return new F(); 173 | }, 174 | 175 | deepEqual: function deepEqual(a, b) { 176 | if (sinon.match && sinon.match.isMatcher(a)) { 177 | return a.test(b); 178 | } 179 | 180 | if (typeof a != 'object' || typeof b != 'object') { 181 | if (isReallyNaN(a) && isReallyNaN(b)) { 182 | return true; 183 | } else { 184 | return a === b; 185 | } 186 | } 187 | 188 | if (isElement(a) || isElement(b)) { 189 | return a === b; 190 | } 191 | 192 | if (a === b) { 193 | return true; 194 | } 195 | 196 | if ((a === null && b !== null) || (a !== null && b === null)) { 197 | return false; 198 | } 199 | 200 | if (a instanceof RegExp && b instanceof RegExp) { 201 | return (a.source === b.source) && (a.global === b.global) && 202 | (a.ignoreCase === b.ignoreCase) && (a.multiline === b.multiline); 203 | } 204 | 205 | var aString = Object.prototype.toString.call(a); 206 | if (aString != Object.prototype.toString.call(b)) { 207 | return false; 208 | } 209 | 210 | if (aString == "[object Date]") { 211 | return a.valueOf() === b.valueOf(); 212 | } 213 | 214 | var prop, aLength = 0, bLength = 0; 215 | 216 | if (aString == "[object Array]" && a.length !== b.length) { 217 | return false; 218 | } 219 | 220 | for (prop in a) { 221 | aLength += 1; 222 | 223 | if (!(prop in b)) { 224 | return false; 225 | } 226 | 227 | if (!deepEqual(a[prop], b[prop])) { 228 | return false; 229 | } 230 | } 231 | 232 | for (prop in b) { 233 | bLength += 1; 234 | } 235 | 236 | return aLength == bLength; 237 | }, 238 | 239 | functionName: function functionName(func) { 240 | var name = func.displayName || func.name; 241 | 242 | // Use function decomposition as a last resort to get function 243 | // name. Does not rely on function decomposition to work - if it 244 | // doesn't debugging will be slightly less informative 245 | // (i.e. toString will say 'spy' rather than 'myFunc'). 246 | if (!name) { 247 | var matches = func.toString().match(/function ([^\s\(]+)/); 248 | name = matches && matches[1]; 249 | } 250 | 251 | return name; 252 | }, 253 | 254 | functionToString: function toString() { 255 | if (this.getCall && this.callCount) { 256 | var thisValue, prop, i = this.callCount; 257 | 258 | while (i--) { 259 | thisValue = this.getCall(i).thisValue; 260 | 261 | for (prop in thisValue) { 262 | if (thisValue[prop] === this) { 263 | return prop; 264 | } 265 | } 266 | } 267 | } 268 | 269 | return this.displayName || "sinon fake"; 270 | }, 271 | 272 | getConfig: function (custom) { 273 | var config = {}; 274 | custom = custom || {}; 275 | var defaults = sinon.defaultConfig; 276 | 277 | for (var prop in defaults) { 278 | if (defaults.hasOwnProperty(prop)) { 279 | config[prop] = custom.hasOwnProperty(prop) ? custom[prop] : defaults[prop]; 280 | } 281 | } 282 | 283 | return config; 284 | }, 285 | 286 | format: function (val) { 287 | return "" + val; 288 | }, 289 | 290 | defaultConfig: { 291 | injectIntoThis: true, 292 | injectInto: null, 293 | properties: ["spy", "stub", "mock", "clock", "server", "requests"], 294 | useFakeTimers: true, 295 | useFakeServer: true 296 | }, 297 | 298 | timesInWords: function timesInWords(count) { 299 | return count == 1 && "once" || 300 | count == 2 && "twice" || 301 | count == 3 && "thrice" || 302 | (count || 0) + " times"; 303 | }, 304 | 305 | calledInOrder: function (spies) { 306 | for (var i = 1, l = spies.length; i < l; i++) { 307 | if (!spies[i - 1].calledBefore(spies[i]) || !spies[i].called) { 308 | return false; 309 | } 310 | } 311 | 312 | return true; 313 | }, 314 | 315 | orderByFirstCall: function (spies) { 316 | return spies.sort(function (a, b) { 317 | // uuid, won't ever be equal 318 | var aCall = a.getCall(0); 319 | var bCall = b.getCall(0); 320 | var aId = aCall && aCall.callId || -1; 321 | var bId = bCall && bCall.callId || -1; 322 | 323 | return aId < bId ? -1 : 1; 324 | }); 325 | }, 326 | 327 | log: function () {}, 328 | 329 | logError: function (label, err) { 330 | var msg = label + " threw exception: "; 331 | sinon.log(msg + "[" + err.name + "] " + err.message); 332 | if (err.stack) { sinon.log(err.stack); } 333 | 334 | setTimeout(function () { 335 | err.message = msg + err.message; 336 | throw err; 337 | }, 0); 338 | }, 339 | 340 | typeOf: function (value) { 341 | if (value === null) { 342 | return "null"; 343 | } 344 | else if (value === undefined) { 345 | return "undefined"; 346 | } 347 | var string = Object.prototype.toString.call(value); 348 | return string.substring(8, string.length - 1).toLowerCase(); 349 | }, 350 | 351 | createStubInstance: function (constructor) { 352 | if (typeof constructor !== "function") { 353 | throw new TypeError("The constructor should be a function."); 354 | } 355 | return sinon.stub(sinon.create(constructor.prototype)); 356 | }, 357 | 358 | restore: function (object) { 359 | if (object !== null && typeof object === "object") { 360 | for (var prop in object) { 361 | if (isRestorable(object[prop])) { 362 | object[prop].restore(); 363 | } 364 | } 365 | } 366 | else if (isRestorable(object)) { 367 | object.restore(); 368 | } 369 | } 370 | }; 371 | 372 | var isNode = typeof module !== "undefined" && module.exports && typeof require == "function"; 373 | var isAMD = typeof define === 'function' && typeof define.amd === 'object' && define.amd; 374 | 375 | function makePublicAPI(require, exports, module) { 376 | module.exports = sinon; 377 | sinon.spy = require("./sinon/spy"); 378 | sinon.spyCall = require("./sinon/call"); 379 | sinon.behavior = require("./sinon/behavior"); 380 | sinon.stub = require("./sinon/stub"); 381 | sinon.mock = require("./sinon/mock"); 382 | sinon.collection = require("./sinon/collection"); 383 | sinon.assert = require("./sinon/assert"); 384 | sinon.sandbox = require("./sinon/sandbox"); 385 | sinon.test = require("./sinon/test"); 386 | sinon.testCase = require("./sinon/test_case"); 387 | sinon.match = require("./sinon/match"); 388 | } 389 | 390 | if (isAMD) { 391 | define(makePublicAPI); 392 | } else if (isNode) { 393 | try { 394 | formatio = require("formatio"); 395 | } catch (e) {} 396 | makePublicAPI(require, exports, module); 397 | } 398 | 399 | if (formatio) { 400 | var formatter = formatio.configure({ quoteStrings: false }); 401 | sinon.format = function () { 402 | return formatter.ascii.apply(formatter, arguments); 403 | }; 404 | } else if (isNode) { 405 | try { 406 | var util = require("util"); 407 | sinon.format = function (value) { 408 | return typeof value == "object" && value.toString === Object.prototype.toString ? util.inspect(value) : value; 409 | }; 410 | } catch (e) { 411 | /* Node, but no util module - would be very old, but better safe than 412 | sorry */ 413 | } 414 | } 415 | 416 | return sinon; 417 | }(typeof formatio == "object" && formatio)); 418 | 419 | /*jslint eqeqeq: false, onevar: false*/ 420 | /*global sinon, module, require, ActiveXObject, XMLHttpRequest, DOMParser*/ 421 | /** 422 | * Minimal Event interface implementation 423 | * 424 | * Original implementation by Sven Fuchs: https://gist.github.com/995028 425 | * Modifications and tests by Christian Johansen. 426 | * 427 | * @author Sven Fuchs (svenfuchs@artweb-design.de) 428 | * @author Christian Johansen (christian@cjohansen.no) 429 | * @license BSD 430 | * 431 | * Copyright (c) 2011 Sven Fuchs, Christian Johansen 432 | */ 433 | 434 | if (typeof sinon == "undefined") { 435 | this.sinon = {}; 436 | } 437 | 438 | (function () { 439 | var push = [].push; 440 | 441 | sinon.Event = function Event(type, bubbles, cancelable, target) { 442 | this.initEvent(type, bubbles, cancelable, target); 443 | }; 444 | 445 | sinon.Event.prototype = { 446 | initEvent: function(type, bubbles, cancelable, target) { 447 | this.type = type; 448 | this.bubbles = bubbles; 449 | this.cancelable = cancelable; 450 | this.target = target; 451 | }, 452 | 453 | stopPropagation: function () {}, 454 | 455 | preventDefault: function () { 456 | this.defaultPrevented = true; 457 | } 458 | }; 459 | 460 | sinon.ProgressEvent = function ProgressEvent(type, progressEventRaw, target) { 461 | this.initEvent(type, false, false, target); 462 | this.loaded = progressEventRaw.loaded || null; 463 | this.total = progressEventRaw.total || null; 464 | }; 465 | 466 | sinon.ProgressEvent.prototype = new sinon.Event(); 467 | 468 | sinon.ProgressEvent.prototype.constructor = sinon.ProgressEvent; 469 | 470 | sinon.CustomEvent = function CustomEvent(type, customData, target) { 471 | this.initEvent(type, false, false, target); 472 | this.detail = customData.detail || null; 473 | }; 474 | 475 | sinon.CustomEvent.prototype = new sinon.Event(); 476 | 477 | sinon.CustomEvent.prototype.constructor = sinon.CustomEvent; 478 | 479 | sinon.EventTarget = { 480 | addEventListener: function addEventListener(event, listener) { 481 | this.eventListeners = this.eventListeners || {}; 482 | this.eventListeners[event] = this.eventListeners[event] || []; 483 | push.call(this.eventListeners[event], listener); 484 | }, 485 | 486 | removeEventListener: function removeEventListener(event, listener) { 487 | var listeners = this.eventListeners && this.eventListeners[event] || []; 488 | 489 | for (var i = 0, l = listeners.length; i < l; ++i) { 490 | if (listeners[i] == listener) { 491 | return listeners.splice(i, 1); 492 | } 493 | } 494 | }, 495 | 496 | dispatchEvent: function dispatchEvent(event) { 497 | var type = event.type; 498 | var listeners = this.eventListeners && this.eventListeners[type] || []; 499 | 500 | for (var i = 0; i < listeners.length; i++) { 501 | if (typeof listeners[i] == "function") { 502 | listeners[i].call(this, event); 503 | } else { 504 | listeners[i].handleEvent(event); 505 | } 506 | } 507 | 508 | return !!event.defaultPrevented; 509 | } 510 | }; 511 | }()); 512 | 513 | /** 514 | * @depend ../../sinon.js 515 | * @depend event.js 516 | */ 517 | /*jslint eqeqeq: false, onevar: false*/ 518 | /*global sinon, module, require, ActiveXObject, XMLHttpRequest, DOMParser*/ 519 | /** 520 | * Fake XMLHttpRequest object 521 | * 522 | * @author Christian Johansen (christian@cjohansen.no) 523 | * @license BSD 524 | * 525 | * Copyright (c) 2010-2013 Christian Johansen 526 | */ 527 | 528 | // wrapper for global 529 | (function(global) { 530 | if (typeof sinon === "undefined") { 531 | global.sinon = {}; 532 | } 533 | 534 | var supportsProgress = typeof ProgressEvent !== "undefined"; 535 | var supportsCustomEvent = typeof CustomEvent !== "undefined"; 536 | sinon.xhr = { XMLHttpRequest: global.XMLHttpRequest }; 537 | var xhr = sinon.xhr; 538 | xhr.GlobalXMLHttpRequest = global.XMLHttpRequest; 539 | xhr.GlobalActiveXObject = global.ActiveXObject; 540 | xhr.supportsActiveX = typeof xhr.GlobalActiveXObject != "undefined"; 541 | xhr.supportsXHR = typeof xhr.GlobalXMLHttpRequest != "undefined"; 542 | xhr.workingXHR = xhr.supportsXHR ? xhr.GlobalXMLHttpRequest : xhr.supportsActiveX 543 | ? function() { return new xhr.GlobalActiveXObject("MSXML2.XMLHTTP.3.0") } : false; 544 | xhr.supportsCORS = 'withCredentials' in (new sinon.xhr.GlobalXMLHttpRequest()); 545 | 546 | /*jsl:ignore*/ 547 | var unsafeHeaders = { 548 | "Accept-Charset": true, 549 | "Accept-Encoding": true, 550 | "Connection": true, 551 | "Content-Length": true, 552 | "Cookie": true, 553 | "Cookie2": true, 554 | "Content-Transfer-Encoding": true, 555 | "Date": true, 556 | "Expect": true, 557 | "Host": true, 558 | "Keep-Alive": true, 559 | "Referer": true, 560 | "TE": true, 561 | "Trailer": true, 562 | "Transfer-Encoding": true, 563 | "Upgrade": true, 564 | "User-Agent": true, 565 | "Via": true 566 | }; 567 | /*jsl:end*/ 568 | 569 | function FakeXMLHttpRequest() { 570 | this.readyState = FakeXMLHttpRequest.UNSENT; 571 | this.requestHeaders = {}; 572 | this.requestBody = null; 573 | this.status = 0; 574 | this.statusText = ""; 575 | this.upload = new UploadProgress(); 576 | if (sinon.xhr.supportsCORS) { 577 | this.withCredentials = false; 578 | } 579 | 580 | 581 | var xhr = this; 582 | var events = ["loadstart", "load", "abort", "loadend"]; 583 | 584 | function addEventListener(eventName) { 585 | xhr.addEventListener(eventName, function (event) { 586 | var listener = xhr["on" + eventName]; 587 | 588 | if (listener && typeof listener == "function") { 589 | listener.call(this, event); 590 | } 591 | }); 592 | } 593 | 594 | for (var i = events.length - 1; i >= 0; i--) { 595 | addEventListener(events[i]); 596 | } 597 | 598 | if (typeof FakeXMLHttpRequest.onCreate == "function") { 599 | FakeXMLHttpRequest.onCreate(this); 600 | } 601 | } 602 | 603 | // An upload object is created for each 604 | // FakeXMLHttpRequest and allows upload 605 | // events to be simulated using uploadProgress 606 | // and uploadError. 607 | function UploadProgress() { 608 | this.eventListeners = { 609 | "progress": [], 610 | "load": [], 611 | "abort": [], 612 | "error": [] 613 | } 614 | } 615 | 616 | UploadProgress.prototype.addEventListener = function(event, listener) { 617 | this.eventListeners[event].push(listener); 618 | }; 619 | 620 | UploadProgress.prototype.removeEventListener = function(event, listener) { 621 | var listeners = this.eventListeners[event] || []; 622 | 623 | for (var i = 0, l = listeners.length; i < l; ++i) { 624 | if (listeners[i] == listener) { 625 | return listeners.splice(i, 1); 626 | } 627 | } 628 | }; 629 | 630 | UploadProgress.prototype.dispatchEvent = function(event) { 631 | var listeners = this.eventListeners[event.type] || []; 632 | 633 | for (var i = 0, listener; (listener = listeners[i]) != null; i++) { 634 | listener(event); 635 | } 636 | }; 637 | 638 | function verifyState(xhr) { 639 | if (xhr.readyState !== FakeXMLHttpRequest.OPENED) { 640 | throw new Error("INVALID_STATE_ERR"); 641 | } 642 | 643 | if (xhr.sendFlag) { 644 | throw new Error("INVALID_STATE_ERR"); 645 | } 646 | } 647 | 648 | // filtering to enable a white-list version of Sinon FakeXhr, 649 | // where whitelisted requests are passed through to real XHR 650 | function each(collection, callback) { 651 | if (!collection) return; 652 | for (var i = 0, l = collection.length; i < l; i += 1) { 653 | callback(collection[i]); 654 | } 655 | } 656 | function some(collection, callback) { 657 | for (var index = 0; index < collection.length; index++) { 658 | if(callback(collection[index]) === true) return true; 659 | } 660 | return false; 661 | } 662 | // largest arity in XHR is 5 - XHR#open 663 | var apply = function(obj,method,args) { 664 | switch(args.length) { 665 | case 0: return obj[method](); 666 | case 1: return obj[method](args[0]); 667 | case 2: return obj[method](args[0],args[1]); 668 | case 3: return obj[method](args[0],args[1],args[2]); 669 | case 4: return obj[method](args[0],args[1],args[2],args[3]); 670 | case 5: return obj[method](args[0],args[1],args[2],args[3],args[4]); 671 | } 672 | }; 673 | 674 | FakeXMLHttpRequest.filters = []; 675 | FakeXMLHttpRequest.addFilter = function(fn) { 676 | this.filters.push(fn) 677 | }; 678 | var IE6Re = /MSIE 6/; 679 | FakeXMLHttpRequest.defake = function(fakeXhr,xhrArgs) { 680 | var xhr = new sinon.xhr.workingXHR(); 681 | each(["open","setRequestHeader","send","abort","getResponseHeader", 682 | "getAllResponseHeaders","addEventListener","overrideMimeType","removeEventListener"], 683 | function(method) { 684 | fakeXhr[method] = function() { 685 | return apply(xhr,method,arguments); 686 | }; 687 | }); 688 | 689 | var copyAttrs = function(args) { 690 | each(args, function(attr) { 691 | try { 692 | fakeXhr[attr] = xhr[attr] 693 | } catch(e) { 694 | if(!IE6Re.test(navigator.userAgent)) throw e; 695 | } 696 | }); 697 | }; 698 | 699 | var stateChange = function() { 700 | fakeXhr.readyState = xhr.readyState; 701 | if(xhr.readyState >= FakeXMLHttpRequest.HEADERS_RECEIVED) { 702 | copyAttrs(["status","statusText"]); 703 | } 704 | if(xhr.readyState >= FakeXMLHttpRequest.LOADING) { 705 | copyAttrs(["responseText"]); 706 | } 707 | if(xhr.readyState === FakeXMLHttpRequest.DONE) { 708 | copyAttrs(["responseXML"]); 709 | } 710 | if(fakeXhr.onreadystatechange) fakeXhr.onreadystatechange.call(fakeXhr, { target: fakeXhr }); 711 | }; 712 | if(xhr.addEventListener) { 713 | for(var event in fakeXhr.eventListeners) { 714 | if(fakeXhr.eventListeners.hasOwnProperty(event)) { 715 | each(fakeXhr.eventListeners[event],function(handler) { 716 | xhr.addEventListener(event, handler); 717 | }); 718 | } 719 | } 720 | xhr.addEventListener("readystatechange",stateChange); 721 | } else { 722 | xhr.onreadystatechange = stateChange; 723 | } 724 | apply(xhr,"open",xhrArgs); 725 | }; 726 | FakeXMLHttpRequest.useFilters = false; 727 | 728 | function verifyRequestOpened(xhr) { 729 | if (xhr.readyState != FakeXMLHttpRequest.OPENED) { 730 | throw new Error("INVALID_STATE_ERR - " + xhr.readyState); 731 | } 732 | } 733 | 734 | function verifyRequestSent(xhr) { 735 | if (xhr.readyState == FakeXMLHttpRequest.DONE) { 736 | throw new Error("Request done"); 737 | } 738 | } 739 | 740 | function verifyHeadersReceived(xhr) { 741 | if (xhr.async && xhr.readyState != FakeXMLHttpRequest.HEADERS_RECEIVED) { 742 | throw new Error("No headers received"); 743 | } 744 | } 745 | 746 | function verifyResponseBodyType(body) { 747 | if (typeof body != "string") { 748 | var error = new Error("Attempted to respond to fake XMLHttpRequest with " + 749 | body + ", which is not a string."); 750 | error.name = "InvalidBodyException"; 751 | throw error; 752 | } 753 | } 754 | 755 | sinon.extend(FakeXMLHttpRequest.prototype, sinon.EventTarget, { 756 | async: true, 757 | 758 | open: function open(method, url, async, username, password) { 759 | this.method = method; 760 | this.url = url; 761 | this.async = typeof async == "boolean" ? async : true; 762 | this.username = username; 763 | this.password = password; 764 | this.responseText = null; 765 | this.responseXML = null; 766 | this.requestHeaders = {}; 767 | this.sendFlag = false; 768 | if(sinon.FakeXMLHttpRequest.useFilters === true) { 769 | var xhrArgs = arguments; 770 | var defake = some(FakeXMLHttpRequest.filters,function(filter) { 771 | return filter.apply(this,xhrArgs) 772 | }); 773 | if (defake) { 774 | return sinon.FakeXMLHttpRequest.defake(this,arguments); 775 | } 776 | } 777 | this.readyStateChange(FakeXMLHttpRequest.OPENED); 778 | }, 779 | 780 | readyStateChange: function readyStateChange(state) { 781 | this.readyState = state; 782 | 783 | if (typeof this.onreadystatechange == "function") { 784 | try { 785 | this.onreadystatechange(); 786 | } catch (e) { 787 | sinon.logError("Fake XHR onreadystatechange handler", e); 788 | } 789 | } 790 | 791 | this.dispatchEvent(new sinon.Event("readystatechange")); 792 | 793 | switch (this.readyState) { 794 | case FakeXMLHttpRequest.DONE: 795 | this.dispatchEvent(new sinon.Event("load", false, false, this)); 796 | this.dispatchEvent(new sinon.Event("loadend", false, false, this)); 797 | this.upload.dispatchEvent(new sinon.Event("load", false, false, this)); 798 | if (supportsProgress) { 799 | this.upload.dispatchEvent(new sinon.ProgressEvent('progress', {loaded: 100, total: 100})); 800 | } 801 | break; 802 | } 803 | }, 804 | 805 | setRequestHeader: function setRequestHeader(header, value) { 806 | verifyState(this); 807 | 808 | if (unsafeHeaders[header] || /^(Sec-|Proxy-)/.test(header)) { 809 | throw new Error("Refused to set unsafe header \"" + header + "\""); 810 | } 811 | 812 | if (this.requestHeaders[header]) { 813 | this.requestHeaders[header] += "," + value; 814 | } else { 815 | this.requestHeaders[header] = value; 816 | } 817 | }, 818 | 819 | // Helps testing 820 | setResponseHeaders: function setResponseHeaders(headers) { 821 | verifyRequestOpened(this); 822 | this.responseHeaders = {}; 823 | 824 | for (var header in headers) { 825 | if (headers.hasOwnProperty(header)) { 826 | this.responseHeaders[header] = headers[header]; 827 | } 828 | } 829 | 830 | if (this.async) { 831 | this.readyStateChange(FakeXMLHttpRequest.HEADERS_RECEIVED); 832 | } else { 833 | this.readyState = FakeXMLHttpRequest.HEADERS_RECEIVED; 834 | } 835 | }, 836 | 837 | // Currently treats ALL data as a DOMString (i.e. no Document) 838 | send: function send(data) { 839 | verifyState(this); 840 | 841 | if (!/^(get|head)$/i.test(this.method)) { 842 | if (this.requestHeaders["Content-Type"]) { 843 | var value = this.requestHeaders["Content-Type"].split(";"); 844 | this.requestHeaders["Content-Type"] = value[0] + ";charset=utf-8"; 845 | } else { 846 | this.requestHeaders["Content-Type"] = "text/plain;charset=utf-8"; 847 | } 848 | 849 | this.requestBody = data; 850 | } 851 | 852 | this.errorFlag = false; 853 | this.sendFlag = this.async; 854 | this.readyStateChange(FakeXMLHttpRequest.OPENED); 855 | 856 | if (typeof this.onSend == "function") { 857 | this.onSend(this); 858 | } 859 | 860 | this.dispatchEvent(new sinon.Event("loadstart", false, false, this)); 861 | }, 862 | 863 | abort: function abort() { 864 | this.aborted = true; 865 | this.responseText = null; 866 | this.errorFlag = true; 867 | this.requestHeaders = {}; 868 | 869 | if (this.readyState > sinon.FakeXMLHttpRequest.UNSENT && this.sendFlag) { 870 | this.readyStateChange(sinon.FakeXMLHttpRequest.DONE); 871 | this.sendFlag = false; 872 | } 873 | 874 | this.readyState = sinon.FakeXMLHttpRequest.UNSENT; 875 | 876 | this.dispatchEvent(new sinon.Event("abort", false, false, this)); 877 | 878 | this.upload.dispatchEvent(new sinon.Event("abort", false, false, this)); 879 | 880 | if (typeof this.onerror === "function") { 881 | this.onerror(); 882 | } 883 | }, 884 | 885 | getResponseHeader: function getResponseHeader(header) { 886 | if (this.readyState < FakeXMLHttpRequest.HEADERS_RECEIVED) { 887 | return null; 888 | } 889 | 890 | if (/^Set-Cookie2?$/i.test(header)) { 891 | return null; 892 | } 893 | 894 | header = header.toLowerCase(); 895 | 896 | for (var h in this.responseHeaders) { 897 | if (h.toLowerCase() == header) { 898 | return this.responseHeaders[h]; 899 | } 900 | } 901 | 902 | return null; 903 | }, 904 | 905 | getAllResponseHeaders: function getAllResponseHeaders() { 906 | if (this.readyState < FakeXMLHttpRequest.HEADERS_RECEIVED) { 907 | return ""; 908 | } 909 | 910 | var headers = ""; 911 | 912 | for (var header in this.responseHeaders) { 913 | if (this.responseHeaders.hasOwnProperty(header) && 914 | !/^Set-Cookie2?$/i.test(header)) { 915 | headers += header + ": " + this.responseHeaders[header] + "\r\n"; 916 | } 917 | } 918 | 919 | return headers; 920 | }, 921 | 922 | setResponseBody: function setResponseBody(body) { 923 | verifyRequestSent(this); 924 | verifyHeadersReceived(this); 925 | verifyResponseBodyType(body); 926 | 927 | var chunkSize = this.chunkSize || 10; 928 | var index = 0; 929 | this.responseText = ""; 930 | 931 | do { 932 | if (this.async) { 933 | this.readyStateChange(FakeXMLHttpRequest.LOADING); 934 | } 935 | 936 | this.responseText += body.substring(index, index + chunkSize); 937 | index += chunkSize; 938 | } while (index < body.length); 939 | 940 | var type = this.getResponseHeader("Content-Type"); 941 | 942 | if (this.responseText && 943 | (!type || /(text\/xml)|(application\/xml)|(\+xml)/.test(type))) { 944 | try { 945 | this.responseXML = FakeXMLHttpRequest.parseXML(this.responseText); 946 | } catch (e) { 947 | // Unable to parse XML - no biggie 948 | } 949 | } 950 | 951 | if (this.async) { 952 | this.readyStateChange(FakeXMLHttpRequest.DONE); 953 | } else { 954 | this.readyState = FakeXMLHttpRequest.DONE; 955 | } 956 | }, 957 | 958 | respond: function respond(status, headers, body) { 959 | this.status = typeof status == "number" ? status : 200; 960 | this.statusText = FakeXMLHttpRequest.statusCodes[this.status]; 961 | this.setResponseHeaders(headers || {}); 962 | this.setResponseBody(body || ""); 963 | }, 964 | 965 | uploadProgress: function uploadProgress(progressEventRaw) { 966 | if (supportsProgress) { 967 | this.upload.dispatchEvent(new sinon.ProgressEvent("progress", progressEventRaw)); 968 | } 969 | }, 970 | 971 | uploadError: function uploadError(error) { 972 | if (supportsCustomEvent) { 973 | this.upload.dispatchEvent(new sinon.CustomEvent("error", {"detail": error})); 974 | } 975 | } 976 | }); 977 | 978 | sinon.extend(FakeXMLHttpRequest, { 979 | UNSENT: 0, 980 | OPENED: 1, 981 | HEADERS_RECEIVED: 2, 982 | LOADING: 3, 983 | DONE: 4 984 | }); 985 | 986 | // Borrowed from JSpec 987 | FakeXMLHttpRequest.parseXML = function parseXML(text) { 988 | var xmlDoc; 989 | 990 | if (typeof DOMParser != "undefined") { 991 | var parser = new DOMParser(); 992 | xmlDoc = parser.parseFromString(text, "text/xml"); 993 | } else { 994 | xmlDoc = new ActiveXObject("Microsoft.XMLDOM"); 995 | xmlDoc.async = "false"; 996 | xmlDoc.loadXML(text); 997 | } 998 | 999 | return xmlDoc; 1000 | }; 1001 | 1002 | FakeXMLHttpRequest.statusCodes = { 1003 | 100: "Continue", 1004 | 101: "Switching Protocols", 1005 | 200: "OK", 1006 | 201: "Created", 1007 | 202: "Accepted", 1008 | 203: "Non-Authoritative Information", 1009 | 204: "No Content", 1010 | 205: "Reset Content", 1011 | 206: "Partial Content", 1012 | 300: "Multiple Choice", 1013 | 301: "Moved Permanently", 1014 | 302: "Found", 1015 | 303: "See Other", 1016 | 304: "Not Modified", 1017 | 305: "Use Proxy", 1018 | 307: "Temporary Redirect", 1019 | 400: "Bad Request", 1020 | 401: "Unauthorized", 1021 | 402: "Payment Required", 1022 | 403: "Forbidden", 1023 | 404: "Not Found", 1024 | 405: "Method Not Allowed", 1025 | 406: "Not Acceptable", 1026 | 407: "Proxy Authentication Required", 1027 | 408: "Request Timeout", 1028 | 409: "Conflict", 1029 | 410: "Gone", 1030 | 411: "Length Required", 1031 | 412: "Precondition Failed", 1032 | 413: "Request Entity Too Large", 1033 | 414: "Request-URI Too Long", 1034 | 415: "Unsupported Media Type", 1035 | 416: "Requested Range Not Satisfiable", 1036 | 417: "Expectation Failed", 1037 | 422: "Unprocessable Entity", 1038 | 500: "Internal Server Error", 1039 | 501: "Not Implemented", 1040 | 502: "Bad Gateway", 1041 | 503: "Service Unavailable", 1042 | 504: "Gateway Timeout", 1043 | 505: "HTTP Version Not Supported" 1044 | }; 1045 | 1046 | sinon.useFakeXMLHttpRequest = function () { 1047 | sinon.FakeXMLHttpRequest.restore = function restore(keepOnCreate) { 1048 | if (xhr.supportsXHR) { 1049 | global.XMLHttpRequest = xhr.GlobalXMLHttpRequest; 1050 | } 1051 | 1052 | if (xhr.supportsActiveX) { 1053 | global.ActiveXObject = xhr.GlobalActiveXObject; 1054 | } 1055 | 1056 | delete sinon.FakeXMLHttpRequest.restore; 1057 | 1058 | if (keepOnCreate !== true) { 1059 | delete sinon.FakeXMLHttpRequest.onCreate; 1060 | } 1061 | }; 1062 | if (xhr.supportsXHR) { 1063 | global.XMLHttpRequest = sinon.FakeXMLHttpRequest; 1064 | } 1065 | 1066 | if (xhr.supportsActiveX) { 1067 | global.ActiveXObject = function ActiveXObject(objId) { 1068 | if (objId == "Microsoft.XMLHTTP" || /^Msxml2\.XMLHTTP/i.test(objId)) { 1069 | 1070 | return new sinon.FakeXMLHttpRequest(); 1071 | } 1072 | 1073 | return new xhr.GlobalActiveXObject(objId); 1074 | }; 1075 | } 1076 | 1077 | return sinon.FakeXMLHttpRequest; 1078 | }; 1079 | 1080 | sinon.FakeXMLHttpRequest = FakeXMLHttpRequest; 1081 | 1082 | })(typeof global === "object" ? global : this); 1083 | 1084 | if (typeof module !== 'undefined' && module.exports) { 1085 | module.exports = sinon; 1086 | } 1087 | 1088 | /** 1089 | * @depend fake_xml_http_request.js 1090 | */ 1091 | /*jslint eqeqeq: false, onevar: false, regexp: false, plusplus: false*/ 1092 | /*global module, require, window*/ 1093 | /** 1094 | * The Sinon "server" mimics a web server that receives requests from 1095 | * sinon.FakeXMLHttpRequest and provides an API to respond to those requests, 1096 | * both synchronously and asynchronously. To respond synchronuously, canned 1097 | * answers have to be provided upfront. 1098 | * 1099 | * @author Christian Johansen (christian@cjohansen.no) 1100 | * @license BSD 1101 | * 1102 | * Copyright (c) 2010-2013 Christian Johansen 1103 | */ 1104 | 1105 | if (typeof sinon == "undefined") { 1106 | var sinon = {}; 1107 | } 1108 | 1109 | sinon.fakeServer = (function () { 1110 | var push = [].push; 1111 | function F() {} 1112 | 1113 | function create(proto) { 1114 | F.prototype = proto; 1115 | return new F(); 1116 | } 1117 | 1118 | function responseArray(handler) { 1119 | var response = handler; 1120 | 1121 | if (Object.prototype.toString.call(handler) != "[object Array]") { 1122 | response = [200, {}, handler]; 1123 | } 1124 | 1125 | if (typeof response[2] != "string") { 1126 | throw new TypeError("Fake server response body should be string, but was " + 1127 | typeof response[2]); 1128 | } 1129 | 1130 | return response; 1131 | } 1132 | 1133 | var wloc = typeof window !== "undefined" ? window.location : {}; 1134 | var rCurrLoc = new RegExp("^" + wloc.protocol + "//" + wloc.host); 1135 | 1136 | function matchOne(response, reqMethod, reqUrl) { 1137 | var rmeth = response.method; 1138 | var matchMethod = !rmeth || rmeth.toLowerCase() == reqMethod.toLowerCase(); 1139 | var url = response.url; 1140 | var matchUrl = !url || url == reqUrl || (typeof url.test == "function" && url.test(reqUrl)); 1141 | 1142 | return matchMethod && matchUrl; 1143 | } 1144 | 1145 | function match(response, request) { 1146 | var requestUrl = request.url; 1147 | 1148 | if (!/^https?:\/\//.test(requestUrl) || rCurrLoc.test(requestUrl)) { 1149 | requestUrl = requestUrl.replace(rCurrLoc, ""); 1150 | } 1151 | 1152 | if (matchOne(response, this.getHTTPMethod(request), requestUrl)) { 1153 | if (typeof response.response == "function") { 1154 | var ru = response.url; 1155 | var args = [request].concat(ru && typeof ru.exec == "function" ? ru.exec(requestUrl).slice(1) : []); 1156 | return response.response.apply(response, args); 1157 | } 1158 | 1159 | return true; 1160 | } 1161 | 1162 | return false; 1163 | } 1164 | 1165 | return { 1166 | create: function () { 1167 | var server = create(this); 1168 | this.xhr = sinon.useFakeXMLHttpRequest(); 1169 | server.requests = []; 1170 | 1171 | this.xhr.onCreate = function (xhrObj) { 1172 | server.addRequest(xhrObj); 1173 | }; 1174 | 1175 | return server; 1176 | }, 1177 | 1178 | addRequest: function addRequest(xhrObj) { 1179 | var server = this; 1180 | push.call(this.requests, xhrObj); 1181 | 1182 | xhrObj.onSend = function () { 1183 | server.handleRequest(this); 1184 | 1185 | if (server.autoRespond && !server.responding) { 1186 | setTimeout(function () { 1187 | server.responding = false; 1188 | server.respond(); 1189 | }, server.autoRespondAfter || 10); 1190 | 1191 | server.responding = true; 1192 | } 1193 | }; 1194 | }, 1195 | 1196 | getHTTPMethod: function getHTTPMethod(request) { 1197 | if (this.fakeHTTPMethods && /post/i.test(request.method)) { 1198 | var matches = (request.requestBody || "").match(/_method=([^\b;]+)/); 1199 | return !!matches ? matches[1] : request.method; 1200 | } 1201 | 1202 | return request.method; 1203 | }, 1204 | 1205 | handleRequest: function handleRequest(xhr) { 1206 | if (xhr.async) { 1207 | if (!this.queue) { 1208 | this.queue = []; 1209 | } 1210 | 1211 | push.call(this.queue, xhr); 1212 | } else { 1213 | this.processRequest(xhr); 1214 | } 1215 | }, 1216 | 1217 | log: function(response, request) { 1218 | var str; 1219 | 1220 | str = "Request:\n" + sinon.format(request) + "\n\n"; 1221 | str += "Response:\n" + sinon.format(response) + "\n\n"; 1222 | 1223 | sinon.log(str); 1224 | }, 1225 | 1226 | respondWith: function respondWith(method, url, body) { 1227 | if (arguments.length == 1 && typeof method != "function") { 1228 | this.response = responseArray(method); 1229 | return; 1230 | } 1231 | 1232 | if (!this.responses) { this.responses = []; } 1233 | 1234 | if (arguments.length == 1) { 1235 | body = method; 1236 | url = method = null; 1237 | } 1238 | 1239 | if (arguments.length == 2) { 1240 | body = url; 1241 | url = method; 1242 | method = null; 1243 | } 1244 | 1245 | push.call(this.responses, { 1246 | method: method, 1247 | url: url, 1248 | response: typeof body == "function" ? body : responseArray(body) 1249 | }); 1250 | }, 1251 | 1252 | respond: function respond() { 1253 | if (arguments.length > 0) this.respondWith.apply(this, arguments); 1254 | var queue = this.queue || []; 1255 | var requests = queue.splice(0, queue.length); 1256 | var request; 1257 | 1258 | while(request = requests.shift()) { 1259 | this.processRequest(request); 1260 | } 1261 | }, 1262 | 1263 | processRequest: function processRequest(request) { 1264 | try { 1265 | if (request.aborted) { 1266 | return; 1267 | } 1268 | 1269 | var response = this.response || [404, {}, ""]; 1270 | 1271 | if (this.responses) { 1272 | for (var l = this.responses.length, i = l - 1; i >= 0; i--) { 1273 | if (match.call(this, this.responses[i], request)) { 1274 | response = this.responses[i].response; 1275 | break; 1276 | } 1277 | } 1278 | } 1279 | 1280 | if (request.readyState != 4) { 1281 | sinon.fakeServer.log(response, request); 1282 | 1283 | request.respond(response[0], response[1], response[2]); 1284 | } 1285 | } catch (e) { 1286 | sinon.logError("Fake server request processing", e); 1287 | } 1288 | }, 1289 | 1290 | restore: function restore() { 1291 | return this.xhr.restore && this.xhr.restore.apply(this.xhr, arguments); 1292 | } 1293 | }; 1294 | }()); 1295 | 1296 | if (typeof module !== 'undefined' && module.exports) { 1297 | module.exports = sinon; 1298 | } 1299 | 1300 | /*jslint eqeqeq: false, plusplus: false, evil: true, onevar: false, browser: true, forin: false*/ 1301 | /*global module, require, window*/ 1302 | /** 1303 | * Fake timer API 1304 | * setTimeout 1305 | * setInterval 1306 | * clearTimeout 1307 | * clearInterval 1308 | * tick 1309 | * reset 1310 | * Date 1311 | * 1312 | * Inspired by jsUnitMockTimeOut from JsUnit 1313 | * 1314 | * @author Christian Johansen (christian@cjohansen.no) 1315 | * @license BSD 1316 | * 1317 | * Copyright (c) 2010-2013 Christian Johansen 1318 | */ 1319 | 1320 | if (typeof sinon == "undefined") { 1321 | var sinon = {}; 1322 | } 1323 | 1324 | (function (global) { 1325 | // node expects setTimeout/setInterval to return a fn object w/ .ref()/.unref() 1326 | // browsers, a number. 1327 | // see https://github.com/cjohansen/Sinon.JS/pull/436 1328 | var timeoutResult = setTimeout(function() {}, 0); 1329 | var addTimerReturnsObject = typeof timeoutResult === 'object'; 1330 | clearTimeout(timeoutResult); 1331 | 1332 | var id = 1; 1333 | 1334 | function addTimer(args, recurring) { 1335 | if (args.length === 0) { 1336 | throw new Error("Function requires at least 1 parameter"); 1337 | } 1338 | 1339 | if (typeof args[0] === "undefined") { 1340 | throw new Error("Callback must be provided to timer calls"); 1341 | } 1342 | 1343 | var toId = id++; 1344 | var delay = args[1] || 0; 1345 | 1346 | if (!this.timeouts) { 1347 | this.timeouts = {}; 1348 | } 1349 | 1350 | this.timeouts[toId] = { 1351 | id: toId, 1352 | func: args[0], 1353 | callAt: this.now + delay, 1354 | invokeArgs: Array.prototype.slice.call(args, 2) 1355 | }; 1356 | 1357 | if (recurring === true) { 1358 | this.timeouts[toId].interval = delay; 1359 | } 1360 | 1361 | if (addTimerReturnsObject) { 1362 | return { 1363 | id: toId, 1364 | ref: function() {}, 1365 | unref: function() {} 1366 | }; 1367 | } 1368 | else { 1369 | return toId; 1370 | } 1371 | } 1372 | 1373 | function parseTime(str) { 1374 | if (!str) { 1375 | return 0; 1376 | } 1377 | 1378 | var strings = str.split(":"); 1379 | var l = strings.length, i = l; 1380 | var ms = 0, parsed; 1381 | 1382 | if (l > 3 || !/^(\d\d:){0,2}\d\d?$/.test(str)) { 1383 | throw new Error("tick only understands numbers and 'h:m:s'"); 1384 | } 1385 | 1386 | while (i--) { 1387 | parsed = parseInt(strings[i], 10); 1388 | 1389 | if (parsed >= 60) { 1390 | throw new Error("Invalid time " + str); 1391 | } 1392 | 1393 | ms += parsed * Math.pow(60, (l - i - 1)); 1394 | } 1395 | 1396 | return ms * 1000; 1397 | } 1398 | 1399 | function createObject(object) { 1400 | var newObject; 1401 | 1402 | if (Object.create) { 1403 | newObject = Object.create(object); 1404 | } else { 1405 | var F = function () {}; 1406 | F.prototype = object; 1407 | newObject = new F(); 1408 | } 1409 | 1410 | newObject.Date.clock = newObject; 1411 | return newObject; 1412 | } 1413 | 1414 | sinon.clock = { 1415 | now: 0, 1416 | 1417 | create: function create(now) { 1418 | var clock = createObject(this); 1419 | 1420 | if (typeof now == "number") { 1421 | clock.now = now; 1422 | } 1423 | 1424 | if (!!now && typeof now == "object") { 1425 | throw new TypeError("now should be milliseconds since UNIX epoch"); 1426 | } 1427 | 1428 | return clock; 1429 | }, 1430 | 1431 | setTimeout: function setTimeout(callback, timeout) { 1432 | return addTimer.call(this, arguments, false); 1433 | }, 1434 | 1435 | clearTimeout: function clearTimeout(timerId) { 1436 | if (!this.timeouts) { 1437 | this.timeouts = []; 1438 | } 1439 | // in Node, timerId is an object with .ref()/.unref(), and 1440 | // its .id field is the actual timer id. 1441 | if (typeof timerId === 'object') { 1442 | timerId = timerId.id 1443 | } 1444 | if (timerId in this.timeouts) { 1445 | delete this.timeouts[timerId]; 1446 | } 1447 | }, 1448 | 1449 | setInterval: function setInterval(callback, timeout) { 1450 | return addTimer.call(this, arguments, true); 1451 | }, 1452 | 1453 | clearInterval: function clearInterval(timerId) { 1454 | this.clearTimeout(timerId); 1455 | }, 1456 | 1457 | setImmediate: function setImmediate(callback) { 1458 | var passThruArgs = Array.prototype.slice.call(arguments, 1); 1459 | 1460 | return addTimer.call(this, [callback, 0].concat(passThruArgs), false); 1461 | }, 1462 | 1463 | clearImmediate: function clearImmediate(timerId) { 1464 | this.clearTimeout(timerId); 1465 | }, 1466 | 1467 | tick: function tick(ms) { 1468 | ms = typeof ms == "number" ? ms : parseTime(ms); 1469 | var tickFrom = this.now, tickTo = this.now + ms, previous = this.now; 1470 | var timer = this.firstTimerInRange(tickFrom, tickTo); 1471 | 1472 | var firstException; 1473 | while (timer && tickFrom <= tickTo) { 1474 | if (this.timeouts[timer.id]) { 1475 | tickFrom = this.now = timer.callAt; 1476 | try { 1477 | this.callTimer(timer); 1478 | } catch (e) { 1479 | firstException = firstException || e; 1480 | } 1481 | } 1482 | 1483 | timer = this.firstTimerInRange(previous, tickTo); 1484 | previous = tickFrom; 1485 | } 1486 | 1487 | this.now = tickTo; 1488 | 1489 | if (firstException) { 1490 | throw firstException; 1491 | } 1492 | 1493 | return this.now; 1494 | }, 1495 | 1496 | firstTimerInRange: function (from, to) { 1497 | var timer, smallest = null, originalTimer; 1498 | 1499 | for (var id in this.timeouts) { 1500 | if (this.timeouts.hasOwnProperty(id)) { 1501 | if (this.timeouts[id].callAt < from || this.timeouts[id].callAt > to) { 1502 | continue; 1503 | } 1504 | 1505 | if (smallest === null || this.timeouts[id].callAt < smallest) { 1506 | originalTimer = this.timeouts[id]; 1507 | smallest = this.timeouts[id].callAt; 1508 | 1509 | timer = { 1510 | func: this.timeouts[id].func, 1511 | callAt: this.timeouts[id].callAt, 1512 | interval: this.timeouts[id].interval, 1513 | id: this.timeouts[id].id, 1514 | invokeArgs: this.timeouts[id].invokeArgs 1515 | }; 1516 | } 1517 | } 1518 | } 1519 | 1520 | return timer || null; 1521 | }, 1522 | 1523 | callTimer: function (timer) { 1524 | if (typeof timer.interval == "number") { 1525 | this.timeouts[timer.id].callAt += timer.interval; 1526 | } else { 1527 | delete this.timeouts[timer.id]; 1528 | } 1529 | 1530 | try { 1531 | if (typeof timer.func == "function") { 1532 | timer.func.apply(null, timer.invokeArgs); 1533 | } else { 1534 | eval(timer.func); 1535 | } 1536 | } catch (e) { 1537 | var exception = e; 1538 | } 1539 | 1540 | if (!this.timeouts[timer.id]) { 1541 | if (exception) { 1542 | throw exception; 1543 | } 1544 | return; 1545 | } 1546 | 1547 | if (exception) { 1548 | throw exception; 1549 | } 1550 | }, 1551 | 1552 | reset: function reset() { 1553 | this.timeouts = {}; 1554 | }, 1555 | 1556 | Date: (function () { 1557 | var NativeDate = Date; 1558 | 1559 | function ClockDate(year, month, date, hour, minute, second, ms) { 1560 | // Defensive and verbose to avoid potential harm in passing 1561 | // explicit undefined when user does not pass argument 1562 | switch (arguments.length) { 1563 | case 0: 1564 | return new NativeDate(ClockDate.clock.now); 1565 | case 1: 1566 | return new NativeDate(year); 1567 | case 2: 1568 | return new NativeDate(year, month); 1569 | case 3: 1570 | return new NativeDate(year, month, date); 1571 | case 4: 1572 | return new NativeDate(year, month, date, hour); 1573 | case 5: 1574 | return new NativeDate(year, month, date, hour, minute); 1575 | case 6: 1576 | return new NativeDate(year, month, date, hour, minute, second); 1577 | default: 1578 | return new NativeDate(year, month, date, hour, minute, second, ms); 1579 | } 1580 | } 1581 | 1582 | return mirrorDateProperties(ClockDate, NativeDate); 1583 | }()) 1584 | }; 1585 | 1586 | function mirrorDateProperties(target, source) { 1587 | if (source.now) { 1588 | target.now = function now() { 1589 | return target.clock.now; 1590 | }; 1591 | } else { 1592 | delete target.now; 1593 | } 1594 | 1595 | if (source.toSource) { 1596 | target.toSource = function toSource() { 1597 | return source.toSource(); 1598 | }; 1599 | } else { 1600 | delete target.toSource; 1601 | } 1602 | 1603 | target.toString = function toString() { 1604 | return source.toString(); 1605 | }; 1606 | 1607 | target.prototype = source.prototype; 1608 | target.parse = source.parse; 1609 | target.UTC = source.UTC; 1610 | target.prototype.toUTCString = source.prototype.toUTCString; 1611 | 1612 | for (var prop in source) { 1613 | if (source.hasOwnProperty(prop)) { 1614 | target[prop] = source[prop]; 1615 | } 1616 | } 1617 | 1618 | return target; 1619 | } 1620 | 1621 | var methods = ["Date", "setTimeout", "setInterval", 1622 | "clearTimeout", "clearInterval"]; 1623 | 1624 | if (typeof global.setImmediate !== "undefined") { 1625 | methods.push("setImmediate"); 1626 | } 1627 | 1628 | if (typeof global.clearImmediate !== "undefined") { 1629 | methods.push("clearImmediate"); 1630 | } 1631 | 1632 | function restore() { 1633 | var method; 1634 | 1635 | for (var i = 0, l = this.methods.length; i < l; i++) { 1636 | method = this.methods[i]; 1637 | 1638 | if (global[method].hadOwnProperty) { 1639 | global[method] = this["_" + method]; 1640 | } else { 1641 | try { 1642 | delete global[method]; 1643 | } catch (e) {} 1644 | } 1645 | } 1646 | 1647 | // Prevent multiple executions which will completely remove these props 1648 | this.methods = []; 1649 | } 1650 | 1651 | function stubGlobal(method, clock) { 1652 | clock[method].hadOwnProperty = Object.prototype.hasOwnProperty.call(global, method); 1653 | clock["_" + method] = global[method]; 1654 | 1655 | if (method == "Date") { 1656 | var date = mirrorDateProperties(clock[method], global[method]); 1657 | global[method] = date; 1658 | } else { 1659 | global[method] = function () { 1660 | return clock[method].apply(clock, arguments); 1661 | }; 1662 | 1663 | for (var prop in clock[method]) { 1664 | if (clock[method].hasOwnProperty(prop)) { 1665 | global[method][prop] = clock[method][prop]; 1666 | } 1667 | } 1668 | } 1669 | 1670 | global[method].clock = clock; 1671 | } 1672 | 1673 | sinon.useFakeTimers = function useFakeTimers(now) { 1674 | var clock = sinon.clock.create(now); 1675 | clock.restore = restore; 1676 | clock.methods = Array.prototype.slice.call(arguments, 1677 | typeof now == "number" ? 1 : 0); 1678 | 1679 | if (clock.methods.length === 0) { 1680 | clock.methods = methods; 1681 | } 1682 | 1683 | for (var i = 0, l = clock.methods.length; i < l; i++) { 1684 | stubGlobal(clock.methods[i], clock); 1685 | } 1686 | 1687 | return clock; 1688 | }; 1689 | }(typeof global != "undefined" && typeof global !== "function" ? global : this)); 1690 | 1691 | sinon.timers = { 1692 | setTimeout: setTimeout, 1693 | clearTimeout: clearTimeout, 1694 | setImmediate: (typeof setImmediate !== "undefined" ? setImmediate : undefined), 1695 | clearImmediate: (typeof clearImmediate !== "undefined" ? clearImmediate: undefined), 1696 | setInterval: setInterval, 1697 | clearInterval: clearInterval, 1698 | Date: Date 1699 | }; 1700 | 1701 | if (typeof module !== 'undefined' && module.exports) { 1702 | module.exports = sinon; 1703 | } 1704 | 1705 | /** 1706 | * @depend fake_server.js 1707 | * @depend fake_timers.js 1708 | */ 1709 | /*jslint browser: true, eqeqeq: false, onevar: false*/ 1710 | /*global sinon*/ 1711 | /** 1712 | * Add-on for sinon.fakeServer that automatically handles a fake timer along with 1713 | * the FakeXMLHttpRequest. The direct inspiration for this add-on is jQuery 1714 | * 1.3.x, which does not use xhr object's onreadystatehandler at all - instead, 1715 | * it polls the object for completion with setInterval. Dispite the direct 1716 | * motivation, there is nothing jQuery-specific in this file, so it can be used 1717 | * in any environment where the ajax implementation depends on setInterval or 1718 | * setTimeout. 1719 | * 1720 | * @author Christian Johansen (christian@cjohansen.no) 1721 | * @license BSD 1722 | * 1723 | * Copyright (c) 2010-2013 Christian Johansen 1724 | */ 1725 | 1726 | (function () { 1727 | function Server() {} 1728 | Server.prototype = sinon.fakeServer; 1729 | 1730 | sinon.fakeServerWithClock = new Server(); 1731 | 1732 | sinon.fakeServerWithClock.addRequest = function addRequest(xhr) { 1733 | if (xhr.async) { 1734 | if (typeof setTimeout.clock == "object") { 1735 | this.clock = setTimeout.clock; 1736 | } else { 1737 | this.clock = sinon.useFakeTimers(); 1738 | this.resetClock = true; 1739 | } 1740 | 1741 | if (!this.longestTimeout) { 1742 | var clockSetTimeout = this.clock.setTimeout; 1743 | var clockSetInterval = this.clock.setInterval; 1744 | var server = this; 1745 | 1746 | this.clock.setTimeout = function (fn, timeout) { 1747 | server.longestTimeout = Math.max(timeout, server.longestTimeout || 0); 1748 | 1749 | return clockSetTimeout.apply(this, arguments); 1750 | }; 1751 | 1752 | this.clock.setInterval = function (fn, timeout) { 1753 | server.longestTimeout = Math.max(timeout, server.longestTimeout || 0); 1754 | 1755 | return clockSetInterval.apply(this, arguments); 1756 | }; 1757 | } 1758 | } 1759 | 1760 | return sinon.fakeServer.addRequest.call(this, xhr); 1761 | }; 1762 | 1763 | sinon.fakeServerWithClock.respond = function respond() { 1764 | var returnVal = sinon.fakeServer.respond.apply(this, arguments); 1765 | 1766 | if (this.clock) { 1767 | this.clock.tick(this.longestTimeout || 0); 1768 | this.longestTimeout = 0; 1769 | 1770 | if (this.resetClock) { 1771 | this.clock.restore(); 1772 | this.resetClock = false; 1773 | } 1774 | } 1775 | 1776 | return returnVal; 1777 | }; 1778 | 1779 | sinon.fakeServerWithClock.restore = function restore() { 1780 | if (this.clock) { 1781 | this.clock.restore(); 1782 | } 1783 | 1784 | return sinon.fakeServer.restore.apply(this, arguments); 1785 | }; 1786 | }()); 1787 | 1788 | -------------------------------------------------------------------------------- /grunt_tasks/jshint.js: -------------------------------------------------------------------------------- 1 | /* jshint node:true */ 2 | /* globals module */ 3 | module.exports = { 4 | files: [ 5 | 'ajax-form.js', 6 | 'gruntfile.js', 7 | 'grunt_tasks/*.js', 8 | 'test/unit/*.js' 9 | ], 10 | options: { 11 | jshintrc: true 12 | } 13 | }; -------------------------------------------------------------------------------- /grunt_tasks/karma.js: -------------------------------------------------------------------------------- 1 | /* jshint node:true */ 2 | /* globals module */ 3 | module.exports = { 4 | options: { 5 | autoWatch : false, 6 | 7 | basePath : '.', 8 | 9 | files : [ 10 | 'bower_components/bind-polyfill/index.js', 11 | 'ajax-form.js', 12 | 'test/unit/*-spec.js' 13 | ], 14 | 15 | frameworks: ['jasmine'], 16 | 17 | plugins : [ 18 | 'karma-firefox-launcher', 19 | 'karma-jasmine', 20 | 'karma-phantomjs-launcher', 21 | 'karma-spec-reporter' 22 | ], 23 | 24 | reporters : [ 25 | 'spec', 26 | ], 27 | 28 | singleRun: true 29 | 30 | }, 31 | dev: { 32 | browsers: ['PhantomJS'] 33 | }, 34 | travis: { 35 | browsers: ['PhantomJS', 'Firefox'] 36 | } 37 | }; -------------------------------------------------------------------------------- /gruntfile.js: -------------------------------------------------------------------------------- 1 | /* jshint node:true */ 2 | function config(name) { 3 | return require('./grunt_tasks/' + name + '.js'); 4 | } 5 | 6 | module.exports = function(grunt) { 7 | grunt.initConfig({ 8 | pkg: grunt.file.readJSON('package.json'), 9 | jshint: config('jshint'), 10 | watch: { 11 | files: ['ajax-form.js', 'grunt_tasks/*.js', 'test/unit/*'], 12 | tasks: ['jshint', 'karma:dev'] 13 | }, 14 | copy: { 15 | npmPreRelease: { 16 | files: [ 17 | {src: 'README.md', dest: 'dist/'}, 18 | {src: 'LICENSE', dest: 'dist/'}, 19 | {src: 'ajax-form.html', dest: 'dist/'}, 20 | {src: 'ajax-form.js', dest: 'dist/'}, 21 | {src: 'package.json', dest: 'dist/'} 22 | ] 23 | } 24 | }, 25 | shell: { 26 | npmRelease: { 27 | command: [ 28 | 'cd dist', 29 | 'npm publish' 30 | ].join('&&') 31 | }, 32 | wctLocal: { 33 | command: [ 34 | '$(npm bin)/wct --plugin local' 35 | ].join('&&') 36 | }, 37 | wctSauce: { 38 | command: [ 39 | '$(npm bin)/wct --plugin sauce' 40 | ].join('&&') 41 | } 42 | } 43 | }); 44 | 45 | grunt.loadNpmTasks('grunt-contrib-jshint'); 46 | grunt.loadNpmTasks('grunt-contrib-watch'); 47 | grunt.loadNpmTasks('grunt-contrib-copy'); 48 | grunt.loadNpmTasks('grunt-shell'); 49 | 50 | grunt.registerTask('default', ['jshint', 'shell:wctLocal']); 51 | grunt.registerTask('travis', ['jshint', 'shell:wctSauce']); 52 | grunt.registerTask('publishToNpm', ['jshint', 'shell:wctLocal', 'copy:npmPreRelease', 'shell:npmRelease']); 53 | }; 54 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": { 3 | "name": "Ray Nicholus" 4 | }, 5 | "bugs": "https://github.com/rnicholus/ajax-form/issues", 6 | "description": "HTML forms on performance-enhancing drugs", 7 | "devDependencies": { 8 | "bower": "1.3.x", 9 | "grunt": "0.4.x", 10 | "grunt-cli": "0.1.13", 11 | "grunt-contrib-copy": ">= 0.7.0", 12 | "grunt-contrib-jshint": "0.10.x", 13 | "grunt-contrib-watch": "^0.6.1", 14 | "grunt-shell": "^0.7.0", 15 | "web-component-tester": "3.3.22", 16 | "webcomponents.js": "0.7.2" 17 | }, 18 | "keywords": ["Polymer", "web-components", "form", "html", "ajax"], 19 | "license": "MIT", 20 | "name": "ajax-form", 21 | "repository": { 22 | "type": "git", 23 | "url": "https://github.com/rnicholus/ajax-form.git" 24 | }, 25 | "version": "2.1.4" 26 | } 27 | -------------------------------------------------------------------------------- /test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /test/typical-form-tests.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 467 | 468 | 469 | 470 | -------------------------------------------------------------------------------- /wct.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | local: { 4 | disabled: true, 5 | browsers: ['chrome', 'safari'] 6 | }, 7 | sauce: { 8 | disabled: true, 9 | browsers: 10 | [ 11 | { 12 | "browserName": "internet explorer", 13 | "platform": "Windows 8.1", 14 | "version": "11" 15 | }, 16 | { 17 | "browserName": "internet explorer", 18 | "platform": "Windows 7", 19 | "version": "10" 20 | }, 21 | 22 | //{ 23 | // "browserName": "chrome", 24 | // "platform": "OS X 10.10", 25 | // "version": "canary" 26 | //}, 27 | { 28 | "browserName": "chrome", 29 | "platform": "Windows 8.1", 30 | "version": "" 31 | }, 32 | { 33 | "browserName": "chrome", 34 | "platform": "Linux", 35 | "version": "" 36 | }, 37 | { 38 | "browserName": "firefox", 39 | "platform": "OS X 10.9", 40 | "version": "40" 41 | }, 42 | // { 43 | // "browserName": "firefox", 44 | // "platform": "Windows 8.1", 45 | // "version": "" 46 | // }, 47 | // { 48 | // "browserName": "firefox", 49 | // "platform": "Linux", 50 | // "version": "39" 51 | // }, 52 | // 53 | //{ 54 | // "browserName": "safari", 55 | // "platform": "OS X 10.10", 56 | // "version": "8" 57 | //}, 58 | { 59 | "browserName": "safari", 60 | "platform": "OS X 10.11", 61 | "version": "9" 62 | } 63 | ] 64 | } 65 | } 66 | }; 67 | --------------------------------------------------------------------------------