Bug 1258489 - Implement HTMLInputElement.webkitdirectory, r=smaug

This commit is contained in:
Andrea Marchesini 2016-06-01 12:29:00 +02:00
parent b965617dd1
commit 3ad65f3a92
10 changed files with 395 additions and 45 deletions

View File

@ -1309,6 +1309,7 @@ GK_ATOM(visuallyselected, "visuallyselected")
GK_ATOM(vlink, "vlink")
GK_ATOM(vspace, "vspace")
GK_ATOM(wbr, "wbr")
GK_ATOM(webkitdirectory, "webkitdirectory")
GK_ATOM(when, "when")
GK_ATOM(where, "where")
GK_ATOM(widget, "widget")

View File

@ -165,6 +165,21 @@ Directory::GetRoot(FileSystemBase* aFileSystem, ErrorResult& aRv)
return task->GetPromise();
}
/* static */ already_AddRefed<Directory>
Directory::Constructor(const GlobalObject& aGlobal,
const nsAString& aRealPath,
ErrorResult& aRv)
{
nsCOMPtr<nsIFile> path;
aRv = NS_NewNativeLocalFile(NS_ConvertUTF16toUTF8(aRealPath),
true, getter_AddRefs(path));
if (NS_WARN_IF(aRv.Failed())) {
return nullptr;
}
return Create(aGlobal.GetAsSupports(), path);
}
/* static */ already_AddRefed<Directory>
Directory::Create(nsISupports* aParent, nsIFile* aFile,
FileSystemBase* aFileSystem)

View File

@ -62,6 +62,11 @@ public:
static already_AddRefed<Promise>
GetRoot(FileSystemBase* aFileSystem, ErrorResult& aRv);
static already_AddRefed<Directory>
Constructor(const GlobalObject& aGlobal,
const nsAString& aRealPath,
ErrorResult& aRv);
static already_AddRefed<Directory>
Create(nsISupports* aParent, nsIFile* aDirectory,
FileSystemBase* aFileSystem = 0);

View File

@ -5,4 +5,5 @@ support-files =
worker_basic.js
[test_basic.html]
[test_webkitdirectory.html]
[test_worker_basic.html]

View File

@ -0,0 +1,122 @@
<!DOCTYPE HTML>
<html>
<head>
<title>Test for webkitdirectory and webkitRelativePath</title>
<script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
</head>
<body>
<input id="inputFileWebkitDirectory" type="file" webkitdirectory></input>
<input id="inputFileWebkitDirectoryAndDirectory" type="file" webkitdirectory directory></input>
<input id="inputFileDirectory" type="file" directory></input>
<script type="application/javascript;version=1.7">
function populateInputFile(aInputFile) {
var url = SimpleTest.getTestFileURL("script_fileList.js");
var script = SpecialPowers.loadChromeScript(url);
var MockFilePicker = SpecialPowers.MockFilePicker;
MockFilePicker.init(window, "A Mock File Picker", SpecialPowers.Ci.nsIFilePicker.modeOpen);
function onOpened(message) {
MockFilePicker.useDirectory(message.dir);
var input = document.getElementById(aInputFile);
input.addEventListener('change', function() {
MockFilePicker.cleanup();
script.destroy();
next();
});
input.click();
}
script.addMessageListener("dir.opened", onOpened);
script.sendAsyncMessage("dir.open", { path: 'test' });
}
function checkFile(file, fileList) {
for (var i = 0; i < fileList.length; ++i) {
ok(fileList[i] instanceof File, "We want just files.");
if (fileList[i].name == file.name) {
is(fileList[i].webkitRelativePath, file.path, "Path matches");
return;
}
}
ok(false, "File not found.");
}
function test_fileList(aInputFile, aWhat) {
var input = document.getElementById(aInputFile);
var fileList = input.files;
if (aWhat == null) {
is(fileList, null, "We want a null fileList for " + aInputFile);
next();
return;
}
is(fileList.length, aWhat.length, "We want just " + aWhat.length + " elements for " + aInputFile);
for (var i = 0; i < aWhat.length; ++i) {
checkFile(aWhat[i], fileList);
}
next();
}
function test_webkitdirectory_attribute() {
var a = document.createElement("input");
a.setAttribute("type", "file");
ok("webkitdirectory" in a, "HTMLInputElement.webkitdirectory exists");
ok(!a.hasAttribute("webkitdirectory"), "No webkitdirectory DOM attribute by default");
ok(!a.webkitdirectory, "No webkitdirectory attribute by default");
a.webkitdirectory = true;
ok(a.hasAttribute("webkitdirectory"), "Webkitdirectory DOM attribute is set");
ok(a.webkitdirectory, "Webkitdirectory attribute is set");
next();
}
function test_setup() {
SpecialPowers.pushPrefEnv({"set": [["dom.input.dirpicker", true],
["dom.webkitBlink.dirPicker.enabled", true]]}, next);
}
var tests = [
test_setup,
function() { populateInputFile('inputFileWebkitDirectory'); },
function() { populateInputFile('inputFileWebkitDirectoryAndDirectory'); },
function() { populateInputFile('inputFileDirectory'); },
function() { test_fileList('inputFileWebkitDirectory', [ { name: 'foo.txt', path: '/foo.txt' },
{ name: 'bar.txt', path: '/subdir/bar.txt' }]); },
function() { test_fileList('inputFileWebkitDirectoryAndDirectory', [ { name: 'foo.txt', path: '/foo.txt' },
{ name: 'bar.txt', path: '/subdir/bar.txt' }]); },
function() { test_fileList('inputFileDirectory', null); },
test_webkitdirectory_attribute,
];
function next() {
if (!tests.length) {
SimpleTest.finish();
return;
}
var test = tests.shift();
test();
}
SimpleTest.waitForExplicitFinish();
next();
</script>
</body>
</html>

View File

@ -218,6 +218,18 @@ const Decimal HTMLInputElement::kStepAny = Decimal(0);
#define PROGRESS_STR "progress"
static const uint32_t kProgressEventInterval = 50; // ms
class GetFilesCallback
{
public:
NS_INLINE_DECL_REFCOUNTING(GetFilesCallback);
virtual void
Callback(nsresult aStatus, const Sequence<RefPtr<File>>& aFiles) = 0;
protected:
virtual ~GetFilesCallback() {}
};
// Retrieving the list of files can be very time/IO consuming. We use this
// helper class to do it just once.
class GetFilesHelper final : public Runnable
@ -295,6 +307,21 @@ public:
ResolveOrRejectPromise(aPromise);
}
void
AddCallback(GetFilesCallback* aCallback)
{
MOZ_ASSERT(aCallback);
// Still working.
if (!mListingCompleted) {
mCallbacks.AppendElement(aCallback);
return;
}
MOZ_ASSERT(mCallbacks.IsEmpty());
RunCallback(aCallback);
}
// CC methods
void Unlink()
{
@ -353,6 +380,14 @@ private:
ResolveOrRejectPromise(promises[i]);
}
// Let's process the pending callbacks.
nsTArray<RefPtr<GetFilesCallback>> callbacks;
callbacks.SwapElements(mCallbacks);
for (uint32_t i = 0; i < callbacks.Length(); ++i) {
RunCallback(callbacks[i]);
}
return NS_OK;
}
@ -510,6 +545,16 @@ private:
aPromise->MaybeResolve(mFiles);
}
void
RunCallback(GetFilesCallback* aCallback)
{
MOZ_ASSERT(NS_IsMainThread());
MOZ_ASSERT(mListingCompleted);
MOZ_ASSERT(aCallback);
aCallback->Callback(mErrorResult, mFiles);
}
nsCOMPtr<nsIGlobalObject> mGlobal;
bool mRecursiveFlag;
@ -531,6 +576,78 @@ private:
nsresult mErrorResult;
nsTArray<RefPtr<Promise>> mPromises;
nsTArray<RefPtr<GetFilesCallback>> mCallbacks;
};
// An helper class for the dispatching of the 'change' event.
class DispatchChangeEventCallback final : public GetFilesCallback
{
public:
explicit DispatchChangeEventCallback(HTMLInputElement* aInputElement)
: mInputElement(aInputElement)
{
MOZ_ASSERT(aInputElement);
}
virtual void
Callback(nsresult aStatus, const Sequence<RefPtr<File>>& aFiles) override
{
nsTArray<OwningFileOrDirectory> array;
for (uint32_t i = 0; i < aFiles.Length(); ++i) {
OwningFileOrDirectory* element = array.AppendElement();
element->SetAsFile() = aFiles[i];
}
mInputElement->SetFilesOrDirectories(array, true);
NS_WARN_IF(NS_FAILED(DispatchEvents()));
}
nsresult
DispatchEvents()
{
nsresult rv = NS_OK;
rv = nsContentUtils::DispatchTrustedEvent(mInputElement->OwnerDoc(),
static_cast<nsIDOMHTMLInputElement*>(mInputElement.get()),
NS_LITERAL_STRING("input"), true,
false);
NS_WARN_IF(NS_FAILED(rv));
rv = nsContentUtils::DispatchTrustedEvent(mInputElement->OwnerDoc(),
static_cast<nsIDOMHTMLInputElement*>(mInputElement.get()),
NS_LITERAL_STRING("change"), true,
false);
return rv;
}
private:
RefPtr<HTMLInputElement> mInputElement;
};
// This callback is used for postponing the calling of SetFilesOrDirectories
// when the exploration of the directory is completed.
class AfterSetFilesOrDirectoriesCallback : public GetFilesCallback
{
public:
AfterSetFilesOrDirectoriesCallback(HTMLInputElement* aInputElement,
bool aSetValueChanged)
: mInputElement(aInputElement)
, mSetValueChanged(aSetValueChanged)
{
MOZ_ASSERT(aInputElement);
}
void
Callback(nsresult aStatus, const Sequence<RefPtr<File>>& aFiles) override
{
if (NS_SUCCEEDED(aStatus)) {
mInputElement->AfterSetFilesOrDirectoriesInternal(mSetValueChanged);
}
}
private:
RefPtr<HTMLInputElement> mInputElement;
bool mSetValueChanged;
};
class HTMLInputElementState final : public nsISupports
@ -865,19 +982,22 @@ HTMLInputElement::nsFilePickerShownCallback::Done(int16_t aResult)
// So, we can safely send one by ourself.
mInput->SetFilesOrDirectories(newFilesOrDirectories, true);
nsresult rv = NS_OK;
rv = nsContentUtils::DispatchTrustedEvent(mInput->OwnerDoc(),
static_cast<nsIDOMHTMLInputElement*>(mInput.get()),
NS_LITERAL_STRING("input"), true,
false);
NS_WARN_IF(NS_FAILED(rv));
RefPtr<DispatchChangeEventCallback> dispatchChangeEventCallback =
new DispatchChangeEventCallback(mInput);
rv = nsContentUtils::DispatchTrustedEvent(mInput->OwnerDoc(),
static_cast<nsIDOMHTMLInputElement*>(mInput.get()),
NS_LITERAL_STRING("change"), true,
false);
if (Preferences::GetBool("dom.webkitBlink.dirPicker.enabled", false) &&
mInput->HasAttr(kNameSpaceID_None, nsGkAtoms::webkitdirectory)) {
ErrorResult error;
GetFilesHelper* helper = mInput->GetOrCreateGetFilesHelper(true, error);
if (NS_WARN_IF(error.Failed())) {
return error.StealNSResult();
}
return rv;
helper->AddCallback(dispatchChangeEventCallback);
return NS_OK;
}
return dispatchChangeEventCallback->DispatchEvents();
}
NS_IMPL_ISUPPORTS(HTMLInputElement::nsFilePickerShownCallback,
@ -2920,6 +3040,19 @@ HTMLInputElement::SetFiles(nsIDOMFileList* aFiles,
void
HTMLInputElement::AfterSetFilesOrDirectories(bool aSetValueChanged)
{
if (Preferences::GetBool("dom.webkitBlink.dirPicker.enabled", false) &&
HasAttr(kNameSpaceID_None, nsGkAtoms::webkitdirectory)) {
// This will call AfterSetFilesOrDirectoriesInternal eventually.
ExploreDirectoryRecursively(aSetValueChanged);
return;
}
AfterSetFilesOrDirectoriesInternal(aSetValueChanged);
}
void
HTMLInputElement::AfterSetFilesOrDirectoriesInternal(bool aSetValueChanged)
{
// No need to flush here, if there's no frame at this point we
// don't need to force creation of one just to tell it about this
@ -2983,7 +3116,9 @@ HTMLInputElement::GetFiles()
}
if (Preferences::GetBool("dom.input.dirpicker", false) &&
HasAttr(kNameSpaceID_None, nsGkAtoms::directory)) {
HasAttr(kNameSpaceID_None, nsGkAtoms::directory) &&
(!Preferences::GetBool("dom.webkitBlink.dirPicker.enabled", false) ||
!HasAttr(kNameSpaceID_None, nsGkAtoms::webkitdirectory))) {
return nullptr;
}
@ -4073,8 +4208,10 @@ HTMLInputElement::MaybeInitPickers(EventChainPostVisitor& aVisitor)
if (target &&
target->GetParent() == this &&
target->IsRootOfNativeAnonymousSubtree() &&
target->HasAttr(kNameSpaceID_None, nsGkAtoms::directory)) {
MOZ_ASSERT(Preferences::GetBool("dom.input.dirpicker", false),
(target->HasAttr(kNameSpaceID_None, nsGkAtoms::directory) ||
target->HasAttr(kNameSpaceID_None, nsGkAtoms::webkitdirectory))) {
MOZ_ASSERT(Preferences::GetBool("dom.input.dirpicker", false) ||
Preferences::GetBool("dom.webkitBlink.dirPicker.enabled", false),
"No API or UI should have been exposed to allow this code to "
"be reached");
type = FILE_PICKER_DIRECTORY;
@ -5284,7 +5421,8 @@ HTMLInputElement::GetAttributeChangeHint(const nsIAtom* aAttribute,
if (aAttribute == nsGkAtoms::type ||
// The presence or absence of the 'directory' attribute determines what
// buttons we show for type=file.
aAttribute == nsGkAtoms::directory) {
aAttribute == nsGkAtoms::directory ||
aAttribute == nsGkAtoms::webkitdirectory) {
retval |= NS_STYLE_HINT_FRAMECHANGE;
} else if (mType == NS_FORM_INPUT_IMAGE &&
(aAttribute == nsGkAtoms::alt ||
@ -5420,41 +5558,18 @@ HTMLInputElement::GetFiles(bool aRecursiveFlag, ErrorResult& aRv)
return nullptr;
}
GetFilesHelper* helper = GetOrCreateGetFilesHelper(aRecursiveFlag, aRv);
if (NS_WARN_IF(aRv.Failed())) {
return nullptr;
}
MOZ_ASSERT(helper);
nsCOMPtr<nsIGlobalObject> global = OwnerDoc()->GetScopeObject();
MOZ_ASSERT(global);
if (!global) {
return nullptr;
}
RefPtr<GetFilesHelper> helper;
if (aRecursiveFlag) {
if (!mGetFilesRecursiveHelper) {
mGetFilesRecursiveHelper =
GetFilesHelper::Create(global,
GetFilesOrDirectoriesInternal(),
aRecursiveFlag, aRv);
if (NS_WARN_IF(aRv.Failed())) {
return nullptr;
}
}
helper = mGetFilesRecursiveHelper;
} else {
if (!mGetFilesNonRecursiveHelper) {
mGetFilesNonRecursiveHelper =
GetFilesHelper::Create(global,
GetFilesOrDirectoriesInternal(),
aRecursiveFlag, aRv);
if (NS_WARN_IF(aRv.Failed())) {
return nullptr;
}
}
helper = mGetFilesNonRecursiveHelper;
}
MOZ_ASSERT(helper);
RefPtr<Promise> p = Promise::Create(global, aRv);
if (aRv.Failed()) {
return nullptr;
@ -7975,6 +8090,60 @@ HTMLInputElement::ClearGetFilesHelpers()
}
}
GetFilesHelper*
HTMLInputElement::GetOrCreateGetFilesHelper(bool aRecursiveFlag,
ErrorResult& aRv)
{
nsCOMPtr<nsIGlobalObject> global = OwnerDoc()->GetScopeObject();
MOZ_ASSERT(global);
if (!global) {
aRv.Throw(NS_ERROR_FAILURE);
return nullptr;
}
if (aRecursiveFlag) {
if (!mGetFilesRecursiveHelper) {
mGetFilesRecursiveHelper =
GetFilesHelper::Create(global,
GetFilesOrDirectoriesInternal(),
aRecursiveFlag, aRv);
if (NS_WARN_IF(aRv.Failed())) {
return nullptr;
}
}
return mGetFilesRecursiveHelper;
}
if (!mGetFilesNonRecursiveHelper) {
mGetFilesNonRecursiveHelper =
GetFilesHelper::Create(global,
GetFilesOrDirectoriesInternal(),
aRecursiveFlag, aRv);
if (NS_WARN_IF(aRv.Failed())) {
return nullptr;
}
}
return mGetFilesNonRecursiveHelper;
}
void
HTMLInputElement::ExploreDirectoryRecursively(bool aSetValueChanged)
{
ErrorResult rv;
GetFilesHelper* helper = GetOrCreateGetFilesHelper(true /* recursionFlag */,
rv);
if (NS_WARN_IF(rv.Failed())) {
AfterSetFilesOrDirectoriesInternal(aSetValueChanged);
return;
}
RefPtr<AfterSetFilesOrDirectoriesCallback> callback =
new AfterSetFilesOrDirectoriesCallback(this, aSetValueChanged);
helper->AddCallback(callback);
}
} // namespace dom
} // namespace mozilla

View File

@ -37,7 +37,9 @@ class EventChainPreVisitor;
namespace dom {
class AfterSetFilesOrDirectoriesRunnable;
class Date;
class DispatchChangeEventCallback;
class File;
class FileList;
class GetFilesHelper;
@ -107,6 +109,9 @@ class HTMLInputElement final : public nsGenericHTMLFormElementWithState,
public nsIDOMNSEditableElement,
public nsIConstraintValidation
{
friend class AfterSetFilesOrDirectoriesCallback;
friend class DispatchChangeEventCallback;
public:
using nsIConstraintValidation::GetValidationMessage;
using nsIConstraintValidation::CheckValidity;
@ -700,6 +705,16 @@ public:
SetHTMLBoolAttr(nsGkAtoms::directory, aValue, aRv);
}
bool WebkitDirectoryAttr() const
{
return HasAttr(kNameSpaceID_None, nsGkAtoms::webkitdirectory);
}
void SetWebkitDirectoryAttr(bool aValue, ErrorResult& aRv)
{
SetHTMLBoolAttr(nsGkAtoms::webkitdirectory, aValue, aRv);
}
bool IsFilesAndDirectoriesSupported() const;
already_AddRefed<Promise> GetFilesAndDirectories(ErrorResult& aRv);
@ -938,8 +953,16 @@ protected:
/**
* Called after calling one of the SetFilesOrDirectories() functions.
* This method can explore the directory recursively if needed.
*/
void AfterSetFilesOrDirectories(bool aSetValueChanged);
void AfterSetFilesOrDirectoriesInternal(bool aSetValueChanged);
/**
* Recursively explore the directory and populate mFileOrDirectories correctly
* for webkitdirectory.
*/
void ExploreDirectoryRecursively(bool aSetValuechanged);
/**
* Determine whether the editor needs to be initialized explicitly for
@ -1256,6 +1279,9 @@ protected:
*/
bool IsPopupBlocked() const;
GetFilesHelper* GetOrCreateGetFilesHelper(bool aRecursiveFlag,
ErrorResult& aRv);
void ClearGetFilesHelpers();
nsCOMPtr<nsIControllers> mControllers;

View File

@ -15,7 +15,10 @@
* http://w3c.github.io/filesystem-api/#idl-def-Directory
* https://microsoftedge.github.io/directory-upload/proposal.html#directory-interface
*/
[Exposed=(Window,Worker)]
// This chromeConstructor is used by the MockFilePicker for testing only.
[ChromeConstructor(DOMString path),
Exposed=(Window,Worker)]
interface Directory {
/*
* The leaf name of the directory.

View File

@ -209,6 +209,9 @@ partial interface HTMLInputElement {
[Throws, Pref="dom.input.dirpicker"]
void chooseDirectory();
[Pref="dom.webkitBlink.dirPicker.enabled", BinaryName="WebkitDirectoryAttr", SetterThrows]
attribute boolean webkitdirectory;
};
[NoInterfaceObject]

View File

@ -103,6 +103,11 @@ this.MockFilePicker = {
this.returnFiles = [file];
},
useDirectory: function(aPath) {
var directory = new this.window.Directory(aPath);
this.returnFiles = [directory];
},
isNsIFile: function(aFile) {
let ret = false;
try {