mirror of
https://we.phorge.it/source/phorge.git
synced 2024-12-27 07:50:57 +01:00
Provide an <input type="file">
control in Remarkup for mobile and users with esoteric windowing systems
Summary: Ref T5187. This definitely feels a bit flimsy and I'm going to hold it until I cut the release since it changes a couple of things about Workflow in general, but it seems to work OK and most of it is fine. The intent is described in T5187#176236. In practice, most of that works like I describe, then the `phui-file-upload` behavior gets some weird glue to figure out if the input is part of the form. Not the most elegant system, but I think it'll hold until we come up with many reasons to write a lot more Javascript. Test Plan: Used both drag-and-drop and the upload dialog to upload files in Safari, Firefox and Chrome. {F1653716} Reviewers: chad Reviewed By: chad Maniphest Tasks: T5187 Differential Revision: https://secure.phabricator.com/D15953
This commit is contained in:
parent
804a5db41a
commit
f2c36a934e
11 changed files with 287 additions and 38 deletions
|
@ -1594,6 +1594,7 @@ phutil_register_library_map(array(
|
|||
'PHUIFeedStoryExample' => 'applications/uiexample/examples/PHUIFeedStoryExample.php',
|
||||
'PHUIFeedStoryView' => 'view/phui/PHUIFeedStoryView.php',
|
||||
'PHUIFormDividerControl' => 'view/form/control/PHUIFormDividerControl.php',
|
||||
'PHUIFormFileControl' => 'view/form/control/PHUIFormFileControl.php',
|
||||
'PHUIFormFreeformDateControl' => 'view/form/control/PHUIFormFreeformDateControl.php',
|
||||
'PHUIFormIconSetControl' => 'view/form/control/PHUIFormIconSetControl.php',
|
||||
'PHUIFormInsetView' => 'view/form/PHUIFormInsetView.php',
|
||||
|
@ -5996,6 +5997,7 @@ phutil_register_library_map(array(
|
|||
'PHUIFeedStoryExample' => 'PhabricatorUIExample',
|
||||
'PHUIFeedStoryView' => 'AphrontView',
|
||||
'PHUIFormDividerControl' => 'AphrontFormControl',
|
||||
'PHUIFormFileControl' => 'AphrontFormControl',
|
||||
'PHUIFormFreeformDateControl' => 'AphrontFormControl',
|
||||
'PHUIFormIconSetControl' => 'AphrontFormControl',
|
||||
'PHUIFormInsetView' => 'AphrontView',
|
||||
|
|
|
@ -56,7 +56,7 @@ final class PhabricatorFileDropUploadController
|
|||
$file_phid = $result['filePHID'];
|
||||
if ($file_phid) {
|
||||
$file = $this->loadFile($file_phid);
|
||||
$result += $this->getFileDictionary($file);
|
||||
$result += $file->getDragAndDropDictionary();
|
||||
}
|
||||
|
||||
return id(new AphrontAjaxResponse())->setContent($result);
|
||||
|
@ -84,7 +84,7 @@ final class PhabricatorFileDropUploadController
|
|||
} else {
|
||||
$result = array(
|
||||
'complete' => true,
|
||||
) + $this->getFileDictionary($file);
|
||||
) + $file->getDragAndDropDictionary();
|
||||
}
|
||||
|
||||
return id(new AphrontAjaxResponse())->setContent($result);
|
||||
|
@ -99,18 +99,10 @@ final class PhabricatorFileDropUploadController
|
|||
'isExplicitUpload' => true,
|
||||
));
|
||||
|
||||
$result = $this->getFileDictionary($file);
|
||||
$result = $file->getDragAndDropDictionary();
|
||||
return id(new AphrontAjaxResponse())->setContent($result);
|
||||
}
|
||||
|
||||
private function getFileDictionary(PhabricatorFile $file) {
|
||||
return array(
|
||||
'id' => $file->getID(),
|
||||
'phid' => $file->getPHID(),
|
||||
'uri' => $file->getBestURI(),
|
||||
);
|
||||
}
|
||||
|
||||
private function loadFile($file_phid) {
|
||||
$viewer = $this->getViewer();
|
||||
|
||||
|
|
|
@ -6,12 +6,51 @@ final class PhabricatorFileUploadDialogController
|
|||
public function handleRequest(AphrontRequest $request) {
|
||||
$viewer = $request->getViewer();
|
||||
|
||||
return $this->newDialog()
|
||||
->setTitle(pht('Upload File'))
|
||||
->appendChild(pht(
|
||||
'To add files, drag and drop them into the comment text area.'))
|
||||
->addCancelButton('/', pht('Close'));
|
||||
$e_file = true;
|
||||
$errors = array();
|
||||
if ($request->isDialogFormPost()) {
|
||||
$file_phids = $request->getStrList('filePHIDs');
|
||||
if ($file_phids) {
|
||||
$files = id(new PhabricatorFileQuery())
|
||||
->setViewer($viewer)
|
||||
->withPHIDs($file_phids)
|
||||
->setRaisePolicyExceptions(true)
|
||||
->execute();
|
||||
} else {
|
||||
$files = array();
|
||||
}
|
||||
|
||||
if ($files) {
|
||||
$results = array();
|
||||
foreach ($files as $file) {
|
||||
$results[] = $file->getDragAndDropDictionary();
|
||||
}
|
||||
|
||||
$content = array(
|
||||
'files' => $results,
|
||||
);
|
||||
|
||||
return id(new AphrontAjaxResponse())->setContent($content);
|
||||
} else {
|
||||
$e_file = pht('Required');
|
||||
$errors[] = pht('You must choose a file to upload.');
|
||||
}
|
||||
}
|
||||
|
||||
$form = id(new AphrontFormView())
|
||||
->appendChild(
|
||||
id(new PHUIFormFileControl())
|
||||
->setName('filePHIDs')
|
||||
->setLabel(pht('Upload File'))
|
||||
->setAllowMultiple(true)
|
||||
->setError($e_file));
|
||||
|
||||
return $this->newDialog()
|
||||
->setTitle(pht('File'))
|
||||
->setErrors($errors)
|
||||
->appendForm($form)
|
||||
->addSubmitButton(pht('Upload'))
|
||||
->addCancelButton('/');
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -851,6 +851,14 @@ final class PhabricatorFile extends PhabricatorFileDAO
|
|||
return $supported;
|
||||
}
|
||||
|
||||
public function getDragAndDropDictionary() {
|
||||
return array(
|
||||
'id' => $this->getID(),
|
||||
'phid' => $this->getPHID(),
|
||||
'uri' => $this->getBestURI(),
|
||||
);
|
||||
}
|
||||
|
||||
public function instantiateStorageEngine() {
|
||||
return self::buildEngine($this->getStorageEngine());
|
||||
}
|
||||
|
|
44
src/view/form/control/PHUIFormFileControl.php
Normal file
44
src/view/form/control/PHUIFormFileControl.php
Normal file
|
@ -0,0 +1,44 @@
|
|||
<?php
|
||||
|
||||
final class PHUIFormFileControl
|
||||
extends AphrontFormControl {
|
||||
|
||||
private $allowMultiple;
|
||||
|
||||
protected function getCustomControlClass() {
|
||||
return 'phui-form-file-upload';
|
||||
}
|
||||
|
||||
public function setAllowMultiple($allow_multiple) {
|
||||
$this->allowMultiple = $allow_multiple;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getAllowMultiple() {
|
||||
return $this->allowMultiple;
|
||||
}
|
||||
|
||||
protected function renderInput() {
|
||||
$file_id = $this->getID();
|
||||
|
||||
Javelin::initBehavior(
|
||||
'phui-file-upload',
|
||||
array(
|
||||
'fileInputID' => $file_id,
|
||||
'inputName' => $this->getName(),
|
||||
'uploadURI' => '/file/dropupload/',
|
||||
'chunkThreshold' => PhabricatorFileStorageEngine::getChunkThreshold(),
|
||||
));
|
||||
|
||||
return phutil_tag(
|
||||
'input',
|
||||
array(
|
||||
'type' => 'file',
|
||||
'multiple' => $this->getAllowMultiple() ? 'multiple' : null,
|
||||
'name' => $this->getName().'.raw',
|
||||
'id' => $file_id,
|
||||
'disabled' => $this->getDisabled() ? 'disabled' : null,
|
||||
));
|
||||
}
|
||||
|
||||
}
|
63
webroot/rsrc/externals/javelin/lib/Workflow.js
vendored
63
webroot/rsrc/externals/javelin/lib/Workflow.js
vendored
|
@ -25,7 +25,7 @@ JX.install('Workflow', {
|
|||
this.setData(data || {});
|
||||
},
|
||||
|
||||
events : ['error', 'finally', 'submit'],
|
||||
events : ['error', 'finally', 'submit', 'start'],
|
||||
|
||||
statics : {
|
||||
_stack : [],
|
||||
|
@ -54,6 +54,9 @@ JX.install('Workflow', {
|
|||
}
|
||||
|
||||
var workflow = new JX.Workflow(form.getAttribute('action'), {});
|
||||
|
||||
workflow._form = form;
|
||||
|
||||
workflow.setDataWithListOfPairs(pairs);
|
||||
workflow.setMethod(form.getAttribute('method'));
|
||||
workflow.listen('finally', function() {
|
||||
|
@ -137,9 +140,14 @@ JX.install('Workflow', {
|
|||
data.push([button.name, button.value || true]);
|
||||
|
||||
var active = JX.Workflow._getActiveWorkflow();
|
||||
|
||||
active._form = form;
|
||||
|
||||
var e = active.invoke('submit', {form: form, data: data});
|
||||
if (!e.getStopped()) {
|
||||
active._destroy();
|
||||
// NOTE: Don't remove the current dialog yet because additional
|
||||
// handlers may still want to access the nodes.
|
||||
|
||||
active
|
||||
.setURI(form.getAttribute('action') || active.getURI())
|
||||
.setDataWithListOfPairs(data)
|
||||
|
@ -156,7 +164,41 @@ JX.install('Workflow', {
|
|||
_root : null,
|
||||
_pushed : false,
|
||||
_data : null,
|
||||
|
||||
_form: null,
|
||||
_paused: 0,
|
||||
_nextCallback: null,
|
||||
|
||||
getSourceForm: function() {
|
||||
return this._form;
|
||||
},
|
||||
|
||||
pause: function() {
|
||||
this._paused++;
|
||||
return this;
|
||||
},
|
||||
|
||||
resume: function() {
|
||||
if (!this._paused) {
|
||||
JX.$E('Resuming a workflow which is not paused!');
|
||||
}
|
||||
|
||||
this._paused--;
|
||||
|
||||
if (!this._paused) {
|
||||
var next = this._nextCallback;
|
||||
this._nextCallback = null;
|
||||
if (next) {
|
||||
next();
|
||||
}
|
||||
}
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
_onload : function(r) {
|
||||
this._destroy();
|
||||
|
||||
// It is permissible to send back a falsey redirect to force a page
|
||||
// reload, so we need to take this branch if the key is present.
|
||||
if (r && (typeof r.redirect != 'undefined')) {
|
||||
|
@ -247,7 +289,19 @@ JX.install('Workflow', {
|
|||
this._root = null;
|
||||
}
|
||||
},
|
||||
|
||||
start : function() {
|
||||
var next = JX.bind(this, this._send);
|
||||
|
||||
this.pause();
|
||||
this._nextCallback = next;
|
||||
|
||||
this.invoke('start', this);
|
||||
|
||||
this.resume();
|
||||
},
|
||||
|
||||
_send: function() {
|
||||
var uri = this.getURI();
|
||||
var method = this.getMethod();
|
||||
var r = new JX.Request(uri, JX.bind(this, this._onload));
|
||||
|
@ -291,6 +345,11 @@ JX.install('Workflow', {
|
|||
return this;
|
||||
},
|
||||
|
||||
addData: function(key, value) {
|
||||
this._data.push([key, value]);
|
||||
return this;
|
||||
},
|
||||
|
||||
setDataWithListOfPairs : function(list_of_pairs) {
|
||||
this._data = list_of_pairs;
|
||||
return this;
|
||||
|
|
|
@ -155,7 +155,7 @@ JX.install('PhabricatorDragAndDropFileUpload', {
|
|||
|
||||
var files = e.getRawEvent().dataTransfer.files;
|
||||
for (var ii = 0; ii < files.length; ii++) {
|
||||
this._sendRequest(files[ii]);
|
||||
this.sendRequest(files[ii]);
|
||||
}
|
||||
|
||||
// Force depth to 0.
|
||||
|
@ -216,7 +216,7 @@ JX.install('PhabricatorDragAndDropFileUpload', {
|
|||
if (!spec.name) {
|
||||
spec.name = 'pasted_file';
|
||||
}
|
||||
this._sendRequest(spec);
|
||||
this.sendRequest(spec);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
@ -224,7 +224,7 @@ JX.install('PhabricatorDragAndDropFileUpload', {
|
|||
this.setIsEnabled(true);
|
||||
},
|
||||
|
||||
_sendRequest : function(spec) {
|
||||
sendRequest : function(spec) {
|
||||
var file = new JX.PhabricatorFileUpload()
|
||||
.setRawFileObject(spec)
|
||||
.setName(spec.name)
|
||||
|
|
|
@ -62,6 +62,26 @@ JX.install('TextAreaUtils', {
|
|||
JX.TextAreaUtils.setSelectionRange(area, start, end);
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Insert a reference to a given uploaded file into a textarea.
|
||||
*/
|
||||
insertFileReference: function(area, file) {
|
||||
var ref = '{F' + file.getID() + '}';
|
||||
|
||||
// If we're inserting immediately after a "}" (usually, another file
|
||||
// reference), put some newlines before our token so that multiple file
|
||||
// uploads get laid out more nicely.
|
||||
var range = JX.TextAreaUtils.getSelectionRange(area);
|
||||
var before = area.value.substring(0, range.start);
|
||||
if (before.match(/\}$/)) {
|
||||
ref = '\n\n' + ref;
|
||||
}
|
||||
|
||||
JX.TextAreaUtils.setSelectionText(area, ref, false);
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Get the document pixel positions of the beginning and end of a character
|
||||
* range in a textarea.
|
||||
|
|
|
@ -10,32 +10,23 @@ JX.behavior('aphront-drag-and-drop-textarea', function(config) {
|
|||
|
||||
var target = JX.$(config.target);
|
||||
|
||||
function onupload(f) {
|
||||
var ref = '{F' + f.getID() + '}';
|
||||
|
||||
// If we're inserting immediately after a "}" (usually, another file
|
||||
// reference), put some newlines before our token so that multiple file
|
||||
// uploads get laid out more nicely.
|
||||
var range = JX.TextAreaUtils.getSelectionRange(target);
|
||||
var before = target.value.substring(0, range.start);
|
||||
if (before.match(/\}$/)) {
|
||||
ref = '\n\n' + ref;
|
||||
}
|
||||
|
||||
JX.TextAreaUtils.setSelectionText(target, ref, false);
|
||||
}
|
||||
|
||||
if (JX.PhabricatorDragAndDropFileUpload.isSupported()) {
|
||||
var drop = new JX.PhabricatorDragAndDropFileUpload(target)
|
||||
.setURI(config.uri)
|
||||
.setChunkThreshold(config.chunkThreshold);
|
||||
|
||||
drop.listen('didBeginDrag', function() {
|
||||
JX.DOM.alterClass(target, config.activatedClass, true);
|
||||
});
|
||||
|
||||
drop.listen('didEndDrag', function() {
|
||||
JX.DOM.alterClass(target, config.activatedClass, false);
|
||||
});
|
||||
drop.listen('didUpload', onupload);
|
||||
|
||||
drop.listen('didUpload', function(file) {
|
||||
JX.TextAreaUtils.insertFileReference(target, file);
|
||||
});
|
||||
|
||||
drop.start();
|
||||
}
|
||||
|
||||
|
|
|
@ -194,7 +194,21 @@ JX.behavior('phabricator-remarkup-assist', function(config) {
|
|||
.start();
|
||||
break;
|
||||
case 'fa-cloud-upload':
|
||||
new JX.Workflow('/file/uploaddialog/').start();
|
||||
new JX.Workflow('/file/uploaddialog/')
|
||||
.setHandler(function(response) {
|
||||
var files = response.files;
|
||||
for (var ii = 0; ii < files.length; ii++) {
|
||||
var file = files[ii];
|
||||
|
||||
var upload = new JX.PhabricatorFileUpload()
|
||||
.setID(file.id)
|
||||
.setPHID(file.phid)
|
||||
.setURI(file.uri);
|
||||
|
||||
JX.TextAreaUtils.insertFileReference(area, upload);
|
||||
}
|
||||
})
|
||||
.start();
|
||||
break;
|
||||
case 'fa-arrows-alt':
|
||||
if (edit_mode == 'fa-arrows-alt') {
|
||||
|
|
80
webroot/rsrc/js/phui/behavior-phui-file-upload.js
Normal file
80
webroot/rsrc/js/phui/behavior-phui-file-upload.js
Normal file
|
@ -0,0 +1,80 @@
|
|||
/**
|
||||
* @provides javelin-behavior-phui-file-upload
|
||||
* @requires javelin-behavior
|
||||
* javelin-stratcom
|
||||
* javelin-dom
|
||||
* phuix-dropdown-menu
|
||||
*/
|
||||
|
||||
JX.behavior('phui-file-upload', function(config) {
|
||||
|
||||
function startUpload(workflow, input) {
|
||||
var files = input.files;
|
||||
|
||||
if (!files || !files.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
var state = {
|
||||
workflow: workflow,
|
||||
input: input,
|
||||
waiting: 0,
|
||||
phids: []
|
||||
};
|
||||
|
||||
var callback = JX.bind(null, didUpload, state);
|
||||
|
||||
var dummy = input;
|
||||
var uploader = new JX.PhabricatorDragAndDropFileUpload(dummy)
|
||||
.setURI(config.uploadURI)
|
||||
.setChunkThreshold(config.chunkThreshold);
|
||||
|
||||
uploader.listen('didUpload', callback);
|
||||
uploader.start();
|
||||
|
||||
workflow.pause();
|
||||
for (var ii = 0; ii < files.length; ii++) {
|
||||
state.waiting++;
|
||||
uploader.sendRequest(files[ii]);
|
||||
}
|
||||
}
|
||||
|
||||
function didUpload(state, file) {
|
||||
state.phids.push(file.getPHID());
|
||||
state.waiting--;
|
||||
|
||||
if (state.waiting) {
|
||||
return;
|
||||
}
|
||||
|
||||
state.workflow
|
||||
.addData(config.inputName, state.phids.join(', '))
|
||||
.resume();
|
||||
}
|
||||
|
||||
JX.Workflow.listen('start', function(workflow) {
|
||||
var form = workflow.getSourceForm();
|
||||
if (!form) {
|
||||
return;
|
||||
}
|
||||
|
||||
var input;
|
||||
try {
|
||||
input = JX.$(config.fileInputID);
|
||||
} catch (ex) {
|
||||
return;
|
||||
}
|
||||
|
||||
var local_form = JX.DOM.findAbove(input, 'form');
|
||||
if (!local_form) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (local_form !== form) {
|
||||
return;
|
||||
}
|
||||
|
||||
startUpload(workflow, input);
|
||||
});
|
||||
|
||||
});
|
Loading…
Reference in a new issue