Commit 4a5b059d authored by Kent Tamura's avatar Kent Tamura Committed by Commit Bot

Form-associated custom elements: Implement ElementInternals.prototype.setFormValue().

Add setFormValue() implementation to blink::ElementInternals, and it
also overrides ListElement::AppendToFormData().

Tests:
- custom-elements/form-submission-file.html
  This won't be merged to WPT because this test uses a Mojo interface mock.
- custom-elements/tentative/form-submission.html
  This will be merged to WPT.

Bug: 905922
Bug: https://github.com/w3c/webcomponents/issues/187
Change-Id: I1d28d1d5e8c2c412e7db18a0b39eb9d517054c04
Reviewed-on: https://chromium-review.googlesource.com/c/1350418
Commit-Queue: Kent Tamura <tkent@chromium.org>
Reviewed-by: default avatarHayato Ito <hayato@chromium.org>
Cr-Commit-Position: refs/heads/master@{#611111}
parent f40bd433
...@@ -5,19 +5,40 @@ ...@@ -5,19 +5,40 @@
#include "third_party/blink/renderer/core/html/custom/element_internals.h" #include "third_party/blink/renderer/core/html/custom/element_internals.h"
#include "third_party/blink/renderer/core/dom/node_lists_node_data.h" #include "third_party/blink/renderer/core/dom/node_lists_node_data.h"
#include "third_party/blink/renderer/core/fileapi/file.h"
#include "third_party/blink/renderer/core/html/forms/form_data.h"
#include "third_party/blink/renderer/core/html/forms/html_form_element.h" #include "third_party/blink/renderer/core/html/forms/html_form_element.h"
#include "third_party/blink/renderer/core/html/html_element.h" #include "third_party/blink/renderer/core/html/html_element.h"
namespace blink { namespace blink {
ElementInternals::ElementInternals(HTMLElement& target) : target_(target) {} ElementInternals::ElementInternals(HTMLElement& target) : target_(target) {
value_.SetUSVString(String());
}
void ElementInternals::Trace(Visitor* visitor) { void ElementInternals::Trace(Visitor* visitor) {
visitor->Trace(target_); visitor->Trace(target_);
visitor->Trace(value_);
visitor->Trace(entry_source_);
ListedElement::Trace(visitor); ListedElement::Trace(visitor);
ScriptWrappable::Trace(visitor); ScriptWrappable::Trace(visitor);
} }
void ElementInternals::setFormValue(const FileOrUSVString& value) {
setFormValue(value, nullptr);
}
void ElementInternals::setFormValue(const FileOrUSVString& value,
FormData* entry_source) {
if (!entry_source || entry_source->size() == 0u) {
value_ = value;
entry_source_ = nullptr;
return;
}
value_ = value;
entry_source_ = MakeGarbageCollected<FormData>(*entry_source);
}
HTMLFormElement* ElementInternals::form() const { HTMLFormElement* ElementInternals::form() const {
return ListedElement::Form(); return ListedElement::Form();
} }
...@@ -52,4 +73,25 @@ bool ElementInternals::IsEnumeratable() const { ...@@ -52,4 +73,25 @@ bool ElementInternals::IsEnumeratable() const {
return true; return true;
} }
void ElementInternals::AppendToFormData(FormData& form_data) {
const AtomicString& name = Target().FastGetAttribute(html_names::kNameAttr);
if (!entry_source_ || entry_source_->size() == 0u) {
if (name.IsNull())
return;
if (value_.IsFile())
form_data.AppendFromElement(name, value_.GetAsFile());
else if (value_.IsUSVString())
form_data.AppendFromElement(name, value_.GetAsUSVString());
else
form_data.AppendFromElement(name, g_empty_string);
return;
}
for (const auto& entry : entry_source_->Entries()) {
if (entry->isFile())
form_data.append(entry->name(), entry->GetFile());
else
form_data.append(entry->name(), entry->Value());
}
}
} // namespace blink } // namespace blink
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_HTML_CUSTOM_ELEMENT_INTERNALS_H_ #ifndef THIRD_PARTY_BLINK_RENDERER_CORE_HTML_CUSTOM_ELEMENT_INTERNALS_H_
#define THIRD_PARTY_BLINK_RENDERER_CORE_HTML_CUSTOM_ELEMENT_INTERNALS_H_ #define THIRD_PARTY_BLINK_RENDERER_CORE_HTML_CUSTOM_ELEMENT_INTERNALS_H_
#include "third_party/blink/renderer/bindings/core/v8/file_or_usv_string.h"
#include "third_party/blink/renderer/core/html/forms/listed_element.h" #include "third_party/blink/renderer/core/html/forms/listed_element.h"
#include "third_party/blink/renderer/platform/bindings/script_wrappable.h" #include "third_party/blink/renderer/platform/bindings/script_wrappable.h"
#include "third_party/blink/renderer/platform/wtf/casting.h" #include "third_party/blink/renderer/platform/wtf/casting.h"
...@@ -25,6 +26,8 @@ class ElementInternals : public ScriptWrappable, public ListedElement { ...@@ -25,6 +26,8 @@ class ElementInternals : public ScriptWrappable, public ListedElement {
void DidUpgrade(); void DidUpgrade();
// IDL attributes/operations // IDL attributes/operations
void setFormValue(const FileOrUSVString& value);
void setFormValue(const FileOrUSVString& value, FormData* entry_source);
HTMLFormElement* form() const; HTMLFormElement* form() const;
private: private:
...@@ -32,9 +35,13 @@ class ElementInternals : public ScriptWrappable, public ListedElement { ...@@ -32,9 +35,13 @@ class ElementInternals : public ScriptWrappable, public ListedElement {
bool IsFormControlElement() const override; bool IsFormControlElement() const override;
bool IsElementInternals() const override; bool IsElementInternals() const override;
bool IsEnumeratable() const override; bool IsEnumeratable() const override;
void AppendToFormData(FormData& form_data) override;
Member<HTMLElement> target_; Member<HTMLElement> target_;
FileOrUSVString value_;
Member<FormData> entry_source_;
DISALLOW_COPY_AND_ASSIGN(ElementInternals); DISALLOW_COPY_AND_ASSIGN(ElementInternals);
}; };
......
...@@ -10,6 +10,7 @@ ...@@ -10,6 +10,7 @@
] ]
interface ElementInternals { interface ElementInternals {
// Attributes and operations for form-associated custom elements. // Attributes and operations for form-associated custom elements.
void setFormValue(FormDataEntryValue value, optional FormData entrySource);
readonly attribute HTMLFormElement? form; readonly attribute HTMLFormElement? form;
}; };
...@@ -87,6 +87,11 @@ String Normalize(const String& input) { ...@@ -87,6 +87,11 @@ String Normalize(const String& input) {
FormData::FormData(const WTF::TextEncoding& encoding) : encoding_(encoding) {} FormData::FormData(const WTF::TextEncoding& encoding) : encoding_(encoding) {}
FormData::FormData(const FormData& form_data)
: encoding_(form_data.encoding_),
entries_(form_data.entries_),
contains_password_data_(form_data.contains_password_data_) {}
FormData::FormData() : encoding_(UTF8Encoding()) {} FormData::FormData() : encoding_(UTF8Encoding()) {}
FormData* FormData::Create(HTMLFormElement* form, FormData* FormData::Create(HTMLFormElement* form,
......
...@@ -66,6 +66,9 @@ class CORE_EXPORT FormData final ...@@ -66,6 +66,9 @@ class CORE_EXPORT FormData final
} }
explicit FormData(const WTF::TextEncoding&); explicit FormData(const WTF::TextEncoding&);
// Clones form_data. This clones |form_data.entries_| Vector, but
// doesn't clone entries in it because they are immutable.
FormData(const FormData& form_data);
FormData(); FormData();
void Trace(blink::Visitor*) override; void Trace(blink::Visitor*) override;
......
<!DOCTYPE html>
<script src="file:///gen/layout_test_data/mojo/public/js/mojo_bindings.js"></script>
<script src="file:///gen/third_party/blink/public/mojom/choosers/file_chooser.mojom.js"></script>
<script src="../fast/forms/resources/mock-file-chooser.js"></script>
<script src="../fast/forms/resources/common.js"></script>
<script src="../resources/testharness.js"></script>
<script src="../resources/testharnessreport.js"></script>
<div id="container"></div>
<input type=file multiple>
<script>
class MyControl extends HTMLElement {
static get formAssociated() { return true; }
constructor() {
super();
this.internals_ = this.attachInternals();
this.value_ = '';
}
get value() {
return this.value_;
}
set value(fileOrString) {
this.internals_.setFormValue(fileOrString);
this.value_ = fileOrString;
}
setValues(nameValues) {
const formData = new FormData();
for (let p of nameValues) {
formData.append(p[0], p[1]);
}
this.internals_.setFormValue(this.value_, formData);
}
}
customElements.define('my-control', MyControl);
function selectFilePromise(paths) {
return new Promise((resolve, reject) => {
mockFileChooserFactory.setPathsToBeChosen(paths);
const file = document.querySelector('input[type=file]');
file.addEventListener('change', () => { resolve(file) });
clickElement(file);
setTimeout(reject, 3000);
});
}
function submitPromise(t) {
return new Promise((resolve, reject) => {
const iframe = document.querySelector('iframe');
iframe.onload = () => resolve(iframe.contentWindow.location.search);
iframe.onerror = () => reject();
document.querySelector('form').submit();
});
}
promise_test(async t => {
$('container').innerHTML = '<form action="../external/wpt/common/blank.html" target="if1">' +
'<input name=name-pd1 value="value-pd1">' +
'<my-control name=ce1></my-control>' +
'</form>' +
'<iframe name="if1"></iframe>';
const fileInput = await selectFilePromise(['../OWNERS']);
document.querySelector('my-control').value = fileInput.files[0];
return submitPromise(t).then(query => {
assert_equals(query, '?name-pd1=value-pd1&ce1=OWNERS');
});
}, 'Single value - A file value');
promise_test(async t => {
$('container').innerHTML = '<form action="../external/wpt/common/blank.html" target="if1">' +
'<my-control name=ce1></my-control>' +
'</form>' +
'<iframe name="if1"></iframe>';
const fileInput = await selectFilePromise(['../OWNERS', '../PRESUBMIT.py']);
document.querySelector('my-control').setValues(
[['sub1', fileInput.files[0]], ['sub2', fileInput.files[1]]]);
return submitPromise(t).then(query => {
assert_equals(query, '?sub1=OWNERS&sub2=PRESUBMIT.py');
});
}, 'Multiple values - file values');
</script>
<!DOCTYPE html>
<script src="../../resources/testharness.js"></script>
<script src="../../resources/testharnessreport.js"></script>
<div id="container"></div>
<script>
class MyControl extends HTMLElement {
static get formAssociated() { return true; }
constructor() {
super();
this.internals_ = this.attachInternals();
this.value_ = '';
}
get value() {
return this.value_;
}
set value(str) {
this.internals_.setFormValue(str);
this.value_ = str;
}
setValues(nameValues) {
const formData = new FormData();
for (let p of nameValues) {
formData.append(p[0], p[1]);
}
this.internals_.setFormValue(this.value_, formData);
}
}
customElements.define('my-control', MyControl);
const $ = document.querySelector.bind(document);
function submitPromise(t) {
return new Promise((resolve, reject) => {
const iframe = $('iframe');
iframe.onload = () => resolve(iframe.contentWindow.location.search);
iframe.onerror = () => reject();
$('form').submit();
});
}
promise_test(t => {
$('#container').innerHTML = '<form action="../../external/wpt/common/blank.html" target="if1">' +
'<input name=name-pd1 value="value-pd1">' +
'<my-control></my-control>' +
'</form>' +
'<iframe name="if1"></iframe>';
return submitPromise(t).then(query => {
assert_equals(query, '?name-pd1=value-pd1');
});
}, 'Single value - name is missing');
promise_test(t => {
$('#container').innerHTML = '<form action="../../external/wpt/common/blank.html" target="if1">' +
'<input name=name-pd1 value="value-pd1">' +
'<my-control name=""></my-control>' +
'<input name=name-pd2 value="value-pd2">' +
'</form>' +
'<iframe name="if1"></iframe>';
$('my-control').value = 'value-ce1';
return submitPromise(t).then(query => {
assert_equals(query, '?name-pd1=value-pd1&=value-ce1&name-pd2=value-pd2');
});
}, 'Single value - empty name exists');
promise_test(t => {
$('#container').innerHTML = '<form action="../../external/wpt/common/blank.html" target="if1">' +
'<input name=name-pd1 value="value-pd1">' +
'<my-control name="name-ce1"></my-control>' +
'<my-control name="name-ce2"></my-control>' +
'</form>' +
'<iframe name="if1"></iframe>';
$('my-control').value = 'value-ce1';
return submitPromise(t).then(query => {
assert_equals(query, '?name-pd1=value-pd1&name-ce1=value-ce1&name-ce2=');
});
}, 'Single value - Non-empty name exists');
promise_test(t => {
$('#container').innerHTML = '<form action="../../external/wpt/common/blank.html" target="if1">' +
'<input name=name-pd1 value="value-pd1">' +
'<my-control></my-control>' +
'</form>' +
'<iframe name="if1"></iframe>';
$('my-control').value = 'value-ce1';
$('my-control').setValues([['sub1', 'subvalue1'],
['sub2', 'subvalue2'],
['sub2', 'subvalue3']]);
return submitPromise(t).then(query => {
assert_equals(query, '?name-pd1=value-pd1&sub1=subvalue1&sub2=subvalue2&sub2=subvalue3');
});
}, 'Multiple values - name content attribute is ignored');
</script>
...@@ -87,12 +87,21 @@ class MockFileChooser { ...@@ -87,12 +87,21 @@ class MockFileChooser {
} }
function toFilePath(str) { function toFilePath(str) {
if (!navigator.platform.startsWith('Win')) if (str === '')
return { path: str }; return navigator.platform.startsWith('Win') ? { path: [] } : { path: '' };
str = str.replace(/\//g, '\\'); const absoluteUrl = (new URL(str, document.URL)).href;
console.assert(absoluteUrl.startsWith('file:///'),
'File selection works only on file: URL documents; document.URL=' + document.URL);
if (!navigator.platform.startsWith('Win')) {
// "file:///Users/foo/..." -> "/Users/foo/..."
return { path: absoluteUrl.substr(7) };
}
// "file:///D:/Users/foo/..." -> "/Users/foo/..."
// We omit drive letters to get identical results on multiple platforms.
const fullPath = absoluteUrl.substr(10).replace(/\//g, '\\');
const path = []; const path = [];
for (let i = 0; i < str.length; ++i) { for (let i = 0; i < fullPath.length; ++i) {
path.push(str.charCodeAt(i)); path.push(fullPath.charCodeAt(i));
} }
return { path: path }; return { path: path };
} }
......
...@@ -2049,6 +2049,7 @@ interface ElementInternals ...@@ -2049,6 +2049,7 @@ interface ElementInternals
attribute @@toStringTag attribute @@toStringTag
getter form getter form
method constructor method constructor
method setFormValue
interface EnterPictureInPictureEvent : Event interface EnterPictureInPictureEvent : Event
attribute @@toStringTag attribute @@toStringTag
getter pictureInPictureWindow getter pictureInPictureWindow
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment