Line data Source code
1 : /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 : /* This Source Code Form is subject to the terms of the Mozilla Public
3 : * License, v. 2.0. If a copy of the MPL was not distributed with this
4 : * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 :
6 : #include "nsFileControlFrame.h"
7 :
8 : #include "nsGkAtoms.h"
9 : #include "nsCOMPtr.h"
10 : #include "nsIDocument.h"
11 : #include "mozilla/dom/NodeInfo.h"
12 : #include "mozilla/dom/Element.h"
13 : #include "mozilla/dom/DataTransfer.h"
14 : #include "mozilla/dom/HTMLButtonElement.h"
15 : #include "mozilla/dom/HTMLInputElement.h"
16 : #include "mozilla/Preferences.h"
17 : #include "nsNodeInfoManager.h"
18 : #include "nsContentCreatorFunctions.h"
19 : #include "nsContentUtils.h"
20 : #include "mozilla/EventStates.h"
21 : #include "mozilla/dom/DOMStringList.h"
22 : #include "mozilla/dom/Directory.h"
23 : #include "mozilla/dom/FileList.h"
24 : #include "nsIDOMDragEvent.h"
25 : #include "nsIDOMFileList.h"
26 : #include "nsContentList.h"
27 : #include "nsIDOMMutationEvent.h"
28 : #include "nsTextNode.h"
29 :
30 : using namespace mozilla;
31 : using namespace mozilla::dom;
32 :
33 : nsIFrame*
34 0 : NS_NewFileControlFrame(nsIPresShell* aPresShell, nsStyleContext* aContext)
35 : {
36 0 : return new (aPresShell) nsFileControlFrame(aContext);
37 : }
38 :
39 0 : NS_IMPL_FRAMEARENA_HELPERS(nsFileControlFrame)
40 :
41 0 : nsFileControlFrame::nsFileControlFrame(nsStyleContext* aContext)
42 0 : : nsBlockFrame(aContext, kClassID)
43 : {
44 0 : AddStateBits(NS_BLOCK_FLOAT_MGR);
45 0 : }
46 :
47 :
48 : void
49 0 : nsFileControlFrame::Init(nsIContent* aContent,
50 : nsContainerFrame* aParent,
51 : nsIFrame* aPrevInFlow)
52 : {
53 0 : nsBlockFrame::Init(aContent, aParent, aPrevInFlow);
54 :
55 0 : mMouseListener = new DnDListener(this);
56 0 : }
57 :
58 : void
59 0 : nsFileControlFrame::DestroyFrom(nsIFrame* aDestructRoot)
60 : {
61 0 : ENSURE_TRUE(mContent);
62 :
63 : // Remove the events.
64 0 : if (mContent) {
65 0 : mContent->RemoveSystemEventListener(NS_LITERAL_STRING("drop"),
66 0 : mMouseListener, false);
67 0 : mContent->RemoveSystemEventListener(NS_LITERAL_STRING("dragover"),
68 0 : mMouseListener, false);
69 : }
70 :
71 0 : nsContentUtils::DestroyAnonymousContent(&mTextContent);
72 0 : nsContentUtils::DestroyAnonymousContent(&mBrowseFilesOrDirs);
73 :
74 0 : mMouseListener->ForgetFrame();
75 0 : nsBlockFrame::DestroyFrom(aDestructRoot);
76 : }
77 :
78 : static already_AddRefed<Element>
79 0 : MakeAnonButton(nsIDocument* aDoc, const char* labelKey,
80 : HTMLInputElement* aInputElement,
81 : const nsAString& aAccessKey)
82 : {
83 0 : RefPtr<Element> button = aDoc->CreateHTMLElement(nsGkAtoms::button);
84 : // NOTE: SetIsNativeAnonymousRoot() has to be called before setting any
85 : // attribute.
86 0 : button->SetIsNativeAnonymousRoot();
87 0 : button->SetAttr(kNameSpaceID_None, nsGkAtoms::type,
88 0 : NS_LITERAL_STRING("button"), false);
89 :
90 : // Set the file picking button text depending on the current locale.
91 0 : nsXPIDLString buttonTxt;
92 : nsContentUtils::GetLocalizedString(nsContentUtils::eFORMS_PROPERTIES,
93 0 : labelKey, buttonTxt);
94 :
95 : // Set the browse button text. It's a bit of a pain to do because we want to
96 : // make sure we are not notifying.
97 : RefPtr<nsTextNode> textContent =
98 0 : new nsTextNode(button->NodeInfo()->NodeInfoManager());
99 :
100 0 : textContent->SetText(buttonTxt, false);
101 :
102 0 : nsresult rv = button->AppendChildTo(textContent, false);
103 0 : if (NS_FAILED(rv)) {
104 0 : return nullptr;
105 : }
106 :
107 : // Make sure access key and tab order for the element actually redirect to the
108 : // file picking button.
109 : RefPtr<HTMLButtonElement> buttonElement =
110 0 : HTMLButtonElement::FromContentOrNull(button);
111 :
112 0 : if (!aAccessKey.IsEmpty()) {
113 0 : buttonElement->SetAccessKey(aAccessKey);
114 : }
115 :
116 : // Both elements are given the same tab index so that the user can tab
117 : // to the file control at the correct index, and then between the two
118 : // buttons.
119 : int32_t tabIndex;
120 0 : aInputElement->GetTabIndex(&tabIndex);
121 0 : buttonElement->SetTabIndex(tabIndex);
122 :
123 0 : return button.forget();
124 : }
125 :
126 : nsresult
127 0 : nsFileControlFrame::CreateAnonymousContent(nsTArray<ContentInfo>& aElements)
128 : {
129 0 : nsCOMPtr<nsIDocument> doc = mContent->GetComposedDoc();
130 :
131 0 : RefPtr<HTMLInputElement> fileContent = HTMLInputElement::FromContentOrNull(mContent);
132 :
133 : // The access key is transferred to the "Choose files..." button only. In
134 : // effect that access key allows access to the control via that button, then
135 : // the user can tab between the two buttons.
136 0 : nsAutoString accessKey;
137 0 : fileContent->GetAccessKey(accessKey);
138 :
139 0 : mBrowseFilesOrDirs = MakeAnonButton(doc, "Browse", fileContent, accessKey);
140 0 : if (!mBrowseFilesOrDirs || !aElements.AppendElement(mBrowseFilesOrDirs)) {
141 0 : return NS_ERROR_OUT_OF_MEMORY;
142 : }
143 :
144 : // Create and setup the text showing the selected files.
145 0 : RefPtr<NodeInfo> nodeInfo;
146 0 : nodeInfo = doc->NodeInfoManager()->GetNodeInfo(nsGkAtoms::label, nullptr,
147 : kNameSpaceID_XUL,
148 0 : nsIDOMNode::ELEMENT_NODE);
149 0 : NS_TrustedNewXULElement(getter_AddRefs(mTextContent), nodeInfo.forget());
150 : // NOTE: SetIsNativeAnonymousRoot() has to be called before setting any
151 : // attribute.
152 0 : mTextContent->SetIsNativeAnonymousRoot();
153 0 : mTextContent->SetAttr(kNameSpaceID_None, nsGkAtoms::crop,
154 0 : NS_LITERAL_STRING("center"), false);
155 :
156 : // Update the displayed text to reflect the current element's value.
157 0 : nsAutoString value;
158 0 : HTMLInputElement::FromContent(mContent)->GetDisplayFileName(value);
159 0 : UpdateDisplayedValue(value, false);
160 :
161 0 : if (!aElements.AppendElement(mTextContent)) {
162 0 : return NS_ERROR_OUT_OF_MEMORY;
163 : }
164 :
165 : // We should be able to interact with the element by doing drag and drop.
166 0 : mContent->AddSystemEventListener(NS_LITERAL_STRING("drop"),
167 0 : mMouseListener, false);
168 0 : mContent->AddSystemEventListener(NS_LITERAL_STRING("dragover"),
169 0 : mMouseListener, false);
170 :
171 0 : SyncDisabledState();
172 :
173 0 : return NS_OK;
174 : }
175 :
176 : void
177 0 : nsFileControlFrame::AppendAnonymousContentTo(nsTArray<nsIContent*>& aElements,
178 : uint32_t aFilter)
179 : {
180 0 : if (mBrowseFilesOrDirs) {
181 0 : aElements.AppendElement(mBrowseFilesOrDirs);
182 : }
183 :
184 0 : if (mTextContent) {
185 0 : aElements.AppendElement(mTextContent);
186 : }
187 0 : }
188 :
189 0 : NS_QUERYFRAME_HEAD(nsFileControlFrame)
190 0 : NS_QUERYFRAME_ENTRY(nsIAnonymousContentCreator)
191 0 : NS_QUERYFRAME_ENTRY(nsIFormControlFrame)
192 0 : NS_QUERYFRAME_TAIL_INHERITING(nsBlockFrame)
193 :
194 : void
195 0 : nsFileControlFrame::SetFocus(bool aOn, bool aRepaint)
196 : {
197 0 : }
198 :
199 : static void
200 0 : AppendBlobImplAsDirectory(nsTArray<OwningFileOrDirectory>& aArray,
201 : BlobImpl* aBlobImpl,
202 : nsIContent* aContent)
203 : {
204 0 : MOZ_ASSERT(aBlobImpl);
205 0 : MOZ_ASSERT(aBlobImpl->IsDirectory());
206 :
207 0 : nsAutoString fullpath;
208 0 : ErrorResult err;
209 0 : aBlobImpl->GetMozFullPath(fullpath, SystemCallerGuarantee(), err);
210 0 : if (err.Failed()) {
211 0 : err.SuppressException();
212 0 : return;
213 : }
214 :
215 0 : nsCOMPtr<nsIFile> file;
216 0 : nsresult rv = NS_NewLocalFile(fullpath, true, getter_AddRefs(file));
217 0 : if (NS_WARN_IF(NS_FAILED(rv))) {
218 0 : return;
219 : }
220 :
221 0 : nsPIDOMWindowInner* inner = aContent->OwnerDoc()->GetInnerWindow();
222 0 : if (!inner || !inner->IsCurrentInnerWindow()) {
223 0 : return;
224 : }
225 :
226 : RefPtr<Directory> directory =
227 0 : Directory::Create(inner, file);
228 0 : MOZ_ASSERT(directory);
229 :
230 0 : OwningFileOrDirectory* element = aArray.AppendElement();
231 0 : element->SetAsDirectory() = directory;
232 : }
233 :
234 : /**
235 : * This is called when we receive a drop or a dragover.
236 : */
237 : NS_IMETHODIMP
238 0 : nsFileControlFrame::DnDListener::HandleEvent(nsIDOMEvent* aEvent)
239 : {
240 0 : NS_ASSERTION(mFrame, "We should have been unregistered");
241 :
242 0 : bool defaultPrevented = false;
243 0 : aEvent->GetDefaultPrevented(&defaultPrevented);
244 0 : if (defaultPrevented) {
245 0 : return NS_OK;
246 : }
247 :
248 0 : nsCOMPtr<nsIDOMDragEvent> dragEvent = do_QueryInterface(aEvent);
249 0 : if (!dragEvent) {
250 0 : return NS_OK;
251 : }
252 :
253 0 : nsCOMPtr<nsIDOMDataTransfer> dataTransfer;
254 0 : dragEvent->GetDataTransfer(getter_AddRefs(dataTransfer));
255 0 : if (!IsValidDropData(dataTransfer)) {
256 0 : return NS_OK;
257 : }
258 :
259 :
260 0 : nsCOMPtr<nsIContent> content = mFrame->GetContent();
261 0 : bool supportsMultiple = content && content->HasAttr(kNameSpaceID_None, nsGkAtoms::multiple);
262 0 : if (!CanDropTheseFiles(dataTransfer, supportsMultiple)) {
263 0 : dataTransfer->SetDropEffect(NS_LITERAL_STRING("none"));
264 0 : aEvent->StopPropagation();
265 0 : return NS_OK;
266 : }
267 :
268 0 : nsAutoString eventType;
269 0 : aEvent->GetType(eventType);
270 0 : if (eventType.EqualsLiteral("dragover")) {
271 : // Prevent default if we can accept this drag data
272 0 : aEvent->PreventDefault();
273 0 : return NS_OK;
274 : }
275 :
276 0 : if (eventType.EqualsLiteral("drop")) {
277 0 : aEvent->StopPropagation();
278 0 : aEvent->PreventDefault();
279 :
280 0 : NS_ASSERTION(content, "The frame has no content???");
281 :
282 0 : HTMLInputElement* inputElement = HTMLInputElement::FromContent(content);
283 0 : NS_ASSERTION(inputElement, "No input element for this file upload control frame!");
284 :
285 0 : nsCOMPtr<nsIDOMFileList> fileList;
286 0 : dataTransfer->GetFiles(getter_AddRefs(fileList));
287 :
288 0 : RefPtr<BlobImpl> webkitDir;
289 : nsresult rv =
290 0 : GetBlobImplForWebkitDirectory(fileList, getter_AddRefs(webkitDir));
291 0 : NS_ENSURE_SUCCESS(rv, NS_OK);
292 :
293 0 : nsTArray<OwningFileOrDirectory> array;
294 0 : if (webkitDir) {
295 0 : AppendBlobImplAsDirectory(array, webkitDir, content);
296 0 : inputElement->MozSetDndFilesAndDirectories(array);
297 : } else {
298 : bool blinkFileSystemEnabled =
299 0 : Preferences::GetBool("dom.webkitBlink.filesystem.enabled", false);
300 : bool dirPickerEnabled =
301 0 : Preferences::GetBool("dom.input.dirpicker", false);
302 0 : if (blinkFileSystemEnabled || dirPickerEnabled) {
303 0 : FileList* files = static_cast<FileList*>(fileList.get());
304 0 : if (files) {
305 0 : for (uint32_t i = 0; i < files->Length(); ++i) {
306 0 : File* file = files->Item(i);
307 0 : if (file) {
308 0 : if (file->Impl() && file->Impl()->IsDirectory()) {
309 0 : AppendBlobImplAsDirectory(array, file->Impl(), content);
310 : } else {
311 0 : OwningFileOrDirectory* element = array.AppendElement();
312 0 : element->SetAsFile() = file;
313 : }
314 : }
315 : }
316 : }
317 : }
318 :
319 : // Entries API.
320 0 : if (blinkFileSystemEnabled) {
321 : // This is rather ugly. Pass the directories as Files using SetFiles,
322 : // but then if blink filesystem API is enabled, it wants
323 : // FileOrDirectory array.
324 0 : inputElement->SetFiles(fileList, true);
325 0 : inputElement->UpdateEntries(array);
326 : }
327 : // Directory Upload API
328 0 : else if (dirPickerEnabled) {
329 0 : inputElement->SetFilesOrDirectories(array, true);
330 : }
331 : // Normal DnD
332 : else {
333 0 : inputElement->SetFiles(fileList, true);
334 : }
335 :
336 0 : nsContentUtils::DispatchTrustedEvent(content->OwnerDoc(), content,
337 0 : NS_LITERAL_STRING("input"), true,
338 0 : false);
339 0 : nsContentUtils::DispatchTrustedEvent(content->OwnerDoc(), content,
340 0 : NS_LITERAL_STRING("change"), true,
341 0 : false);
342 : }
343 : }
344 :
345 0 : return NS_OK;
346 : }
347 :
348 : nsresult
349 0 : nsFileControlFrame::DnDListener::GetBlobImplForWebkitDirectory(nsIDOMFileList* aFileList,
350 : BlobImpl** aBlobImpl)
351 : {
352 0 : *aBlobImpl = nullptr;
353 :
354 : HTMLInputElement* inputElement =
355 0 : HTMLInputElement::FromContent(mFrame->GetContent());
356 : bool webkitDirPicker =
357 0 : Preferences::GetBool("dom.webkitBlink.dirPicker.enabled", false) &&
358 0 : inputElement->HasAttr(kNameSpaceID_None, nsGkAtoms::webkitdirectory);
359 0 : if (!webkitDirPicker) {
360 0 : return NS_OK;
361 : }
362 :
363 0 : if (!aFileList) {
364 0 : return NS_ERROR_FAILURE;
365 : }
366 :
367 0 : FileList* files = static_cast<FileList*>(aFileList);
368 : // webkitdirectory doesn't care about the length of the file list but
369 : // only about the first item on it.
370 0 : uint32_t len = files->Length();
371 0 : if (len) {
372 0 : File* file = files->Item(0);
373 0 : if (file) {
374 0 : BlobImpl* impl = file->Impl();
375 0 : if (impl && impl->IsDirectory()) {
376 0 : RefPtr<BlobImpl> retVal = impl;
377 0 : retVal.swap(*aBlobImpl);
378 0 : return NS_OK;
379 : }
380 : }
381 : }
382 :
383 0 : return NS_ERROR_FAILURE;
384 : }
385 :
386 : bool
387 0 : nsFileControlFrame::DnDListener::IsValidDropData(nsIDOMDataTransfer* aDOMDataTransfer)
388 : {
389 0 : nsCOMPtr<DataTransfer> dataTransfer = do_QueryInterface(aDOMDataTransfer);
390 0 : NS_ENSURE_TRUE(dataTransfer, false);
391 :
392 : // We only support dropping files onto a file upload control
393 0 : nsTArray<nsString> types;
394 0 : dataTransfer->GetTypes(types, CallerType::System);
395 :
396 0 : return types.Contains(NS_LITERAL_STRING("Files"));
397 : }
398 :
399 : bool
400 0 : nsFileControlFrame::DnDListener::CanDropTheseFiles(nsIDOMDataTransfer* aDOMDataTransfer,
401 : bool aSupportsMultiple)
402 : {
403 0 : nsCOMPtr<DataTransfer> dataTransfer = do_QueryInterface(aDOMDataTransfer);
404 0 : NS_ENSURE_TRUE(dataTransfer, false);
405 :
406 0 : nsCOMPtr<nsIDOMFileList> fileList;
407 0 : dataTransfer->GetFiles(getter_AddRefs(fileList));
408 :
409 0 : RefPtr<BlobImpl> webkitDir;
410 : nsresult rv =
411 0 : GetBlobImplForWebkitDirectory(fileList, getter_AddRefs(webkitDir));
412 : // Just check if either there isn't webkitdirectory attribute, or
413 : // fileList has a directory which can be dropped to the element.
414 : // No need to use webkitDir for anything here.
415 0 : NS_ENSURE_SUCCESS(rv, false);
416 :
417 0 : uint32_t listLength = 0;
418 0 : if (fileList) {
419 0 : fileList->GetLength(&listLength);
420 : }
421 0 : return listLength <= 1 || aSupportsMultiple;
422 : }
423 :
424 : nscoord
425 0 : nsFileControlFrame::GetMinISize(gfxContext *aRenderingContext)
426 : {
427 : nscoord result;
428 0 : DISPLAY_MIN_WIDTH(this, result);
429 :
430 : // Our min width is our pref width
431 0 : result = GetPrefISize(aRenderingContext);
432 0 : return result;
433 : }
434 :
435 : void
436 0 : nsFileControlFrame::SyncDisabledState()
437 : {
438 0 : EventStates eventStates = mContent->AsElement()->State();
439 0 : if (eventStates.HasState(NS_EVENT_STATE_DISABLED)) {
440 0 : mBrowseFilesOrDirs->SetAttr(kNameSpaceID_None, nsGkAtoms::disabled,
441 0 : EmptyString(), true);
442 : } else {
443 0 : mBrowseFilesOrDirs->UnsetAttr(kNameSpaceID_None, nsGkAtoms::disabled, true);
444 : }
445 0 : }
446 :
447 : nsresult
448 0 : nsFileControlFrame::AttributeChanged(int32_t aNameSpaceID,
449 : nsIAtom* aAttribute,
450 : int32_t aModType)
451 : {
452 0 : if (aNameSpaceID == kNameSpaceID_None && aAttribute == nsGkAtoms::tabindex) {
453 0 : if (aModType == nsIDOMMutationEvent::REMOVAL) {
454 0 : mBrowseFilesOrDirs->UnsetAttr(aNameSpaceID, aAttribute, true);
455 : } else {
456 0 : nsAutoString value;
457 0 : mContent->GetAttr(aNameSpaceID, aAttribute, value);
458 0 : mBrowseFilesOrDirs->SetAttr(aNameSpaceID, aAttribute, value, true);
459 : }
460 : }
461 :
462 0 : return nsBlockFrame::AttributeChanged(aNameSpaceID, aAttribute, aModType);
463 : }
464 :
465 : void
466 0 : nsFileControlFrame::ContentStatesChanged(EventStates aStates)
467 : {
468 0 : if (aStates.HasState(NS_EVENT_STATE_DISABLED)) {
469 0 : nsContentUtils::AddScriptRunner(new SyncDisabledStateEvent(this));
470 : }
471 0 : }
472 :
473 : #ifdef DEBUG_FRAME_DUMP
474 : nsresult
475 0 : nsFileControlFrame::GetFrameName(nsAString& aResult) const
476 : {
477 0 : return MakeFrameName(NS_LITERAL_STRING("FileControl"), aResult);
478 : }
479 : #endif
480 :
481 : void
482 0 : nsFileControlFrame::UpdateDisplayedValue(const nsAString& aValue, bool aNotify)
483 : {
484 0 : mTextContent->SetAttr(kNameSpaceID_None, nsGkAtoms::value, aValue, aNotify);
485 0 : }
486 :
487 : nsresult
488 0 : nsFileControlFrame::SetFormProperty(nsIAtom* aName,
489 : const nsAString& aValue)
490 : {
491 0 : if (nsGkAtoms::value == aName) {
492 0 : UpdateDisplayedValue(aValue, true);
493 : }
494 0 : return NS_OK;
495 : }
496 :
497 : void
498 0 : nsFileControlFrame::BuildDisplayList(nsDisplayListBuilder* aBuilder,
499 : const nsRect& aDirtyRect,
500 : const nsDisplayListSet& aLists)
501 : {
502 0 : BuildDisplayListForInline(aBuilder, aDirtyRect, aLists);
503 0 : }
504 :
505 : #ifdef ACCESSIBILITY
506 : a11y::AccType
507 0 : nsFileControlFrame::AccessibleType()
508 : {
509 0 : return a11y::eHTMLFileInputType;
510 : }
511 : #endif
512 :
513 : ////////////////////////////////////////////////////////////
514 : // Mouse listener implementation
515 :
516 0 : NS_IMPL_ISUPPORTS(nsFileControlFrame::MouseListener,
517 : nsIDOMEventListener)
|