Skip to content

Commit

Permalink
Re-implement FormData
Browse files Browse the repository at this point in the history
- Fixes #1533: blobs and files and filenames weren't quite working right
- Forms are now serialized according to the rather-complex form data set serialization algorithm
- Tweaked some textarea value stuff; unsure if there are visible changes, but now it is much closer to the spec
  • Loading branch information
domenic committed Jul 2, 2016
1 parent 9a9c5fb commit 63f8010
Show file tree
Hide file tree
Showing 16 changed files with 407 additions and 127 deletions.
2 changes: 1 addition & 1 deletion .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@
"max-params": "off",
"max-statements": "off",
"max-statements-per-line": ["error", { "max": 1 }],
"new-cap": "error",
"new-cap": ["error", { "capIsNewExceptions": ["USVString"] }],
"new-parens": "error",
"newline-after-var": "off",
"newline-before-return": "off",
Expand Down
6 changes: 1 addition & 5 deletions lib/jsdom/living/blob.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

const blobSymbols = require("./blob-symbols");

class Blob {
module.exports = class Blob {
constructor() {
if (!(this instanceof Blob)) {
throw new TypeError("DOM object constructor cannot be called as a function.");
Expand Down Expand Up @@ -71,8 +71,4 @@ class Blob {
toString() {
return "[object Blob]";
}
}

module.exports = function (core) {
core.Blob = Blob;
};
24 changes: 10 additions & 14 deletions lib/jsdom/living/file.js
Original file line number Diff line number Diff line change
@@ -1,21 +1,17 @@
"use strict";

const fileSymbols = require("./file-symbols");
const Blob = require("./blob");

module.exports = function (core) {
const Blob = core.Blob;

class File extends Blob {
constructor(fileBits, fileName) {
super(fileBits, arguments[2]);
if (!(this instanceof File)) {
throw new TypeError("DOM object constructor cannot be called as a function.");
}
this[fileSymbols.name] = fileName.replace(/\//g, ":");
}
get name() {
return this[fileSymbols.name];
module.exports = class File extends Blob {
constructor(fileBits, fileName) {
super(fileBits, arguments[2]);
if (!(this instanceof File)) {
throw new TypeError("DOM object constructor cannot be called as a function.");
}
this[fileSymbols.name] = fileName.replace(/\//g, ":");
}
get name() {
return this[fileSymbols.name];
}
core.File = File;
};
80 changes: 0 additions & 80 deletions lib/jsdom/living/form-data.js

This file was deleted.

22 changes: 21 additions & 1 deletion lib/jsdom/living/helpers/form-controls.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
"use strict";

const submittableLocalNames = new Set(["button", "input", "keygen", "object", "select", "textarea"]);

exports.isDisabled = formControl => {
if (formControl.localName === "button" || formControl.localName === "input" || formControl.localName === "select" ||
formControl.localName === "textarea") {
Expand All @@ -20,4 +22,22 @@ exports.isDisabled = formControl => {
}

return false;
}
};

exports.isSubmittable = formControl => {
// https://html.spec.whatwg.org/multipage/forms.html#category-submit
return submittableLocalNames.has(formControl.localName);
};

exports.isButton = formControl => {
// https://html.spec.whatwg.org/multipage/forms.html#concept-button
return formControl.type === "button" || formControl.type === "submit" || formControl.type === "reset" ||
formControl.type === "image" || formControl.localName === "button";
};

exports.normalizeToCRLF = string => {
return string.replace(/\r([^\n])/g, "\r\n$1")
.replace(/\r$/, "\r\n")
.replace(/([^\r])\n/g, "$1\r\n")
.replace(/^\n/, "\r\n");
};
7 changes: 4 additions & 3 deletions lib/jsdom/living/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ exports.History = require("./generated/History").interface;

exports.DOMParser = require("./generated/DOMParser").interface;

exports.FormData = require("./generated/FormData").interface;

require("./register-elements")(exports);

// These need to be cleaned up...
Expand All @@ -45,10 +47,9 @@ require("./html-collection")(exports);
require("./node-filter")(exports);
require("./node-iterator")(exports);
require("./node-list")(exports);
require("./blob")(exports);
require("./file")(exports);
exports.Blob = require("./blob");
exports.File = require("./file");
require("./filelist")(exports);
require("./form-data")(exports);
require("./xmlhttprequest-event-target")(exports);
require("./xmlhttprequest-upload")(exports);

Expand Down
5 changes: 5 additions & 0 deletions lib/jsdom/living/nodes/HTMLButtonElement-impl.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ class HTMLButtonElementImpl extends HTMLElementImpl {
}
}

_getValue() {
const valueAttr = this.getAttribute("value");
return valueAttr === null ? "" : valueAttr;
}

get form() {
return closest(this, "form");
}
Expand Down
4 changes: 4 additions & 0 deletions lib/jsdom/living/nodes/HTMLInputElement-impl.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ class HTMLInputElementImpl extends HTMLElementImpl {
this._preCancelState = null;
}

_getValue() {
return this._value;
}

_preClickActivationSteps() {
if (this.type === "checkbox") {
this.checked = !this.checked;
Expand Down
42 changes: 22 additions & 20 deletions lib/jsdom/living/nodes/HTMLTextAreaElement-impl.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,38 @@ const HTMLElementImpl = require("./HTMLElement-impl").implementation;

const DOMException = require("../../web-idl/DOMException");
const closest = require("../helpers/traversal").closest;
const normalizeToCRLF = require("../helpers/form-controls").normalizeToCRLF;

class HTMLTextAreaElement extends HTMLElementImpl {
constructor(args, privateData) {
super(args, privateData);

this._apiValue = null;
this._rawValue = "";
this._dirtyValue = false;
}

_formReset() {
this._apiValue = null;
this._rawValue = this.textContent;
this._dirtyValue = false;
}

_getAPIValue() {
return this._rawValue.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
}

_getValue() {
// Hard-wrapping omitted, for now.
return normalizeToCRLF(this._rawValue);
}

_modified() {
super._modified();

if (this._dirtyValue === false) {
this._rawValue = this.textContent;
}
}

get form() {
return closest(this, "form");
}
Expand All @@ -31,28 +49,12 @@ class HTMLTextAreaElement extends HTMLElementImpl {
}

get value() {
// The WHATWG specifies that when "textContent" changes, the "raw value"
// (just the API value in jsdom) must also be updated.
// This slightly different solution has identical results, but is a lot less complex.
if (this._dirtyValue) {
if (this._apiValue === null) {
return "";
}
return this._apiValue;
}

let val = this.defaultValue;
val = val.replace(/\r\n|\r/g, "\n"); // API value normalizes line breaks per WHATWG
return val;
return this._getAPIValue();
}

set value(val) {
if (val) {
val = val.replace(/\r\n|\r/g, "\n"); // API value normalizes line breaks per WHATWG
}

this._rawValue = val;
this._dirtyValue = true;
this._apiValue = val;

this._selectionStart = 0;
this._selectionEnd = 0;
Expand Down

0 comments on commit 63f8010

Please sign in to comment.