1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-12-23 14:00:56 +01:00

Improve drag-and-drop uploader

Summary:
Make it discoverable, show uploading progress, show file thumbnails, allow you
to remove files, make it a generic form component.

Test Plan:
Uploaded ducks

Reviewed By: tomo
Reviewers: aran, tomo, jungejason, tuomaspelkonen
CC: anjali, aran, epriestley, tomo
Differential Revision: 334
This commit is contained in:
epriestley 2011-05-22 16:11:41 -07:00
parent 8af5bb117d
commit 109a202b6c
16 changed files with 413 additions and 121 deletions

View file

@ -7,6 +7,15 @@
*/
celerity_register_resource_map(array(
'aphront-attached-file-view-css' =>
array(
'uri' => '/res/79bc2c2e/rsrc/css/aphront/attached-file-view.css',
'type' => 'css',
'requires' =>
array(
),
'disk' => '/rsrc/css/aphront/attached-file-view.css',
),
'aphront-crumbs-view-css' =>
array(
'uri' => '/res/9009e6bd/rsrc/css/aphront/crumbs-view.css',
@ -45,7 +54,7 @@ celerity_register_resource_map(array(
),
'aphront-form-view-css' =>
array(
'uri' => '/res/dadf31b1/rsrc/css/aphront/form-view.css',
'uri' => '/res/38a347da/rsrc/css/aphront/form-view.css',
'type' => 'css',
'requires' =>
array(
@ -270,6 +279,16 @@ celerity_register_resource_map(array(
),
'disk' => '/rsrc/js/javelin/lib/behavior.js',
),
0 =>
array(
'uri' => '/res/39de799e/rsrc/js/javelin/docs/Base.js',
'type' => 'js',
'requires' =>
array(
0 => 'javelin-install',
),
'disk' => '/rsrc/js/javelin/docs/Base.js',
),
'javelin-behavior-aphront-basic-tokenizer' =>
array(
'uri' => '/res/bce3961b/rsrc/js/application/core/behavior-tokenizer.js',
@ -284,6 +303,19 @@ celerity_register_resource_map(array(
),
'disk' => '/rsrc/js/application/core/behavior-tokenizer.js',
),
'javelin-behavior-aphront-drag-and-drop' =>
array(
'uri' => '/res/170115f4/rsrc/js/application/core/behavior-drag-and-drop.js',
'type' => 'js',
'requires' =>
array(
0 => 'javelin-behavior',
1 => 'javelin-dom',
2 => 'javelin-util',
3 => 'phabricator-drag-and-drop-file-upload',
),
'disk' => '/rsrc/js/application/core/behavior-drag-and-drop.js',
),
'javelin-behavior-aphront-form-disable-on-submit' =>
array(
'uri' => '/res/6c659ede/rsrc/js/application/core/behavior-form.js',
@ -477,18 +509,6 @@ celerity_register_resource_map(array(
),
'disk' => '/rsrc/js/application/maniphest/behavior-transaction-controls.js',
),
'javelin-behavior-maniphest-transaction-drag-and-drop' =>
array(
'uri' => '/res/5bf1f40c/rsrc/js/application/maniphest/behavior-transaction-drag-and-drop.js',
'type' => 'js',
'requires' =>
array(
0 => 'javelin-behavior',
1 => 'javelin-dom',
2 => 'phabricator-drag-and-drop-file-upload',
),
'disk' => '/rsrc/js/application/maniphest/behavior-transaction-drag-and-drop.js',
),
'javelin-behavior-maniphest-transaction-expand' =>
array(
'uri' => '/res/966410de/rsrc/js/application/maniphest/behavior-transaction-expand.js',
@ -671,16 +691,6 @@ celerity_register_resource_map(array(
),
'disk' => '/rsrc/js/javelin/lib/control/typeahead/Typeahead.js',
),
0 =>
array(
'uri' => '/res/39de799e/rsrc/js/javelin/docs/Base.js',
'type' => 'js',
'requires' =>
array(
0 => 'javelin-install',
),
'disk' => '/rsrc/js/javelin/docs/Base.js',
),
'javelin-typeahead-normalizer' =>
array(
'uri' => '/res/a5d60e3c/rsrc/js/javelin/lib/control/typeahead/normalizer/TypeaheadNormalizer.js',
@ -888,7 +898,7 @@ celerity_register_resource_map(array(
),
'phabricator-drag-and-drop-file-upload' =>
array(
'uri' => '/res/711efc61/rsrc/js/application/core/DragAndDropFileUpload.js',
'uri' => '/res/63a06ad9/rsrc/js/application/core/DragAndDropFileUpload.js',
'type' => 'js',
'requires' =>
array(
@ -1025,7 +1035,26 @@ celerity_register_resource_map(array(
'uri' => '/res/pkg/33f413ef/typeahead.pkg.js',
'type' => 'js',
),
'ac3f56cc' =>
'd985d27a' =>
array (
'name' => 'javelin.pkg.js',
'symbols' =>
array (
0 => 'javelin-util',
1 => 'javelin-install',
2 => 'javelin-event',
3 => 'javelin-stratcom',
4 => 'javelin-behavior',
5 => 'javelin-request',
6 => 'javelin-vector',
7 => 'javelin-dom',
8 => 'javelin-json',
9 => 'javelin-uri',
),
'uri' => '/res/pkg/d985d27a/javelin.pkg.js',
'type' => 'js',
),
'e8e3f8ab' =>
array (
'name' => 'core.pkg.css',
'symbols' =>
@ -1046,28 +1075,9 @@ celerity_register_resource_map(array(
13 => 'phabricator-remarkup-css',
14 => 'syntax-highlighting-css',
),
'uri' => '/res/pkg/ac3f56cc/core.pkg.css',
'uri' => '/res/pkg/e8e3f8ab/core.pkg.css',
'type' => 'css',
),
'd985d27a' =>
array (
'name' => 'javelin.pkg.js',
'symbols' =>
array (
0 => 'javelin-util',
1 => 'javelin-install',
2 => 'javelin-event',
3 => 'javelin-stratcom',
4 => 'javelin-behavior',
5 => 'javelin-request',
6 => 'javelin-vector',
7 => 'javelin-dom',
8 => 'javelin-json',
9 => 'javelin-uri',
),
'uri' => '/res/pkg/d985d27a/javelin.pkg.js',
'type' => 'js',
),
'ed383f69' =>
array (
'name' => 'differential.pkg.js',
@ -1085,15 +1095,15 @@ celerity_register_resource_map(array(
),
'reverse' =>
array (
'aphront-crumbs-view-css' => 'ac3f56cc',
'aphront-dialog-view-css' => 'ac3f56cc',
'aphront-form-view-css' => 'ac3f56cc',
'aphront-list-filter-view-css' => 'ac3f56cc',
'aphront-panel-view-css' => 'ac3f56cc',
'aphront-side-nav-view-css' => 'ac3f56cc',
'aphront-table-view-css' => 'ac3f56cc',
'aphront-tokenizer-control-css' => 'ac3f56cc',
'aphront-typeahead-control-css' => 'ac3f56cc',
'aphront-crumbs-view-css' => 'e8e3f8ab',
'aphront-dialog-view-css' => 'e8e3f8ab',
'aphront-form-view-css' => 'e8e3f8ab',
'aphront-list-filter-view-css' => 'e8e3f8ab',
'aphront-panel-view-css' => 'e8e3f8ab',
'aphront-side-nav-view-css' => 'e8e3f8ab',
'aphront-table-view-css' => 'e8e3f8ab',
'aphront-tokenizer-control-css' => 'e8e3f8ab',
'aphront-typeahead-control-css' => 'e8e3f8ab',
'differential-changeset-view-css' => '1ac25e8a',
'differential-core-view-css' => '1ac25e8a',
'differential-revision-add-comment-css' => '1ac25e8a',
@ -1128,11 +1138,11 @@ celerity_register_resource_map(array(
'javelin-util' => 'd985d27a',
'javelin-vector' => 'd985d27a',
'javelin-workflow' => '122a6b6d',
'phabricator-core-buttons-css' => 'ac3f56cc',
'phabricator-core-css' => 'ac3f56cc',
'phabricator-directory-css' => 'ac3f56cc',
'phabricator-remarkup-css' => 'ac3f56cc',
'phabricator-standard-page-view' => 'ac3f56cc',
'syntax-highlighting-css' => 'ac3f56cc',
'phabricator-core-buttons-css' => 'e8e3f8ab',
'phabricator-core-css' => 'e8e3f8ab',
'phabricator-directory-css' => 'e8e3f8ab',
'phabricator-remarkup-css' => 'e8e3f8ab',
'phabricator-standard-page-view' => 'e8e3f8ab',
'syntax-highlighting-css' => 'e8e3f8ab',
),
));

View file

@ -13,6 +13,7 @@ phutil_register_library_map(array(
'Aphront404Response' => 'aphront/response/404',
'AphrontAjaxResponse' => 'aphront/response/ajax',
'AphrontApplicationConfiguration' => 'aphront/applicationconfiguration',
'AphrontAttachedFileView' => 'view/control/attachedfile',
'AphrontController' => 'aphront/controller',
'AphrontCrumbsView' => 'view/layout/crumbs',
'AphrontDatabaseConnection' => 'storage/connection/base',
@ -26,6 +27,7 @@ phutil_register_library_map(array(
'AphrontFormCheckboxControl' => 'view/form/control/checkbox',
'AphrontFormControl' => 'view/form/control/base',
'AphrontFormDividerControl' => 'view/form/control/divider',
'AphrontFormDragAndDropUploadControl' => 'view/form/control/draganddropupload',
'AphrontFormFileControl' => 'view/form/control/file',
'AphrontFormMarkupControl' => 'view/form/control/markup',
'AphrontFormPasswordControl' => 'view/form/control/password',
@ -528,6 +530,7 @@ phutil_register_library_map(array(
'Aphront400Response' => 'AphrontResponse',
'Aphront404Response' => 'AphrontResponse',
'AphrontAjaxResponse' => 'AphrontResponse',
'AphrontAttachedFileView' => 'AphrontView',
'AphrontCrumbsView' => 'AphrontView',
'AphrontDefaultApplicationConfiguration' => 'AphrontApplicationConfiguration',
'AphrontDefaultApplicationController' => 'AphrontController',
@ -538,6 +541,7 @@ phutil_register_library_map(array(
'AphrontFormCheckboxControl' => 'AphrontFormControl',
'AphrontFormControl' => 'AphrontView',
'AphrontFormDividerControl' => 'AphrontFormControl',
'AphrontFormDragAndDropUploadControl' => 'AphrontFormControl',
'AphrontFormFileControl' => 'AphrontFormControl',
'AphrontFormMarkupControl' => 'AphrontFormControl',
'AphrontFormPasswordControl' => 'AphrontFormControl',

View file

@ -30,12 +30,13 @@ class PhabricatorFileDropUploadController extends PhabricatorFileController {
'name' => $request->getStr('name'),
));
$view = new AphrontAttachedFileView();
$view->setFile($file);
return id(new AphrontAjaxResponse())->setContent(
array(
'name' => $file->getName(),
'phid' => $file->getPHID(),
'size' => $file->getByteSize(),
'uri' => $file->getViewURI(),
'html' => $view->render(),
));
}

View file

@ -9,6 +9,7 @@
phutil_require_module('phabricator', 'aphront/response/ajax');
phutil_require_module('phabricator', 'applications/files/controller/base');
phutil_require_module('phabricator', 'applications/files/storage/file');
phutil_require_module('phabricator', 'view/control/attachedfile');
phutil_require_module('phutil', 'utils');

View file

@ -202,6 +202,10 @@ class PhabricatorFile extends PhabricatorFileDAO {
return PhabricatorFileURI::getViewURIForPHID($this->getPHID());
}
public function getThumb60x45URI() {
return '/file/xform/thumb-60x45/'.$this->getPHID().'/';
}
public function isViewableInBrowser() {
return ($this->getViewableMimeType() !== null);
}

View file

@ -222,6 +222,8 @@ class ManiphestTaskDetailController extends ManiphestController {
$draft_text = null;
}
$panel_id = celerity_generate_unique_node_id();
$comment_form = new AphrontFormView();
$comment_form
->setUser($user)
@ -286,12 +288,11 @@ class ManiphestTaskDetailController extends ManiphestController {
->setValue($draft_text)
->setID('transaction-comments'))
->appendChild(
id(new AphrontFormMarkupControl())
id(new AphrontFormDragAndDropUploadControl())
->setLabel('Attached Files')
->setValue(
'<div id="file-list">'.
'None'.
'</div>'))
->setName('files')
->setDragAndDropTarget($panel_id)
->setActivatedClass('aphront-panel-view-drag-and-drop'))
->appendChild(
id(new AphrontFormSubmitControl())
->setValue('Avast!'));
@ -335,22 +336,11 @@ class ManiphestTaskDetailController extends ManiphestController {
'map' => $control_map,
));
$id = celerity_generate_unique_node_id();
$comment_panel = new AphrontPanelView();
$comment_panel->appendChild($comment_form);
$comment_panel->setID($id);
$comment_panel->setID($panel_id);
$comment_panel->addClass('aphront-panel-accent');
$comment_panel->setHeader('Leap Into Action');
Javelin::initBehavior(
'maniphest-transaction-drag-and-drop',
array(
'target' => $id,
'activatedClass' => 'aphront-panel-view-drag-and-drop',
'uri' => '/file/dropupload/',
'list' => 'file-list',
));
$comment_panel->setHeader('Weigh In');
$preview_panel =
'<div class="aphront-panel-preview">

View file

@ -21,8 +21,8 @@ phutil_require_module('phabricator', 'applications/phid/handle/data');
phutil_require_module('phabricator', 'infrastructure/celerity/api');
phutil_require_module('phabricator', 'infrastructure/javelin/api');
phutil_require_module('phabricator', 'view/form/base');
phutil_require_module('phabricator', 'view/form/control/draganddropupload');
phutil_require_module('phabricator', 'view/form/control/file');
phutil_require_module('phabricator', 'view/form/control/markup');
phutil_require_module('phabricator', 'view/form/control/select');
phutil_require_module('phabricator', 'view/form/control/submit');
phutil_require_module('phabricator', 'view/form/control/textarea');

View file

@ -0,0 +1,73 @@
<?php
/*
* Copyright 2011 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
final class AphrontAttachedFileView extends AphrontView {
private $file;
public function setFile(PhabricatorFile $file) {
$this->file = $file;
return $this;
}
public function render() {
require_celerity_resource('aphront-attached-file-view-css');
$file = $this->file;
$phid = $file->getPHID();
$thumb = phutil_render_tag(
'img',
array(
'src' => $file->getThumb60x45URI(),
'width' => 60,
'height' => 45,
));
$name = phutil_render_tag(
'a',
array(
'href' => $file->getViewURI(),
'target' => '_blank',
),
phutil_escape_html($file->getName()));
$size = number_format($file->getByteSize()).' bytes';
$remove = javelin_render_tag(
'a',
array(
'class' => 'button grey',
'sigil' => 'aphront-attached-file-view-remove',
// NOTE: Using 'ref' here instead of 'meta' because the file upload
// endpoint doesn't receive request metadata and thus can't generate
// a valid response with node metadata.
'ref' => $file->getPHID(),
),
"\xE2\x9C\x96"); // "Heavy Multiplication X"
return
'<table class="aphront-attached-file-view">
<tr>
<td>'.$thumb.'</td>
<th><strong>'.$name.'</strong><br />'.$size.'</th>
<td class="aphront-attached-file-view-remove">'.$remove.'</td>
</tr>
</table>';
}
}

View file

@ -0,0 +1,16 @@
<?php
/**
* This file is automatically generated. Lint this module to rebuild it.
* @generated
*/
phutil_require_module('phabricator', 'infrastructure/celerity/api');
phutil_require_module('phabricator', 'infrastructure/javelin/markup');
phutil_require_module('phabricator', 'view/base');
phutil_require_module('phutil', 'markup');
phutil_require_source('AphrontAttachedFileView.php');

View file

@ -0,0 +1,80 @@
<?php
/*
* Copyright 2011 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
class AphrontFormDragAndDropUploadControl extends AphrontFormControl {
private $dragAndDropTarget;
private $activatedClass;
public function __construct() {
$this->setControlID(celerity_generate_unique_node_id());
$this->setControlStyle('display: none;');
}
protected function getCustomControlClass() {
return 'aphront-form-drag-and-drop-upload';
}
public function setDragAndDropTarget($id) {
$this->dragAndDropTarget = $id;
return $this;
}
public function setActivatedClass($class) {
$this->activatedClass = $class;
return $this;
}
protected function renderInput() {
require_celerity_resource('aphront-attached-file-view-css');
$list_id = celerity_generate_unique_node_id();
$files = $this->getValue();
$value = array();
foreach ($files as $file) {
$view = new AphrontAttachedFileView();
$view->setFile($file);
$value[$file->getPHID()] = array(
'phid' => $file->getPHID(),
'html' => $view->render(),
);
}
Javelin::initBehavior(
'aphront-drag-and-drop',
array(
'control' => $this->getControlID(),
'name' => $this->getName(),
'value' => nonempty($value, null),
'list' => $list_id,
'uri' => '/file/dropupload/',
'target' => $this->dragAndDropTarget,
'activatedClass' => $this->activatedClass,
));
return phutil_render_tag(
'div',
array(
'id' => $list_id,
'class' => 'aphront-form-drag-and-drop-file-list',
),
'');
}
}

View file

@ -0,0 +1,18 @@
<?php
/**
* This file is automatically generated. Lint this module to rebuild it.
* @generated
*/
phutil_require_module('phabricator', 'infrastructure/celerity/api');
phutil_require_module('phabricator', 'infrastructure/javelin/api');
phutil_require_module('phabricator', 'view/control/attachedfile');
phutil_require_module('phabricator', 'view/form/control/base');
phutil_require_module('phutil', 'markup');
phutil_require_module('phutil', 'utils');
phutil_require_source('AphrontFormDragAndDropUploadControl.php');

View file

@ -0,0 +1,24 @@
/**
* @provides aphront-attached-file-view-css
*/
.aphront-attached-file-view {
border: 1px solid #aaaaaa;
background: #f9f9f9;
width: 100%;
margin: 4px 0;
}
.aphront-attached-file-view td,
.aphront-attached-file-view th {
padding: 4px;
}
.aphront-attached-file-view th {
width: 100%;
font-size: 11px;
}
.aphront-attached-file-view-remove {
vertical-align: middle;
}

View file

@ -116,3 +116,13 @@ table.aphront-form-control-checkbox-layout th {
background: #f3f3f3;
border: 1px solid #afafaf;
}
.aphront-form-drag-and-drop-file-list {
width: 400px;
}
.drag-and-drop-instructions {
color: #333333;
font-size: 11px;
padding: 6px 8px;
}

View file

@ -16,6 +16,14 @@ JX.install('PhabricatorDragAndDropFileUpload', {
events : ['willUpload', 'didUpload'],
statics : {
isSupported : function() {
// TODO: Is there a better capability test for this? This seems okay in
// Safari, Firefox and Chrome.
return !!window.FileList;
}
},
members : {
_node : null,
_depth : 0,

View file

@ -0,0 +1,92 @@
/**
* @provides javelin-behavior-aphront-drag-and-drop
* @requires javelin-behavior
* javelin-dom
* javelin-util
* phabricator-drag-and-drop-file-upload
*/
JX.behavior('aphront-drag-and-drop', function(config) {
// The control renders hidden by default; if we don't have support for
// drag-and-drop just leave it hidden.
if (!JX.PhabricatorDragAndDropFileUpload.isSupported()) {
return;
}
// Show the control, since we have browser support.
JX.$(config.control).style.display = '';
var files = config.value || {};
var pending = 0;
var list = JX.$(config.list);
var drop = new JX.PhabricatorDragAndDropFileUpload(JX.$(config.target))
.setActivatedClass(config.activatedClass)
.setURI(config.uri);
drop.listen('willUpload', function(f) {
pending++;
redraw();
});
drop.listen('didUpload', function(f) {
files[f.phid] = f;
// This redraws "Upload complete!"
pending--;
redraw(true);
// This redraws the instructions.
JX.defer(redraw, 1000);
});
drop.start();
redraw();
JX.DOM.listen(
list,
'click',
'aphront-attached-file-view-remove',
function(e) {
e.kill();
delete files[e.getTarget().getAttribute('ref')];
redraw();
});
function redraw(completed) {
var items = [];
for (var k in files) {
var file = files[k];
items.push(JX.$N('div', {}, JX.$H(file.html)));
items.push(JX.$N(
'input',
{
type: "hidden",
name: config.name + "[" + file.phid + "]",
value: file.phid
}));
}
var status;
if (!pending) {
if (completed) {
status = JX.$H('<strong>Upload complete!</strong>');
} else {
arrow = String.fromCharCode(0x21EA);
status = JX.$H(
arrow + ' <strong>Drag and Drop</strong> files here to upload them.');
}
} else {
status = JX.$H(
'Uploading <strong>' + parseInt(pending, 10) + '<strong> files...');
}
status = JX.$N('div', {className: 'drag-and-drop-instructions'}, status);
items.push(status);
JX.DOM.setContent(list, items);
}
});

View file

@ -1,39 +0,0 @@
/**
* @provides javelin-behavior-maniphest-transaction-drag-and-drop
* @requires javelin-behavior
* javelin-dom
* phabricator-drag-and-drop-file-upload
*/
JX.behavior('maniphest-transaction-drag-and-drop', function(config) {
var files = [];
var drop = new JX.PhabricatorDragAndDropFileUpload(JX.$(config.target))
.setActivatedClass(config.activatedClass)
.setURI(config.uri);
drop.listen('didUpload', function(f) {
files.push(f);
redraw();
});
drop.start();
function redraw() {
var items = [];
for (var ii = 0; ii < files.length; ii++) {
items.push(JX.$N('div', {}, files[ii].name));
items.push(JX.$N(
'input',
{
type: "hidden",
name: "files[" + files[ii].phid + "]",
value: files[ii].phid
}));
}
JX.DOM.setContent(JX.$(config.list), items);
}
});