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 @@
#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/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/script_state.h"
#include "third_party/blink/renderer/platform/blob/blob_data.h"
......@@ -150,6 +151,31 @@ File* File::Create(
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,
const String& relative_path) {
File* file = MakeGarbageCollected<File>(path, File::kAllContentTypes,
......@@ -386,4 +412,14 @@ bool File::HasSameSource(const File& other) const {
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
......@@ -39,6 +39,7 @@ class ExceptionState;
class ExecutionContext;
class FilePropertyBag;
class FileMetadata;
class FormControlState;
class KURL;
class CORE_EXPORT File final : public Blob {
......@@ -102,6 +103,13 @@ class CORE_EXPORT File final : public Blob {
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,
const String& relative_path);
......@@ -220,6 +228,9 @@ class CORE_EXPORT File final : public Blob {
// of the file objects are same or not.
bool HasSameSource(const File& other) const;
// Return false if this File instance is not serializable to FormControlState.
bool AppendToControlState(FormControlState& state);
private:
void InvalidateSnapshotMetadata() { snapshot_size_ = -1; }
......
......@@ -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_registry.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/html_form_element.h"
#include "third_party/blink/renderer/core/html/forms/validity_state.h"
......@@ -63,6 +64,7 @@ void ElementInternals::setFormValue(const FileOrUSVString& value,
}
value_ = value;
entry_source_ = MakeGarbageCollected<FormData>(*entry_source);
NotifyFormStateChanged();
}
HTMLFormElement* ElementInternals::form(ExceptionState& exception_state) const {
......@@ -186,6 +188,8 @@ void ElementInternals::DidUpgrade() {
lists->InvalidateCaches(nullptr);
}
}
Target().GetDocument().GetFormController().RestoreControlStateOnUpgrade(
*this);
}
bool ElementInternals::IsTargetFormAssociated() const {
......@@ -292,4 +296,41 @@ void ElementInternals::DisabledStateMightBeChanged() {
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
......@@ -67,6 +67,10 @@ class ElementInternals : public ScriptWrappable, public ListedElement {
String validationMessage() const override;
String ValidationSubMessage() const 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_;
......
......@@ -87,25 +87,20 @@ InputTypeView* FileInputType::CreateView() {
template <typename ItemType, typename VectorType>
VectorType CreateFilesFrom(const FormControlState& state,
ItemType (*factory)(const String&,
const String&,
const String&)) {
ItemType (*factory)(const FormControlState&,
wtf_size_t&)) {
VectorType files;
files.ReserveInitialCapacity(state.ValueSize() / 3);
for (wtf_size_t i = 0; i < state.ValueSize(); i += 3) {
const String& path = state[i];
const String& name = state[i + 1];
const String& relative_path = state[i + 2];
files.push_back(factory(path, name, relative_path));
for (wtf_size_t i = 0; i < state.ValueSize();) {
files.push_back(factory(state, i));
}
return files;
}
Vector<String> FileInputType::FilesFromFormControlState(
const FormControlState& state) {
return CreateFilesFrom<String, Vector<String>>(
state,
[](const String& path, const String&, const String&) { return path; });
return CreateFilesFrom<String, Vector<String>>(state,
&File::PathFromControlState);
}
const AtomicString& FileInputType::FormControlType() const {
......@@ -117,14 +112,8 @@ FormControlState FileInputType::SaveFormControlState() const {
return FormControlState();
FormControlState state;
unsigned num_files = file_list_->length();
for (unsigned i = 0; i < num_files; ++i) {
if (file_list_->item(i)->HasBackingFile()) {
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
}
for (unsigned i = 0; i < num_files; ++i)
file_list_->item(i)->AppendToControlState(state);
return state;
}
......@@ -133,12 +122,7 @@ void FileInputType::RestoreFormControlState(const FormControlState& state) {
return;
HeapVector<Member<File>> file_vector =
CreateFilesFrom<File*, HeapVector<Member<File>>>(
state, [](const String& path, const String& name,
const String& relative_path) {
if (relative_path.IsEmpty())
return File::CreateForUserProvidedFile(path, name);
return File::CreateWithRelativePath(path, relative_path);
});
state, &File::CreateFromControlState);
FileList* file_list = FileList::Create();
for (const auto& file : file_vector)
file_list->Append(file);
......
......@@ -28,6 +28,8 @@
#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/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/html_form_element.h"
#include "third_party/blink/renderer/core/html/forms/html_input_element.h"
......@@ -51,7 +53,9 @@ inline HTMLFormElement* OwnerFormForState(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
......@@ -245,7 +249,8 @@ std::unique_ptr<SavedFormState> SavedFormState::Deserialize(
String type = state_vector[index++];
FormControlState state = FormControlState::Deserialize(state_vector, index);
if (type.IsEmpty() ||
type.Find(IsNotFormControlTypeCharacter) != kNotFound ||
(type.Find(IsNotFormControlTypeCharacter) != kNotFound &&
!CustomElement::IsValidName(AtomicString(type))) ||
state.IsFailure())
return nullptr;
saved_form_state->AppendControlState(AtomicString(name), AtomicString(type),
......@@ -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(
const Vector<String>& state_vector) {
Vector<String> to_return;
......
......@@ -111,6 +111,8 @@ class CORE_EXPORT FormController final
void WillDeleteForm(HTMLFormElement*);
void RestoreControlStateFor(ListedElement&);
void RestoreControlStateIn(HTMLFormElement&);
// For a upgraded form-associated custom element.
void RestoreControlStateOnUpgrade(ListedElement&);
static Vector<String> GetReferencedFilePaths(
const Vector<String>& state_vector);
......
......@@ -1531,6 +1531,12 @@ bool HTMLElement::IsLabelable() const {
return IsFormAssociatedCustomElement();
}
void HTMLElement::FinishParsingChildren() {
Element::FinishParsingChildren();
if (IsFormAssociatedCustomElement())
EnsureElementInternals().TakeStateAndRestore();
}
} // namespace blink
#ifndef NDEBUG
......
......@@ -179,6 +179,7 @@ class CORE_EXPORT HTMLElement : public Element {
InsertionNotificationRequest InsertedInto(ContainerNode&) override;
void RemovedFrom(ContainerNode& insertion_point) override;
void DidMoveToNewDocument(Document& old_document) override;
void FinishParsingChildren() override;
private:
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