Bug 1556358 - Part 2: Save and restore custom element form data. r=edgar

Depends on D175542

Differential Revision: https://phabricator.services.mozilla.com/D174115
This commit is contained in:
Adam Vandolder 2023-06-19 13:44:15 +00:00
parent 6794259cb5
commit d6f990f53a
15 changed files with 496 additions and 50 deletions

View File

@ -150,6 +150,7 @@
#include "mozilla/dom/ContentParent.h"
#include "mozilla/dom/CustomElementRegistry.h"
#include "mozilla/dom/CustomElementRegistryBinding.h"
#include "mozilla/dom/CustomElementTypes.h"
#include "mozilla/dom/DOMArena.h"
#include "mozilla/dom/DOMException.h"
#include "mozilla/dom/DOMExceptionBinding.h"
@ -168,8 +169,10 @@
#include "mozilla/dom/FileBlobImpl.h"
#include "mozilla/dom/FileSystemSecurity.h"
#include "mozilla/dom/FilteredNodeIterator.h"
#include "mozilla/dom/FormData.h"
#include "mozilla/dom/FragmentOrElement.h"
#include "mozilla/dom/FromParser.h"
#include "mozilla/dom/HTMLElement.h"
#include "mozilla/dom/HTMLFormElement.h"
#include "mozilla/dom/HTMLInputElement.h"
#include "mozilla/dom/HTMLTextAreaElement.h"
@ -9921,6 +9924,97 @@ void nsContentUtils::EnqueueLifecycleCallback(
aDefinition);
}
/* static */
CustomElementFormValue nsContentUtils::ConvertToCustomElementFormValue(
const OwningFileOrUSVStringOrFormData& aState) {
if (aState.IsFile()) {
RefPtr<BlobImpl> impl = aState.GetAsFile()->Impl();
return {std::move(impl)};
}
if (aState.IsUSVString()) {
return aState.GetAsUSVString();
}
nsTArray<FormDataTuple> array;
aState.GetAsFormData()->ForEach(
[&array](const nsString& aName,
const OwningBlobOrDirectoryOrUSVString& aValue) {
FormDataTuple data;
data.name() = aName;
if (aValue.IsBlob()) {
FormDataValue value(WrapNotNull(aValue.GetAsBlob()->Impl()));
data.value() = std::move(value);
} else if (aValue.IsUSVString()) {
data.value() = aValue.GetAsUSVString();
} else {
MOZ_ASSERT_UNREACHABLE("Can't save FormData entry Directory value!");
}
array.AppendElement(data);
return true;
});
return std::move(array);
}
/* static */
Nullable<OwningFileOrUSVStringOrFormData>
nsContentUtils::ExtractFormAssociatedCustomElementValue(
mozilla::dom::HTMLElement* aElement,
const mozilla::dom::CustomElementFormValue& aCEValue) {
OwningFileOrUSVStringOrFormData value;
switch (aCEValue.type()) {
case CustomElementFormValue::TBlobImpl: {
nsPIDOMWindowInner* window = aElement->OwnerDoc()->GetInnerWindow();
if (!window) {
return {};
}
RefPtr<File> file =
File::Create(window->AsGlobal(), aCEValue.get_BlobImpl());
if (NS_WARN_IF(!file)) {
return {};
}
value.SetAsFile() = file;
} break;
case CustomElementFormValue::TnsString:
value.SetAsUSVString() = aCEValue.get_nsString();
break;
case CustomElementFormValue::TArrayOfFormDataTuple: {
const auto& array = aCEValue.get_ArrayOfFormDataTuple();
auto formData = MakeRefPtr<FormData>();
for (auto i = 0ul; i < array.Length(); ++i) {
const auto& item = array.ElementAt(i);
switch (item.value().type()) {
case FormDataValue::TnsString:
formData->AddNameValuePair(item.name(),
item.value().get_nsString());
break;
case FormDataValue::TBlobImpl: {
auto blobImpl = item.value().get_BlobImpl();
auto* blob = Blob::Create(
aElement->GetComposedDoc()->GetOwnerGlobal(), blobImpl);
formData->AddNameBlobPair(item.name(), blob);
} break;
default:
continue;
}
}
value.SetAsFormData() = formData;
} break;
case CustomElementFormValue::Tvoid_t:
return {};
default:
NS_WARNING("Invalid CustomElementContentData type!");
return {};
}
return value;
}
/* static */
void nsContentUtils::AppendDocumentLevelNativeAnonymousContentTo(
Document* aDocument, nsTArray<nsIContent*>& aElements) {

View File

@ -174,6 +174,7 @@ class ContentChild;
class ContentFrameMessageManager;
class ContentParent;
struct CustomElementDefinition;
class CustomElementFormValue;
class CustomElementRegistry;
class DataTransfer;
class Document;
@ -182,6 +183,7 @@ class DOMArena;
class Element;
class Event;
class EventTarget;
class HTMLElement;
class HTMLInputElement;
class IPCTransferable;
class IPCTransferableData;
@ -190,6 +192,7 @@ class IPCTransferableDataItem;
struct LifecycleCallbackArgs;
class MessageBroadcaster;
class NodeInfo;
class OwningFileOrUSVStringOrFormData;
class Selection;
struct StructuredSerializeOptions;
class WorkerPrivate;
@ -3049,6 +3052,14 @@ class nsContentUtils {
const mozilla::dom::LifecycleCallbackArgs& aArgs,
mozilla::dom::CustomElementDefinition* aDefinition = nullptr);
static mozilla::dom::CustomElementFormValue ConvertToCustomElementFormValue(
const mozilla::dom::OwningFileOrUSVStringOrFormData& aState);
static mozilla::dom::Nullable<mozilla::dom::OwningFileOrUSVStringOrFormData>
ExtractFormAssociatedCustomElementValue(
mozilla::dom::HTMLElement* aElement,
const mozilla::dom::CustomElementFormValue& aCEValue);
/**
* Appends all "document level" native anonymous content subtree roots for
* aDocument to aElements. Document level NAC subtrees are those created

View File

@ -455,14 +455,20 @@ class nsIContent : public nsINode {
*/
static inline bool RequiresDoneCreatingElement(int32_t aNamespace,
nsAtom* aName) {
if (aNamespace == kNameSpaceID_XHTML &&
(aName == nsGkAtoms::input || aName == nsGkAtoms::button ||
aName == nsGkAtoms::audio || aName == nsGkAtoms::video)) {
MOZ_ASSERT(
!RequiresDoneAddingChildren(aNamespace, aName),
"Both DoneCreatingElement and DoneAddingChildren on a same element "
"isn't supported.");
return true;
if (aNamespace == kNameSpaceID_XHTML) {
if (aName == nsGkAtoms::input || aName == nsGkAtoms::button ||
aName == nsGkAtoms::audio || aName == nsGkAtoms::video) {
MOZ_ASSERT(!RequiresDoneAddingChildren(aNamespace, aName),
"Both DoneCreatingElement and DoneAddingChildren on a "
"same element isn't supported.");
return true;
}
if (aName->IsDynamic()) {
// This could be a form-associated custom element, so check if its
// name includes a -.
nsDependentString name(aName->GetUTF16String());
return name.Contains('-');
}
}
return false;
}

View File

@ -49,7 +49,8 @@ ElementInternals::ElementInternals(HTMLElement* aTarget)
: nsIFormControl(FormControlType::FormAssociatedCustomElement),
mTarget(aTarget),
mForm(nullptr),
mFieldSet(nullptr) {}
mFieldSet(nullptr),
mControlNumber(-1) {}
nsISupports* ElementInternals::GetParentObject() { return ToSupports(mTarget); }
@ -457,4 +458,26 @@ DocGroup* ElementInternals::GetDocGroup() {
return mTarget->OwnerDoc()->GetDocGroup();
}
void ElementInternals::RestoreFormValue(
Nullable<OwningFileOrUSVStringOrFormData>&& aValue,
Nullable<OwningFileOrUSVStringOrFormData>&& aState) {
mSubmissionValue = aValue;
mState = aState;
if (!mState.IsNull()) {
nsContentUtils::EnqueueLifecycleCallback(
ElementCallbackType::eFormStateRestore, mTarget,
{
.mState = mState,
.mReason = RestoreReason::Restore,
});
}
}
void ElementInternals::InitializeControlNumber() {
MOZ_ASSERT(mControlNumber == -1,
"FACE control number should only be initialized once!");
mControlNumber = mTarget->OwnerDoc()->GetNextControlNumber();
}
} // namespace mozilla::dom

View File

@ -82,11 +82,33 @@ class ElementInternals final : public nsIFormControl,
NS_IMETHOD Reset() override;
NS_IMETHOD SubmitNamesValues(mozilla::dom::FormData* aFormData) override;
bool AllowDrop() override { return true; }
int32_t GetParserInsertedControlNumberForStateKey() const override {
return mControlNumber;
}
void SetFieldSet(mozilla::dom::HTMLFieldSetElement* aFieldSet) {
mFieldSet = aFieldSet;
}
const Nullable<OwningFileOrUSVStringOrFormData>& GetFormSubmissionValue()
const {
return mSubmissionValue;
}
const Nullable<OwningFileOrUSVStringOrFormData>& GetFormState() const {
return mState;
}
void RestoreFormValue(Nullable<OwningFileOrUSVStringOrFormData>&& aValue,
Nullable<OwningFileOrUSVStringOrFormData>&& aState);
const nsCString& GetStateKey() const { return mStateKey; }
void SetStateKey(nsCString&& key) {
MOZ_ASSERT(mStateKey.IsEmpty(), "FACE state key should only be set once!");
mStateKey = key;
}
void InitializeControlNumber();
void UpdateFormOwner();
void UpdateBarredFromConstraintValidation();
@ -166,8 +188,8 @@ class ElementInternals final : public nsIFormControl,
Nullable<OwningFileOrUSVStringOrFormData> mSubmissionValue;
// https://html.spec.whatwg.org/#face-state
// TODO: Bug 1734841 - Figure out how to support form restoration or
// autocomplete for form-associated custom element
// TODO: Bug 1734841 - Figure out how to support autocomplete for
// form-associated custom element.
Nullable<OwningFileOrUSVStringOrFormData> mState;
// https://html.spec.whatwg.org/#face-validation-message
@ -177,6 +199,15 @@ class ElementInternals final : public nsIFormControl,
RefPtr<nsGenericHTMLElement> mValidationAnchor;
AttrArray mAttrs;
// Used to store the key to a form-associated custom element in the current
// session. Is empty until element has been upgraded.
nsCString mStateKey;
// A number for a form-associated custom element that is unique within its
// owner document. This is only set to a number for elements inserted into the
// document by the parser from the network. Otherwise, it is -1.
int32_t mControlNumber;
};
} // namespace mozilla::dom

View File

@ -6,18 +6,27 @@
#include "mozilla/dom/HTMLElement.h"
#include "mozilla/dom/CustomElementRegistry.h"
#include "mozilla/dom/HTMLElementBinding.h"
#include "mozilla/EventDispatcher.h"
#include "mozilla/PresState.h"
#include "mozilla/dom/CustomElementRegistry.h"
#include "mozilla/dom/ElementInternalsBinding.h"
#include "mozilla/dom/FormData.h"
#include "mozilla/dom/HTMLElementBinding.h"
#include "nsContentUtils.h"
#include "nsGenericHTMLElement.h"
#include "nsILayoutHistoryState.h"
namespace mozilla::dom {
HTMLElement::HTMLElement(already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo)
HTMLElement::HTMLElement(already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo,
FromParser aFromParser)
: nsGenericHTMLFormElement(std::move(aNodeInfo)) {
if (NodeInfo()->Equals(nsGkAtoms::bdi)) {
AddStatesSilently(ElementState::HAS_DIR_ATTR_LIKE_AUTO);
}
if (aFromParser & FROM_PARSER_NETWORK) {
SetFlags(HTML_ELEMENT_FROM_PARSER_NETWORK);
}
}
NS_IMPL_CYCLE_COLLECTION_INHERITED(HTMLElement, nsGenericHTMLFormElement)
@ -73,6 +82,91 @@ void HTMLElement::UnbindFromTree(bool aNullParent) {
UpdateBarredFromConstraintValidation();
}
void HTMLElement::DoneCreatingElement() {
if (MOZ_UNLIKELY(IsFormAssociatedElement())) {
MaybeRestoreFormAssociatedCustomElementState();
}
}
void HTMLElement::SaveState() {
if (MOZ_LIKELY(!IsFormAssociatedElement())) {
return;
}
auto* internals = GetElementInternals();
nsCString stateKey = internals->GetStateKey();
if (stateKey.IsEmpty()) {
return;
}
nsCOMPtr<nsILayoutHistoryState> history = GetLayoutHistory(false);
if (!history) {
return;
}
// Get the pres state for this key, if it doesn't exist, create one.
PresState* result = history->GetState(stateKey);
if (!result) {
UniquePtr<PresState> newState = NewPresState();
result = newState.get();
history->AddState(stateKey, std::move(newState));
}
const auto& state = internals->GetFormState();
const auto& value = internals->GetFormSubmissionValue();
result->contentData() = CustomElementTuple(
value.IsNull()
? CustomElementFormValue(void_t{})
: nsContentUtils::ConvertToCustomElementFormValue(value.Value()),
state.IsNull()
? CustomElementFormValue(void_t{})
: nsContentUtils::ConvertToCustomElementFormValue(state.Value()));
}
void HTMLElement::MaybeRestoreFormAssociatedCustomElementState() {
MOZ_ASSERT(IsFormAssociatedElement());
auto* internals = GetElementInternals();
if (internals->GetStateKey().IsEmpty()) {
Document* doc = GetUncomposedDoc();
nsCString stateKey;
nsContentUtils::GenerateStateKey(this, doc, stateKey);
internals->SetStateKey(std::move(stateKey));
RestoreFormAssociatedCustomElementState();
}
}
void HTMLElement::RestoreFormAssociatedCustomElementState() {
MOZ_ASSERT(IsFormAssociatedElement());
auto* internals = GetElementInternals();
const nsCString& stateKey = internals->GetStateKey();
if (stateKey.IsEmpty()) {
return;
}
nsCOMPtr<nsILayoutHistoryState> history = GetLayoutHistory(true);
if (!history) {
return;
}
PresState* result = history->GetState(stateKey);
if (!result) {
return;
}
auto& content = result->contentData();
if (content.type() != PresContentData::TCustomElementTuple) {
return;
}
auto& ce = content.get_CustomElementTuple();
internals->RestoreFormValue(
nsContentUtils::ExtractFormAssociatedCustomElementValue(this, ce.value()),
nsContentUtils::ExtractFormAssociatedCustomElementValue(this,
ce.state()));
}
void HTMLElement::SetCustomElementDefinition(
CustomElementDefinition* aDefinition) {
nsGenericHTMLFormElement::SetCustomElementDefinition(aDefinition);
@ -84,13 +178,15 @@ void HTMLElement::SetCustomElementDefinition(
aDefinition->mFormAssociated) {
CustomElementData* data = GetCustomElementData();
MOZ_ASSERT(data);
data->GetOrCreateElementInternals(this);
auto* internals = data->GetOrCreateElementInternals(this);
// This is for the case that script constructs a custom element directly,
// e.g. via new MyCustomElement(), where the upgrade steps won't be ran to
// update the disabled state in UpdateFormOwner().
if (data->mState == CustomElementData::State::eCustom) {
UpdateDisabledState(true);
} else if (HasFlag(HTML_ELEMENT_FROM_PARSER_NETWORK)) {
internals->InitializeControlNumber();
}
}
}
@ -198,6 +294,8 @@ void HTMLElement::UpdateFormOwner() {
UpdateFieldSet(true);
UpdateDisabledState(true);
UpdateBarredFromConstraintValidation();
MaybeRestoreFormAssociatedCustomElementState();
}
bool HTMLElement::IsDisabledForEvents(WidgetEvent* aEvent) {
@ -338,7 +436,7 @@ nsGenericHTMLElement* NS_NewHTMLElement(
mozilla::dom::FromParser aFromParser) {
RefPtr<mozilla::dom::NodeInfo> nodeInfo(aNodeInfo);
auto* nim = nodeInfo->NodeInfoManager();
return new (nim) mozilla::dom::HTMLElement(nodeInfo.forget());
return new (nim) mozilla::dom::HTMLElement(nodeInfo.forget(), aFromParser);
}
// Distinct from the above in order to have function pointer that compared
@ -348,5 +446,5 @@ nsGenericHTMLElement* NS_NewCustomElement(
mozilla::dom::FromParser aFromParser) {
RefPtr<mozilla::dom::NodeInfo> nodeInfo(aNodeInfo);
auto* nim = nodeInfo->NodeInfoManager();
return new (nim) mozilla::dom::HTMLElement(nodeInfo.forget());
return new (nim) mozilla::dom::HTMLElement(nodeInfo.forget(), aFromParser);
}

View File

@ -13,7 +13,8 @@ namespace mozilla::dom {
class HTMLElement final : public nsGenericHTMLFormElement {
public:
explicit HTMLElement(already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo);
explicit HTMLElement(already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo,
FromParser aFromParser = NOT_FROM_PARSER);
// nsISupports
NS_DECL_ISUPPORTS_INHERITED
@ -30,6 +31,7 @@ class HTMLElement final : public nsGenericHTMLFormElement {
// nsIContent
nsresult BindToTree(BindContext&, nsINode& aParent) override;
void UnbindFromTree(bool aNullParent = true) override;
void DoneCreatingElement() override;
// Element
void SetCustomElementDefinition(
@ -46,9 +48,12 @@ class HTMLElement final : public nsGenericHTMLFormElement {
bool IsFormAssociatedElement() const override;
void AfterClearForm(bool aUnbindOrDelete) override;
void FieldSetDisabledChanged(bool aNotify) override;
void SaveState() override;
void UpdateFormOwner();
void MaybeRestoreFormAssociatedCustomElementState();
protected:
virtual ~HTMLElement() = default;
@ -75,6 +80,9 @@ class HTMLElement final : public nsGenericHTMLFormElement {
void UpdateBarredFromConstraintValidation();
ElementInternals* GetElementInternals() const;
MOZ_CAN_RUN_SCRIPT_BOUNDARY
void RestoreFormAssociatedCustomElementState();
};
} // namespace mozilla::dom

View File

@ -1833,6 +1833,9 @@ nsresult nsGenericHTMLFormElement::BindToTree(BindContext& aContext,
}
void nsGenericHTMLFormElement::UnbindFromTree(bool aNullParent) {
// Save state before doing anything else.
SaveState();
if (IsFormAssociatedElement()) {
if (HTMLFormElement* form = GetFormInternal()) {
// Might need to unset form
@ -2240,6 +2243,12 @@ void nsGenericHTMLFormElement::FieldSetDisabledChanged(bool aNotify) {
UpdateDisabledState(aNotify);
}
void nsGenericHTMLFormElement::SaveSubtreeState() {
SaveState();
nsGenericHTMLElement::SaveSubtreeState();
}
//----------------------------------------------------------------------
void nsGenericHTMLElement::Click(CallerType aCallerType) {
@ -2573,12 +2582,6 @@ nsINode* nsGenericHTMLFormControlElement::GetScopeChainParent() const {
return mForm ? mForm : nsGenericHTMLElement::GetScopeChainParent();
}
void nsGenericHTMLFormControlElement::SaveSubtreeState() {
SaveState();
nsGenericHTMLFormElement::SaveSubtreeState();
}
nsIContent::IMEState nsGenericHTMLFormControlElement::GetDesiredIMEState() {
TextEditor* textEditor = GetTextEditorInternal();
if (!textEditor) {
@ -2592,12 +2595,6 @@ nsIContent::IMEState nsGenericHTMLFormControlElement::GetDesiredIMEState() {
return state;
}
void nsGenericHTMLFormControlElement::UnbindFromTree(bool aNullParent) {
// Save state before doing anything
SaveState();
nsGenericHTMLFormElement::UnbindFromTree(aNullParent);
}
void nsGenericHTMLFormControlElement::GetAutocapitalize(
nsAString& aValue) const {
if (nsContentUtils::HasNonEmptyAttr(this, kNameSpaceID_None,
@ -2962,7 +2959,7 @@ PresState* nsGenericHTMLFormControlElementWithState::GetPrimaryPresState() {
}
already_AddRefed<nsILayoutHistoryState>
nsGenericHTMLFormControlElementWithState::GetLayoutHistory(bool aRead) {
nsGenericHTMLFormElement::GetLayoutHistory(bool aRead) {
nsCOMPtr<Document> doc = GetUncomposedDoc();
if (!doc) {
return nullptr;

View File

@ -980,10 +980,11 @@ class HTMLFieldSetElement;
enum {
// Used to handle keyboard activation.
HTML_ELEMENT_ACTIVE_FOR_KEYBOARD = HTML_ELEMENT_FLAG_BIT(0),
HTML_ELEMENT_FROM_PARSER_NETWORK = HTML_ELEMENT_FLAG_BIT(1),
// Remaining bits are type specific.
HTML_ELEMENT_TYPE_SPECIFIC_BITS_OFFSET =
ELEMENT_TYPE_SPECIFIC_BITS_OFFSET + 1,
ELEMENT_TYPE_SPECIFIC_BITS_OFFSET + 2,
};
ASSERT_NODE_FLAGS_SPACE(HTML_ELEMENT_TYPE_SPECIFIC_BITS_OFFSET);
@ -1027,6 +1028,7 @@ class nsGenericHTMLFormElement : public nsGenericHTMLElement {
already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo);
// nsIContent
void SaveSubtreeState() override;
nsresult BindToTree(BindContext&, nsINode& aParent) override;
void UnbindFromTree(bool aNullParent = true) override;
@ -1054,6 +1056,15 @@ class nsGenericHTMLFormElement : public nsGenericHTMLElement {
void ClearForm(bool aRemoveFromForm, bool aUnbindOrDelete);
/**
* Get the layout history object for a particular piece of content.
*
* @param aRead if true, won't return a layout history state if the
* layout history state is empty.
* @return the history state object
*/
already_AddRefed<nsILayoutHistoryState> GetLayoutHistory(bool aRead);
protected:
virtual ~nsGenericHTMLFormElement() = default;
@ -1147,6 +1158,13 @@ class nsGenericHTMLFormElement : public nsGenericHTMLElement {
* See https://html.spec.whatwg.org/#form-associated-element.
*/
virtual bool IsFormAssociatedElement() const { return false; }
/**
* Save to presentation state. The form element will determine whether it
* has anything to save and if so, create an entry in the layout history for
* its pres context.
*/
virtual void SaveState() {}
};
class nsGenericHTMLFormControlElement : public nsGenericHTMLFormElement,
@ -1165,9 +1183,7 @@ class nsGenericHTMLFormControlElement : public nsGenericHTMLFormElement,
bool IsHTMLFormControlElement() const final { return true; }
// nsIContent
void SaveSubtreeState() override;
IMEState GetDesiredIMEState() override;
void UnbindFromTree(bool aNullParent = true) override;
// nsGenericHTMLElement
// autocapitalize attribute support
@ -1211,13 +1227,6 @@ class nsGenericHTMLFormControlElement : public nsGenericHTMLFormElement,
bool IsAutocapitalizeInheriting() const;
/**
* Save to presentation state. The form control will determine whether it
* has anything to save and if so, create an entry in the layout history for
* its pres context.
*/
virtual void SaveState() {}
nsresult SubmitDirnameDir(mozilla::dom::FormData* aFormData);
/** The form that contains this control */
@ -1271,15 +1280,6 @@ class nsGenericHTMLFormControlElementWithState
*/
mozilla::PresState* GetPrimaryPresState();
/**
* Get the layout history object for a particular piece of content.
*
* @param aRead if true, won't return a layout history state if the
* layout history state is empty.
* @return the history state object
*/
already_AddRefed<nsILayoutHistoryState> GetLayoutHistory(bool aRead);
/**
* Called when we have been cloned and adopted, and the information of the
* node has been changed.

View File

@ -101,6 +101,8 @@ skip-if = os == "android"
[test_radio_radionodelist.html]
[test_required_attribute.html]
[test_restore_form_elements.html]
[test_save_restore_custom_elements.html]
support-files = save_restore_custom_elements_sample.html
[test_save_restore_radio_groups.html]
[test_select_change_event.html]
skip-if = os == 'mac'

View File

@ -0,0 +1,43 @@
<script>
class CEBase extends HTMLElement {
static formAssociated = true;
constructor() {
super();
this.internals = this.attachInternals();
this.state_ = undefined;
}
formStateRestoreCallback(state, reason) {
if (reason == "restore") {
this.state_ = state;
}
}
set(state, value) {
this.state_ = state;
this.value_ = value;
this.internals.setFormValue(value, state);
}
get state() {
return this.state_;
}
get value() {
return this.value_;
}
}
customElements.define("c-e", class extends CEBase {});
</script>
<form>
<c-e id="custom0"></c-e>
<c-e id="custom1"></c-e>
<c-e id="custom2"></c-e>
<c-e id="custom3"></c-e>
<c-e id="custom4"></c-e>
<upgraded-ce id="upgraded0"></upgraded-ce>
<upgraded-ce id="upgraded1"></upgraded-ce>
<upgraded-ce id="upgraded2"></upgraded-ce>
<upgraded-ce id="upgraded3"></upgraded-ce>
<upgraded-ce id="upgraded4"></upgraded-ce>
</form>
<script>
customElements.define("upgraded-ce", class extends CEBase {});
</script>

View File

@ -0,0 +1,90 @@
<!DOCTYPE HTML>
<html>
<!--
https://bugzilla.mozilla.org/show_bug.cgi?id=1556358
-->
<head>
<title>Test for Bug 1556358</title>
<script src="/tests/SimpleTest/SimpleTest.js"></script>
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
</head>
<body>
<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1556358">Mozilla Bug 1556358</a>
<p id="display"></p>
<div id="content">
<iframe src="save_restore_custom_elements_sample.html"></iframe>
</div>
<script type="application/javascript">
/** Test for Bug 1556358 **/
function formDataWith(...entries) {
const formData = new FormData();
for (let [key, value] of entries) {
formData.append(key, value);
}
return formData;
}
const states = [
"test state",
new File(["state"], "state.txt"),
formDataWith(["1", "state"], ["2", new Blob(["state_blob"])]),
null,
undefined,
];
const values = [
"test value",
new File(["value"], "value.txt"),
formDataWith(["1", "value"], ["2", new Blob(["value_blob"])]),
"null state",
"both value and state",
];
add_task(async () => {
const frame = document.querySelector("iframe");
const elementTags = ["c-e", "upgraded-ce"];
// Set the custom element values.
for (const tags of elementTags) {
[...frame.contentDocument.querySelectorAll(tags)]
.forEach((e, i) => {
e.set(states[i], values[i]);
});
}
await new Promise(resolve => {
frame.addEventListener("load", resolve);
frame.contentWindow.location.reload();
});
for (const tag of elementTags) {
// Retrieve the restored values.
const ceStates =
[...frame.contentDocument.querySelectorAll(tag)].map((e) => e.state);
is(ceStates.length, 5, "Should have 5 custom element states");
const [restored, original] = [ceStates, states];
is(restored[0], original[0], "Value should be restored");
const file = restored[1];
isnot(file, original[1], "Restored file object differs from original object.");
is(file.name, original[1].name, "File name should be restored");
is(await file.text(), await original[1].text(), "File text should be restored");
const formData = restored[2];
isnot(formData, original[2], "Restored formdata object differs from original object.");
is(formData.get("1"), original[2].get("1"), "Form data string should be restored");
is(await formData.get("2").text(), await original[2].get("2").text(), "Form data blob should be restored");
isnot(restored[3], original[3], "Null values don't get restored");
is(restored[3], undefined, "Null values don't get restored");
is(restored[4], "both value and state", "Undefined state should be set to value");
}
});
</script>
</body>
</html>

View File

@ -0,0 +1,39 @@
/* -*- Mode: C++; c-basic-offset: 2; indent-tabs-mode: nil; tab-width: 8 -*- */
/* vim: set sw=2 ts=8 et tw=80 ft=cpp : */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
include "mozilla/dom/IPCBlobUtils.h";
[RefCounted] using class mozilla::dom::BlobImpl from "mozilla/dom/BlobImpl.h";
using struct mozilla::void_t from "mozilla/ipc/IPCCore.h";
namespace mozilla {
namespace dom {
// Types used to store form-associated custom element state.
union FormDataValue {
BlobImpl;
nsString;
};
struct FormDataTuple {
nsString name;
FormDataValue value;
};
union CustomElementFormValue {
void_t;
nullable BlobImpl;
nsString;
FormDataTuple[];
};
struct CustomElementTuple {
CustomElementFormValue value;
CustomElementFormValue state;
};
} // namespace dom
} // namespace mozilla

View File

@ -167,6 +167,7 @@ PREPROCESSED_IPDL_SOURCES += [
]
IPDL_SOURCES += [
"CustomElementTypes.ipdlh",
"DOMTypes.ipdlh",
"IPCTransferable.ipdlh",
"MemoryReportTypes.ipdlh",

View File

@ -11,6 +11,8 @@ using struct mozilla::void_t from "mozilla/ipc/IPCCore.h";
using struct nsPoint from "nsPoint.h";
[RefCounted] using class mozilla::dom::BlobImpl from "mozilla/dom/BlobImpl.h";
include CustomElementTypes;
namespace mozilla {
struct SelectContentData {
@ -40,6 +42,7 @@ union PresContentData {
// We can need to serialize blobs in order to transmit this type, so we need
// to handle that in a custom handler.
FileContentData[];
CustomElementTuple;
};
struct PresState {