Skip to content

Commit

Permalink
FormData.constructor: implement optional submitter parameter
Browse files Browse the repository at this point in the history
In order to fully support it, this requires more robust Image Button
support (i.e. track selected coordinate, use when constructing form
data set)

Spec: https://xhr.spec.whatwg.org/#interface-formdata
Spec PR: whatwg/xhr#366
  • Loading branch information
jenseng committed Feb 17, 2023
1 parent 65ef06f commit 197685a
Show file tree
Hide file tree
Showing 6 changed files with 63 additions and 13 deletions.
19 changes: 15 additions & 4 deletions lib/jsdom/living/events/MouseEvent-impl.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,31 +16,42 @@ class MouseEventImpl extends UIEventImpl {
}
get pageX() {
if (this._dispatchFlag) {
return 0;
return this._initialContainingBlockRelativeY;
}
const offset = wrapperForImpl(this.view)?.scrollX || 0;
return offset + this.clientX;
}
get pageY() {
if (this._dispatchFlag) {
return 0;
return this._initialContainingBlockRelativeY;
}
const offset = wrapperForImpl(this.view)?.scrollY || 0;
return offset + this.clientY;
}
get offsetX() {
if (this._dispatchFlag) {
return 0;
return this._paddingEdgeRelativeX;
}
return this.pageX;
}
get offsetY() {
if (this._dispatchFlag) {
return 0;
return this._paddingEdgeRelativeY;
}
return this.pageY;
}

constructor(globalObject, args, privateData) {
super(globalObject, args, privateData);

// Used for coordinate values during dispatch and to retrieve the selected coordinate
// TODO: support non-zero values somehow if/when jsdom adds layout support
this._initialContainingBlockRelativeX = 0;
this._initialContainingBlockRelativeY = 0;
this._paddingEdgeRelativeX = 0;
this._paddingEdgeRelativeY = 0;
}

initMouseEvent(
type,
bubbles,
Expand Down
12 changes: 12 additions & 0 deletions lib/jsdom/living/nodes/HTMLFormElement-impl.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,18 @@ class HTMLFormElementImpl extends HTMLElementImpl {
super._descendantRemoved(parent, child);
}

_getSubmittableElementNodes() {
return domSymbolTree.treeToArray(this.getRootNode({}), {
filter: node => {
if (!isSubmittable(node)) {
return false;
}

return formOwner(node) === this;
}
});
}

_getElementNodes() {
return domSymbolTree.treeToArray(this.getRootNode({}), {
filter: node => {
Expand Down
11 changes: 9 additions & 2 deletions lib/jsdom/living/nodes/HTMLInputElement-impl.js
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ class HTMLInputElementImpl extends HTMLElementImpl {
}
}

_activationBehavior() {
_activationBehavior(event) {
if (!this._mutable && this.type !== "checkbox" && this.type !== "radio") {
return;
}
Expand All @@ -200,7 +200,14 @@ class HTMLInputElementImpl extends HTMLElementImpl {
fireAnEvent("input", this, undefined, { bubbles: true });
fireAnEvent("change", this, undefined, { bubbles: true });
}
} else if (form && (this.type === "submit" || this.type === "image")) {
} else if (form && this.type === "image") {
// https://html.spec.whatwg.org/multipage/input.html#image-button-state-(type=image):input-activation-behavior

// At this point the dispatch flag is false, so offsetX/Y reflect pageX/Y, which is not what we want.
// Instead we use _paddingEdgeRelativeX/Y, since they reflect the actual selected coordinates.
this._selectedCoordinate = { x: event._paddingEdgeRelativeX, y: event._paddingEdgeRelativeY };
form._doRequestSubmit(this);
} else if (form && this.type === "submit") {
form._doRequestSubmit(this);
} else if (form && this.type === "reset") {
form._doReset();
Expand Down
31 changes: 26 additions & 5 deletions lib/jsdom/living/xhr/FormData-impl.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
"use strict";
const DOMException = require("domexception/webidl2js-wrapper");
const idlUtils = require("../generated/utils");
const { closest } = require("../helpers/traversal");
const { isDisabled, isSubmittable, isButton } = require("../helpers/form-controls");
const { isDisabled, isButton, isSubmitButton } = require("../helpers/form-controls");
const Blob = require("../generated/Blob.js");
const File = require("../generated/File.js");
const conversions = require("webidl-conversions");
Expand All @@ -12,7 +13,19 @@ exports.implementation = class FormDataImpl {
this._entries = [];

if (args[0] !== undefined) {
this._entries = constructTheEntryList(args[0]);
const [form, submitter = null] = args;
if (submitter !== null) {
if (!isSubmitButton(submitter)) {
throw new TypeError("The specified element is not a submit button");
}
if (submitter.form !== form) {
throw DOMException.create(this._globalObject, [
"The specified element is not owned by this form element",
"NotFoundError"
]);
}
}
this._entries = constructTheEntryList(form, submitter);
}
}

Expand Down Expand Up @@ -96,7 +109,7 @@ function constructTheEntryList(form, submitter) {
// TODO: handle encoding
// TODO: handling "constructing entry list"

const controls = form.elements.filter(isSubmittable); // submittable is a subset of listed
const controls = form._getSubmittableElementNodes();
const entryList = [];

for (const field of controls) {
Expand All @@ -119,10 +132,18 @@ function constructTheEntryList(form, submitter) {
continue;
}

// TODO: Handle <input type="image">
const name = field.getAttributeNS(null, "name");
if (
field.localName === "input" && field.type === "image") {
const prefix = name ? `${name}.` : "";
const coordinate = field._selectedCoordinate ?? { x: 0, y: 0 };
appendAnEntry(entryList, `${prefix}x`, coordinate.x);
appendAnEntry(entryList, `${prefix}y`, coordinate.y);
continue;
}

// TODO: handle form-associated custom elements.

const name = field.getAttributeNS(null, "name");
if (name === null || name === "") {
continue;
}
Expand Down
2 changes: 1 addition & 1 deletion lib/jsdom/living/xhr/FormData.webidl
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ typedef (File or USVString) FormDataEntryValue;

[Exposed=(Window,Worker)]
interface FormData {
constructor(optional HTMLFormElement form);
constructor(optional HTMLFormElement form, optional HTMLElement? submitter = null);

undefined append(USVString name, USVString value);
undefined append(USVString name, Blob value, optional USVString filename);
Expand Down
1 change: 0 additions & 1 deletion test/web-platform-tests/to-run.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1476,7 +1476,6 @@ event-upload-progress.any.html: [fail, Unknown]
formdata.html:
"Newly created FormData contains entries added to \"formData\" IDL attribute of FormDataEvent.": [fail, FormDataEvent not implemented]
"|new FormData()| in formdata event handler should throw": [fail, FormDataEvent not implemented]
formdata/constructor-submitter.html: [fail, Not implemented]
getallresponseheaders.htm: [fail, Unknown]
getresponseheader.any.html: [fail, Unknown]
headers-normalize-response.htm: [timeout, Unknown]
Expand Down

0 comments on commit 197685a

Please sign in to comment.