Commit e7ea801e authored by Kent Tamura's avatar Kent Tamura Committed by Commit Bot

Form-associated custom elements: Support the state restore feature

Restarting Chrome application, duplicate a tab, and back/forward
navigation call 'restoreValueCallback' of form-associated custom
elements to restore their state.

Implementation:
* element_internals.{h,cc}: Hook some methods to support the state
  restore feature.
  ElementInternals::RestoreFormControlState() posts a microtask to
  call 'restoreValueCallback'.

* form_controller.{h,cc}:
  - ControlType() supports ElementInternals. We store custom element
    name as a type.
  - SavedFormState::Deserialize(): Accept custom element names.
  - FormController::RestoreControlStateOnUpgrade(): Added for
    upgraded custom elements.

* html_element.{h,cc}: FinishParsingChildren()
  Hook it to restore the state of form-associated custom elements
  which are not owned by a <form>.

* file.{h,cc}, file_input_type.cc:
  Move logic to save/restore File state to blink::File in order to
  share it with ElementInternals.

Change-Id: Idb8ea5d333afbd566ae838334800cbf0b8128af5
Reviewed-on: https://chromium-review.googlesource.com/c/1436416Reviewed-by: default avatarHayato Ito <hayato@chromium.org>
Commit-Queue: Hayato Ito <hayato@chromium.org>
Commit-Queue: Kent Tamura <tkent@chromium.org>
Auto-Submit: Kent Tamura <tkent@chromium.org>
Cr-Commit-Position: refs/heads/master@{#626447}
parent 1bf80bb5
...@@ -31,6 +31,7 @@ ...@@ -31,6 +31,7 @@
#include "third_party/blink/public/platform/platform.h" #include "third_party/blink/public/platform/platform.h"
#include "third_party/blink/renderer/core/fileapi/file_property_bag.h" #include "third_party/blink/renderer/core/fileapi/file_property_bag.h"
#include "third_party/blink/renderer/core/frame/use_counter.h" #include "third_party/blink/renderer/core/frame/use_counter.h"
#include "third_party/blink/renderer/core/html/forms/form_controller.h"
#include "third_party/blink/renderer/platform/bindings/exception_state.h" #include "third_party/blink/renderer/platform/bindings/exception_state.h"
#include "third_party/blink/renderer/platform/bindings/script_state.h" #include "third_party/blink/renderer/platform/bindings/script_state.h"
#include "third_party/blink/renderer/platform/blob/blob_data.h" #include "third_party/blink/renderer/platform/blob/blob_data.h"
...@@ -150,6 +151,31 @@ File* File::Create( ...@@ -150,6 +151,31 @@ File* File::Create(
BlobDataHandle::Create(std::move(blob_data), file_size)); BlobDataHandle::Create(std::move(blob_data), file_size));
} }
File* File::CreateFromControlState(const FormControlState& state,
wtf_size_t& index) {
if (index + 2 >= state.ValueSize()) {
index = state.ValueSize();
return nullptr;
}
String path = state[index++];
String name = state[index++];
String relative_path = state[index++];
if (relative_path.IsEmpty())
return File::CreateForUserProvidedFile(path, name);
return File::CreateWithRelativePath(path, relative_path);
}
String File::PathFromControlState(const FormControlState& state,
wtf_size_t& index) {
if (index + 2 >= state.ValueSize()) {
index = state.ValueSize();
return String();
}
String path = state[index];
index += 3;
return path;
}
File* File::CreateWithRelativePath(const String& path, File* File::CreateWithRelativePath(const String& path,
const String& relative_path) { const String& relative_path) {
File* file = MakeGarbageCollected<File>(path, File::kAllContentTypes, File* file = MakeGarbageCollected<File>(path, File::kAllContentTypes,
...@@ -386,4 +412,14 @@ bool File::HasSameSource(const File& other) const { ...@@ -386,4 +412,14 @@ bool File::HasSameSource(const File& other) const {
return Uuid() == other.Uuid(); return Uuid() == other.Uuid();
} }
bool File::AppendToControlState(FormControlState& state) {
// FIXME: handle Blob-backed File instances, see http://crbug.com/394948
if (!HasBackingFile())
return false;
state.Append(GetPath());
state.Append(name());
state.Append(webkitRelativePath());
return true;
}
} // namespace blink } // namespace blink
...@@ -39,6 +39,7 @@ class ExceptionState; ...@@ -39,6 +39,7 @@ class ExceptionState;
class ExecutionContext; class ExecutionContext;
class FilePropertyBag; class FilePropertyBag;
class FileMetadata; class FileMetadata;
class FormControlState;
class KURL; class KURL;
class CORE_EXPORT File final : public Blob { class CORE_EXPORT File final : public Blob {
...@@ -102,6 +103,13 @@ class CORE_EXPORT File final : public Blob { ...@@ -102,6 +103,13 @@ class CORE_EXPORT File final : public Blob {
std::move(blob_data_handle)); std::move(blob_data_handle));
} }
// For session restore feature.
// See also AppendToControlState().
static File* CreateFromControlState(const FormControlState& state,
wtf_size_t& index);
static String PathFromControlState(const FormControlState& state,
wtf_size_t& index);
static File* CreateWithRelativePath(const String& path, static File* CreateWithRelativePath(const String& path,
const String& relative_path); const String& relative_path);
...@@ -220,6 +228,9 @@ class CORE_EXPORT File final : public Blob { ...@@ -220,6 +228,9 @@ class CORE_EXPORT File final : public Blob {
// of the file objects are same or not. // of the file objects are same or not.
bool HasSameSource(const File& other) const; bool HasSameSource(const File& other) const;
// Return false if this File instance is not serializable to FormControlState.
bool AppendToControlState(FormControlState& state);
private: private:
void InvalidateSnapshotMetadata() { snapshot_size_ = -1; } void InvalidateSnapshotMetadata() { snapshot_size_ = -1; }
......
...@@ -9,6 +9,7 @@ ...@@ -9,6 +9,7 @@
#include "third_party/blink/renderer/core/html/custom/custom_element.h" #include "third_party/blink/renderer/core/html/custom/custom_element.h"
#include "third_party/blink/renderer/core/html/custom/custom_element_registry.h" #include "third_party/blink/renderer/core/html/custom/custom_element_registry.h"
#include "third_party/blink/renderer/core/html/custom/validity_state_flags.h" #include "third_party/blink/renderer/core/html/custom/validity_state_flags.h"
#include "third_party/blink/renderer/core/html/forms/form_controller.h"
#include "third_party/blink/renderer/core/html/forms/form_data.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/forms/validity_state.h" #include "third_party/blink/renderer/core/html/forms/validity_state.h"
...@@ -63,6 +64,7 @@ void ElementInternals::setFormValue(const FileOrUSVString& value, ...@@ -63,6 +64,7 @@ void ElementInternals::setFormValue(const FileOrUSVString& value,
} }
value_ = value; value_ = value;
entry_source_ = MakeGarbageCollected<FormData>(*entry_source); entry_source_ = MakeGarbageCollected<FormData>(*entry_source);
NotifyFormStateChanged();
} }
HTMLFormElement* ElementInternals::form(ExceptionState& exception_state) const { HTMLFormElement* ElementInternals::form(ExceptionState& exception_state) const {
...@@ -186,6 +188,8 @@ void ElementInternals::DidUpgrade() { ...@@ -186,6 +188,8 @@ void ElementInternals::DidUpgrade() {
lists->InvalidateCaches(nullptr); lists->InvalidateCaches(nullptr);
} }
} }
Target().GetDocument().GetFormController().RestoreControlStateOnUpgrade(
*this);
} }
bool ElementInternals::IsTargetFormAssociated() const { bool ElementInternals::IsTargetFormAssociated() const {
...@@ -292,4 +296,41 @@ void ElementInternals::DisabledStateMightBeChanged() { ...@@ -292,4 +296,41 @@ void ElementInternals::DisabledStateMightBeChanged() {
CustomElement::EnqueueDisabledStateChangedCallback(Target(), new_disabled); CustomElement::EnqueueDisabledStateChangedCallback(Target(), new_disabled);
} }
bool ElementInternals::ClassSupportsStateRestore() const {
return true;
}
bool ElementInternals::ShouldSaveAndRestoreFormControlState() const {
// We don't save/restore control state in a form with autocomplete=off.
return Target().isConnected() && (!Form() || Form()->ShouldAutocomplete());
}
FormControlState ElementInternals::SaveFormControlState() const {
FormControlState state;
if (value_.IsUSVString()) {
state.Append("USVString");
state.Append(value_.GetAsUSVString());
} else if (value_.IsFile()) {
state.Append("File");
File* file = value_.GetAsFile();
file->AppendToControlState(state);
}
// Add nothing if value_.IsNull().
return state;
}
void ElementInternals::RestoreFormControlState(const FormControlState& state) {
if (state.ValueSize() < 2)
return;
if (state[0] == "USVString") {
value_ = FileOrUSVString::FromUSVString(state[1]);
} else if (state[0] == "File") {
wtf_size_t i = 1;
if (auto* file = File::CreateFromControlState(state, i))
value_ = FileOrUSVString::FromFile(file);
}
if (!value_.IsNull())
CustomElement::EnqueueRestoreValueCallback(Target(), value_);
}
} // namespace blink } // namespace blink
...@@ -67,6 +67,10 @@ class ElementInternals : public ScriptWrappable, public ListedElement { ...@@ -67,6 +67,10 @@ class ElementInternals : public ScriptWrappable, public ListedElement {
String validationMessage() const override; String validationMessage() const override;
String ValidationSubMessage() const override; String ValidationSubMessage() const override;
void DisabledStateMightBeChanged() override; void DisabledStateMightBeChanged() override;
bool ClassSupportsStateRestore() const override;
bool ShouldSaveAndRestoreFormControlState() const override;
FormControlState SaveFormControlState() const override;
void RestoreFormControlState(const FormControlState& state) override;
Member<HTMLElement> target_; Member<HTMLElement> target_;
......
...@@ -87,25 +87,20 @@ InputTypeView* FileInputType::CreateView() { ...@@ -87,25 +87,20 @@ InputTypeView* FileInputType::CreateView() {
template <typename ItemType, typename VectorType> template <typename ItemType, typename VectorType>
VectorType CreateFilesFrom(const FormControlState& state, VectorType CreateFilesFrom(const FormControlState& state,
ItemType (*factory)(const String&, ItemType (*factory)(const FormControlState&,
const String&, wtf_size_t&)) {
const String&)) {
VectorType files; VectorType files;
files.ReserveInitialCapacity(state.ValueSize() / 3); files.ReserveInitialCapacity(state.ValueSize() / 3);
for (wtf_size_t i = 0; i < state.ValueSize(); i += 3) { for (wtf_size_t i = 0; i < state.ValueSize();) {
const String& path = state[i]; files.push_back(factory(state, i));
const String& name = state[i + 1];
const String& relative_path = state[i + 2];
files.push_back(factory(path, name, relative_path));
} }
return files; return files;
} }
Vector<String> FileInputType::FilesFromFormControlState( Vector<String> FileInputType::FilesFromFormControlState(
const FormControlState& state) { const FormControlState& state) {
return CreateFilesFrom<String, Vector<String>>( return CreateFilesFrom<String, Vector<String>>(state,
state, &File::PathFromControlState);
[](const String& path, const String&, const String&) { return path; });
} }
const AtomicString& FileInputType::FormControlType() const { const AtomicString& FileInputType::FormControlType() const {
...@@ -117,14 +112,8 @@ FormControlState FileInputType::SaveFormControlState() const { ...@@ -117,14 +112,8 @@ FormControlState FileInputType::SaveFormControlState() const {
return FormControlState(); return FormControlState();
FormControlState state; FormControlState state;
unsigned num_files = file_list_->length(); unsigned num_files = file_list_->length();
for (unsigned i = 0; i < num_files; ++i) { for (unsigned i = 0; i < num_files; ++i)
if (file_list_->item(i)->HasBackingFile()) { file_list_->item(i)->AppendToControlState(state);
state.Append(file_list_->item(i)->GetPath());
state.Append(file_list_->item(i)->name());
state.Append(file_list_->item(i)->webkitRelativePath());
}
// FIXME: handle Blob-backed File instances, see http://crbug.com/394948
}
return state; return state;
} }
...@@ -133,12 +122,7 @@ void FileInputType::RestoreFormControlState(const FormControlState& state) { ...@@ -133,12 +122,7 @@ void FileInputType::RestoreFormControlState(const FormControlState& state) {
return; return;
HeapVector<Member<File>> file_vector = HeapVector<Member<File>> file_vector =
CreateFilesFrom<File*, HeapVector<Member<File>>>( CreateFilesFrom<File*, HeapVector<Member<File>>>(
state, [](const String& path, const String& name, state, &File::CreateFromControlState);
const String& relative_path) {
if (relative_path.IsEmpty())
return File::CreateForUserProvidedFile(path, name);
return File::CreateWithRelativePath(path, relative_path);
});
FileList* file_list = FileList::Create(); FileList* file_list = FileList::Create();
for (const auto& file : file_vector) for (const auto& file : file_vector)
file_list->Append(file); file_list->Append(file);
......
...@@ -28,6 +28,8 @@ ...@@ -28,6 +28,8 @@
#include "third_party/blink/renderer/core/dom/document.h" #include "third_party/blink/renderer/core/dom/document.h"
#include "third_party/blink/renderer/core/dom/element_traversal.h" #include "third_party/blink/renderer/core/dom/element_traversal.h"
#include "third_party/blink/renderer/core/dom/events/scoped_event_queue.h" #include "third_party/blink/renderer/core/dom/events/scoped_event_queue.h"
#include "third_party/blink/renderer/core/html/custom/custom_element.h"
#include "third_party/blink/renderer/core/html/custom/element_internals.h"
#include "third_party/blink/renderer/core/html/forms/file_chooser.h" #include "third_party/blink/renderer/core/html/forms/file_chooser.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/forms/html_input_element.h" #include "third_party/blink/renderer/core/html/forms/html_input_element.h"
...@@ -51,7 +53,9 @@ inline HTMLFormElement* OwnerFormForState(const ListedElement& control) { ...@@ -51,7 +53,9 @@ inline HTMLFormElement* OwnerFormForState(const ListedElement& control) {
} }
const AtomicString& ControlType(const ListedElement& control) { const AtomicString& ControlType(const ListedElement& control) {
return ToHTMLFormControlElement(control).type(); if (auto* control_element = ToHTMLFormControlElementOrNull(control))
return control_element->type();
return To<ElementInternals>(control).Target().localName();
} }
} // namespace } // namespace
...@@ -245,7 +249,8 @@ std::unique_ptr<SavedFormState> SavedFormState::Deserialize( ...@@ -245,7 +249,8 @@ std::unique_ptr<SavedFormState> SavedFormState::Deserialize(
String type = state_vector[index++]; String type = state_vector[index++];
FormControlState state = FormControlState::Deserialize(state_vector, index); FormControlState state = FormControlState::Deserialize(state_vector, index);
if (type.IsEmpty() || if (type.IsEmpty() ||
type.Find(IsNotFormControlTypeCharacter) != kNotFound || (type.Find(IsNotFormControlTypeCharacter) != kNotFound &&
!CustomElement::IsValidName(AtomicString(type))) ||
state.IsFailure()) state.IsFailure())
return nullptr; return nullptr;
saved_form_state->AppendControlState(AtomicString(name), AtomicString(type), saved_form_state->AppendControlState(AtomicString(name), AtomicString(type),
...@@ -575,6 +580,15 @@ void FormController::RestoreControlStateIn(HTMLFormElement& form) { ...@@ -575,6 +580,15 @@ void FormController::RestoreControlStateIn(HTMLFormElement& form) {
} }
} }
void FormController::RestoreControlStateOnUpgrade(ListedElement& control) {
DCHECK(control.ClassSupportsStateRestore());
if (!control.ShouldSaveAndRestoreFormControlState())
return;
FormControlState state = TakeStateForFormElement(control);
if (state.ValueSize() > 0)
control.RestoreFormControlState(state);
}
Vector<String> FormController::GetReferencedFilePaths( Vector<String> FormController::GetReferencedFilePaths(
const Vector<String>& state_vector) { const Vector<String>& state_vector) {
Vector<String> to_return; Vector<String> to_return;
......
...@@ -111,6 +111,8 @@ class CORE_EXPORT FormController final ...@@ -111,6 +111,8 @@ class CORE_EXPORT FormController final
void WillDeleteForm(HTMLFormElement*); void WillDeleteForm(HTMLFormElement*);
void RestoreControlStateFor(ListedElement&); void RestoreControlStateFor(ListedElement&);
void RestoreControlStateIn(HTMLFormElement&); void RestoreControlStateIn(HTMLFormElement&);
// For a upgraded form-associated custom element.
void RestoreControlStateOnUpgrade(ListedElement&);
static Vector<String> GetReferencedFilePaths( static Vector<String> GetReferencedFilePaths(
const Vector<String>& state_vector); const Vector<String>& state_vector);
......
...@@ -1531,6 +1531,12 @@ bool HTMLElement::IsLabelable() const { ...@@ -1531,6 +1531,12 @@ bool HTMLElement::IsLabelable() const {
return IsFormAssociatedCustomElement(); return IsFormAssociatedCustomElement();
} }
void HTMLElement::FinishParsingChildren() {
Element::FinishParsingChildren();
if (IsFormAssociatedCustomElement())
EnsureElementInternals().TakeStateAndRestore();
}
} // namespace blink } // namespace blink
#ifndef NDEBUG #ifndef NDEBUG
......
...@@ -179,6 +179,7 @@ class CORE_EXPORT HTMLElement : public Element { ...@@ -179,6 +179,7 @@ class CORE_EXPORT HTMLElement : public Element {
InsertionNotificationRequest InsertedInto(ContainerNode&) override; InsertionNotificationRequest InsertedInto(ContainerNode&) override;
void RemovedFrom(ContainerNode& insertion_point) override; void RemovedFrom(ContainerNode& insertion_point) override;
void DidMoveToNewDocument(Document& old_document) override; void DidMoveToNewDocument(Document& old_document) override;
void FinishParsingChildren() override;
private: private:
String DebugNodeName() const final; String DebugNodeName() const final;
......
<!DOCTYPE html>
<body>
<script src="../resources/testharness.js"></script>
<script src="../resources/testharnessreport.js"></script>
<iframe src="resources/form-associated-state-restore-frame.html"></iframe>
<script>
let t = async_test('Form-associated custom elements can restore their values on back-forward navigation');
function doneTest() {
t.step(() => {
let d = document.querySelector('iframe').contentDocument;
assert_equals(d.querySelector('my-control-1').value, 'edit1');
assert_equals(d.querySelector('my-control-2').value, 'edit2');
t.done();
});
}
</script>
</body>
<!DOCTYPE html>
<body>
<script>
class MyControl1 extends HTMLElement {
static get formAssociated() { return true; }
constructor() {
super();
this.internals_ = this.attachInternals();
this.value_ = 'initial';
}
get value() {
return this.value_;
}
set value(v) {
this.value_ = v;
this.internals_.setFormValue(v);
}
restoreValueCallback(v) {
this.value = v;
}
}
customElements.define('my-control-1', MyControl1);
</script>
<input id=emptyOnFirstVisit name="state">
<form action="../../resources/back.html" id="form1">
<my-control-1></my-control-1>
<my-control-2></my-control-2>
</form>
<script>
let $ = document.querySelector.bind(document);
function upgradeMyControl2() {
class MyControl2 extends HTMLElement {
static get formAssociated() { return true; }
constructor() {
super();
this.internals_ = this.attachInternals();
this.value_ = 'initial';
}
get value() {
return this.value_;
}
set value(v) {
this.value_ = v;
this.internals_.setFormValue(v);
}
restoreValueCallback(v) {
this.value = v;
}
}
customElements.define('my-control-2', MyControl2);
customElements.upgrade($('my-control-2'));
}
function runTest() {
let state = $('#emptyOnFirstVisit');
if (!state.value) {
// First visit
state.value = 'visited';
upgradeMyControl2();
$('my-control-1').value = 'edit1';
$('my-control-2').value = 'edit2';
setTimeout(() => { $('form').submit(); }, 100);
} else {
// Second visit
upgradeMyControl2();
parent.doneTest();
}
}
runTest();
</script>
</body>
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