elofgren: USABILITY: Fix usability issue #48 "Delete and Move Files buttons need repositioning"

Changes: 
* Remove the 'Move Files' button and enable multi-file drag and drop file moving 
* Position the 'Delete selected files' button under the checkboxes and make it turn red on hover 
* Add 'Ctrl' and 'Shift' range selection to file checkboxes 
* Add 'X' delete links to the right of each file 
* Rename the 'Save' button to 'Save folder name' and don't show it when listing 'assets' folder since it can't be renamed 
More info: http://www.silverstripe.com/google-summer-of-code-forum/flat/1607
(merged from branches/gsoc)


git-svn-id: svn://svn.silverstripe.com/silverstripe/open/modules/cms/trunk@41927 467b73ca-7a2a-4603-9d3b-597d59a354a9
This commit is contained in:
Ingo Schommer 2007-09-15 20:47:35 +00:00
parent 52f8e607c9
commit 3cae2fb249
6 changed files with 285 additions and 84 deletions

View File

@ -231,12 +231,22 @@ HTML;
if($record) {
$nameField = ($id != "root") ? new TextField("Name", "Folder Name") : new HiddenField("Name");
if( $record->userCanEdit() ) {
$deleteButton = new InlineFormAction('deletemarked',"Delete selected files", 'delete');
$deleteButton->includeDefaultJS(false);
} else {
$deleteButton = new HiddenField('deletemarked');
}
$fields = new FieldSet(
new HiddenField("Title"),
new TabSet("Root",
new Tab("Files",
$nameField,
$fileList
$fileList,
$deleteButton,
new HiddenField("FileIDs"),
new HiddenField("DestFolderID")
),
new Tab("Details",
new ReadonlyField("URL"),
@ -255,11 +265,10 @@ HTML;
$actions = new FieldSet();
if( $record->userCanEdit() ) {
// Only show save button if not 'assets' folder
if( $record->userCanEdit() && $id != "root") {
$actions = new FieldSet(
new FormAction('deletemarked',"Delete files"),
new FormAction('movemarked',"Move files..."),
new FormAction('save',"Save")
new FormAction('save',"Save folder name")
);
}
@ -288,28 +297,11 @@ HTML;
}
}
/**
* Returns the form used to specify options for the "move marked" action.
*/
public function MoveMarkedOptionsForm() {
$folderDropdown = new TreeDropdownField("DestFolderID", "Move files to", "Folder");
$folderDropdown->setFilterFunction(create_function('$obj', 'return $obj->class == "Folder";'));
return new CMSActionOptionsForm($this, "MoveMarkedOptionsForm", new FieldSet(
new HiddenField("ID"),
new HiddenField("FileIDs"),
$folderDropdown
),
new FieldSet(
new FormAction("movemarked", "Move marked files")
));
}
/**
* Perform the "move marked" action.
* Called by ajax, with a JavaScript return.
* Called and returns in same way as 'save' function
*/
public function movemarked() {
public function movemarked($urlParams, $form) {
if($_REQUEST['DestFolderID'] && is_numeric($_REQUEST['DestFolderID'])) {
$destFolderID = $_REQUEST['DestFolderID'];
$fileList = "'" . ereg_replace(' *, *',"','",trim(addslashes($_REQUEST['FileIDs']))) . "'";
@ -330,36 +322,21 @@ HTML;
user_error("No files in $fileList could be found!", E_USER_ERROR);
}
}
echo <<<JS
statusMessage("Moved $numFiles files");
JS;
$message = 'Moved '.$numFiles.' files';
FormResponse::status_message($message, "good");
FormResponse::add("$('Form_EditForm').getPageFromServer($('Form_EditForm_ID').value)");
return FormResponse::respond();
} else {
user_error("Bad data: $_REQUEST[DestFolderID]", E_USER_ERROR);
}
}
/**
* Returns the form used to specify options for the "delete marked" action.
* In actual fact, this form only has hidden fields and the button is auto-clickd without the
* form being displayed; it's just the most consistent way of providing this information to the
* CMS.
*/
public function DeleteMarkedOptionsForm() {
return new CMSActionOptionsForm($this, "DeleteMarkedOptionsForm", new FieldSet(
new HiddenField("ID"),
new HiddenField("FileIDs")
),
new FieldSet(
new FormAction("deletemarked", "Delete marked files")
));
}
/**
* Perform the "delete marked" action.
* Called by ajax, with a JavaScript return.
* Called and returns in same way as 'save' function
*/
public function deletemarked() {
public function deletemarked($urlParams, $form) {
$fileList = "'" . ereg_replace(' *, *',"','",trim(addslashes($_REQUEST['FileIDs']))) . "'";
$numFiles = 0;
$folderID = 0;
@ -403,12 +380,11 @@ JS;
user_error("No files in $fileList could be found!", E_USER_ERROR);
}
}
echo <<<JS
$deleteList
$('Form_EditForm').getPageFromServer($('Form_EditForm_ID').value);
statusMessage("Deleted $numFiles files.$brokenPageList");
JS;
$message = "Deleted $numFiles files.$brokenPageList";
FormResponse::add($deleteList);
FormResponse::status_message($message, "good");
FormResponse::add("$('Form_EditForm').getPageFromServer($('Form_EditForm_ID').value)");
return FormResponse::respond();
}

View File

@ -99,7 +99,7 @@
ul.tree span.a {
cursor: pointer;
}
ul.tree span.a.over {
.filefolderhover span.a, ul.tree span.a.over {
background-color: #FFFFBB !important;
/* these push the highlight out to the left of the window */

View File

@ -375,7 +375,7 @@ table.CMSList tbody td.current td {
.ajaxActions input:hover {
background: #fff;
}
.ajaxActions input.delete:hover {
input.delete:hover {
background: #ce0000;
color: #fff;
}
@ -793,6 +793,32 @@ div.CommentFilter {
font-style: italic;
}
/**
* AssetAdmin
*/
#right form .inlineformaction {
margin-left: 0;
}
.dragfile, .dragfile img {
float: left;
cursor: move;
}
.NumFilesIndicator {
text-align: left;
padding: 1px 1px 1px 10px;
font-size: 16px;
font-weight: bold;
border: 1px solid #cc9;
color: #660;
background-color: #F9F9E3;
margin: 2px;
}
.DraggedHandle {
width: 30em !important;
}
.ComplexTableField tbody td.markingcheckbox {
cursor: default;
}
/**
* SecurityAdmin
*/

View File

@ -39,27 +39,29 @@ function action_upload_right(e) {
return false;
}
function action_deletemarked_right() {
$('action_deletemarked_options').onComplete = function() {}
if(confirm("Do you really want to delete the marked files?")) {
$('action_deletemarked_options').send();
/**
* Set up save folder name action
*/
Behaviour.register( {
'#Form_EditForm_save': {
onclick : function() {
$('Form_EditForm').save(false, null, 'save', false);
return false;
}
}
}
function action_movemarked_right() {
$('action_movemarked_options').toggle();
}
});
MarkingPropertiesForm = Class.extend('ActionPropertiesForm');
MarkingPropertiesForm.applyTo('#action_movemarked_options', "Please select some files to move!");
MarkingPropertiesForm.applyTo('#action_deletemarked_options', "Please select some files to delete!");
MarkingPropertiesButton = Class.create();
MarkingPropertiesButton.applyTo('#Form_EditForm_deletemarked', "Please select some files to delete!", 'deletemarked', 'Do you really want to delete the marked files?');
MarkingPropertiesForm.prototype = {
initialize: function(noneCheckedError) {
MarkingPropertiesButton.prototype = {
initialize: function(noneCheckedError, action, confirmMessage) {
this.noneCheckedError = noneCheckedError;
this.action = action;
this.confirmMessage = confirmMessage;
},
send: function() {
onclick: function() {
var i, list = "", checkboxes = $('Form_EditForm').elements['Files[]'];
if(!checkboxes) checkboxes = [];
if(!checkboxes.length) checkboxes = [ checkboxes ];
@ -67,21 +69,207 @@ MarkingPropertiesForm.prototype = {
if(checkboxes[i].checked) list += (list?',':'') + checkboxes[i].value;
}
if(this.elements.DestFolderID && !this.elements.DestFolderID.value) {
alert("Please select a folder to move files to!");
return false;
} else if(list == "") {
if(list == "") {
alert(this.noneCheckedError);
return false;
} else {
this.elements.FileIDs.value = list;
return this.ActionPropertiesForm.send();
$('Form_EditForm_FileIDs').value = list;
}
// If there is a confirmation message, show it before submitting
if('' != this.confirmMessage) {
// Only submit if OK button is clicked
if (confirm(this.confirmMessage)) {
$('Form_EditForm').save(false, null, this.action);
}
} else {
$('Form_EditForm').save(false, null, this.action);
}
return false;
}
}
// CheckBoxRange adapted from: http://jroller.com/page/rmcmahon?entry=checkboxrange_with_prototype
var CheckBoxRange = Class.create();
CheckBoxRange.prototype = {
currentBox: null,
form: null,
field: null,
initialize: function(form, field) {
this.form = form;
this.field = field;
this.eventPossibleCheckHappened = this.possibleCheckHappened.bindAsEventListener(this);
Event.observe(form, "click", this.eventPossibleCheckHappened);
Event.observe(form, "keyup", this.eventPossibleCheckHappened);
},
possibleCheckHappened: function(event) {
var target = Event.element(event);
if ((event.button == 0 || event.keyCode == 32 || event.keyCode == 17) &&
this.isCheckBox(target) && target.form == $(this.form) && target.name == this.field) {
// If ctrl or shift is keys are pressed
if ((event.shiftKey || event.ctrlKey ) && this.currentBox)
this.updateCheckBoxRange(this.currentBox, target);
this.currentBox = target;
}
},
isCheckBox: function(e) {
return (e.tagName.toLowerCase() == "input" && e.type.toLowerCase() == "checkbox");
},
updateCheckBoxRange: function(start, end) {
var last_clicked = end;
var checkboxes = Form.getInputs(this.form, 'checkbox', this.field);
var checkbox;
var last;
for (var i=0; (checkbox = checkboxes[i]); ++i) {
if (checkbox == end) {
last = start;
break;
}
if (checkbox == start) {
last = end;
break;
}
}
for (; (checkbox = checkboxes[i]); ++i) {
if (checkbox != last_clicked && checkbox.checked != last_clicked.checked)
checkbox.click();
if (checkbox == last)
break;
}
}
}
// SubsDraggable adapted from http://dev.rubyonrails.org/ticket/5771
// extentions for scriptaculous dragdrop.js
Object.extend(Class, {
superrise: function(obj, names){
names.each( function(n){ obj['super_' + n] = obj[n] } )
return obj;
}
})
// Draggable that allows substitution of draggable element
var SubsDraggable = Class.create();
SubsDraggable.prototype = Object.extend({}, Draggable.prototype);
Class.superrise(SubsDraggable.prototype, ['initialize', 'startDrag', 'finishDrag'])
Object.extend( SubsDraggable.prototype , {
initialize: function(event) {
this.super_initialize.apply(this, arguments);
if( typeof(this.options.dragelement) == 'undefined' ) this.options.dragelement = false;
},
startDrag: function(event) {
if( this.options.dragelement ) {
this._originalElement = this.element;
// Get the id of the file being dragged
var beingDraggedId = this.element.id.replace('drag-Files-','');
this.element = this.options.dragelement(this.element);
Position.absolutize(this.element);
Position.clone(this._originalElement, this.element);
// Add # files being moved message
this.element.className = 'dragfile DraggedHandle';
// We are at least moving the 1 file being dragged
var numMoved = 1;
var i, checkboxes = $('Form_EditForm').elements['Files[]'];
if(!checkboxes) checkboxes = [];
if(!checkboxes.length) checkboxes = [ checkboxes ];
for(i=0;i<checkboxes.length;i++) {
// Total up the other files that are checked
if(checkboxes[i].checked && checkboxes[i].value != beingDraggedId) {
numMoved++;
}
}
numFilesIndicator = document.createElement('span');
numFilesIndicator.innerHTML = 'Moving ' + numMoved + ' files';
numFilesIndicator.className = 'NumFilesIndicator';
this.element.appendChild(numFilesIndicator);
}
this.super_startDrag(event);
},
finishDrag: function(event, success) {
this.super_finishDrag(event, success);
if(this.options.dragelement){
Element.remove(this.element);
this.element = this._originalElement;
this._originalElement = null;
}
}
})
// gets element that should be dragged instead of original element
// returned element should be added to DOM tree, and will be deleted by dragdrop library
function getDragElement(element){
var el = element.cloneNode(true);
el.id = '';
document.body.appendChild(el);
return el;
}
// Set up DRAG handle
DragFileItem = Class.create();
DragFileItem.prototype = {
initialize: function() {
if (this.id)
{
this.draggable = new SubsDraggable(this.id, {revert:true,ghosting:false,dragelement:getDragElement});
}
},
destroy: function() {
this.draggable = null;
}
}
DragFileItem.applyTo('#Form_EditForm_Files tr td.markingcheckbox div.dragfile');
// Set up folder drop target
DropFileItem = Class.create();
DropFileItem.prototype = {
initialize: function() {
// Get this.recordID from the last "-" separated chunk of the id HTML attribute
// eg: <li id="treenode-6"> would give a recordID of 6
if(this.id && this.id.match(/-([^-]+)$/))
this.recordID = RegExp.$1;
// Do not allow files to be dropped to the root
if (this.recordID != 'root') {
this.droppable = Droppables.add(this.id, {accept:'dragfile', hoverclass:'filefolderhover',
onDrop:function(droppedElement) {
// Get this.recordID from the last "-" separated chunk of the id HTML attribute
// eg: <li id="treenode-6"> would give a recordID of 6
if(this.element.id && this.element.id.match(/-([^-]+)$/))
this.recordID = RegExp.$1;
$('Form_EditForm').elements['DestFolderID'].value = this.recordID;
// Add the dropped file to the list of files to move
var list = droppedElement.getElementsByTagName('img')[0].id.replace('drag-img-Files-','');
var i, checkboxes = $('Form_EditForm').elements['Files[]'];
if(!checkboxes) checkboxes = [];
if(!checkboxes.length) checkboxes = [ checkboxes ];
// Add each checked file to the list of ones to move
for(i=0;i<checkboxes.length;i++) {
if(checkboxes[i].checked) list += (list?',':'') + checkboxes[i].value;
}
$('Form_EditForm_FileIDs').value = list;
$('Form_EditForm').save(false, null, 'movemarked')
}
});
}
},
destroy: function() {
this.droppable = null;
this.recordID = null;
}
}
DropFileItem.applyTo('#sitetree li');
/**
* Add File Action
@ -119,13 +307,17 @@ addfolder.prototype = {
$('addpage_options').elements.ParentID.value = st.getIdxOf(st.firstSelected());
Ajax.SubmitForm('addpage_options', null, {
onSuccess : Ajax.Evaluator,
onSuccess : this.onSuccess,
onFailure : this.showAddPageError
});
return false;
},
onSuccess: function(response) {
Ajax.Evaluator(response);
// Make it possible to drop files into the new folder
DropFileItem.applyTo('#sitetree li');
console.log('should now be able to drop into new folder');
},
showAddPageError: function(response) {
errorMessage('Error adding folder', response);
}

View File

@ -3,8 +3,6 @@
<div id="form_actions_right" class="ajaxActions">
</div>
$MoveMarkedOptionsForm
$DeleteMarkedOptionsForm
<!--
<form class="actionparams" id="action_movemarked_options" style="display:none" action="admin/assets/movemarked">
<fieldset>

View File

@ -3,25 +3,34 @@
<table class="data">
<thead>
<tr>
<% if Markable %><th width="18">&nbsp;</th><% end_if %>
<% if Markable %><th width="36">&nbsp;</th><% end_if %>
<% control Headings %>
<th class="$Name">$Title</th>
<% end_control %>
<th width="18">&nbsp;</th>
<% if Can(delete) %><th width="18">&nbsp;</th><% end_if %>
</tr>
</thead>
<tbody>
<% control Items %>
<tr id="record-$Parent.Name-$ID">
<% if Markable %><td width="18" class="markingcheckbox">$MarkingCheckbox</td><% end_if %>
<% if Markable %><td width="36" class="markingcheckbox"><div class="dragfile" id="drag-$Parent.Name-$ID"><img id="drag-img-$Parent.Name-$ID" lt="Drag to folder on left to move file" title="Drag to folder on left to move file" src="sapphire/images/drag.gif" /></div> $MarkingCheckbox</td><% end_if %>
<% control Fields %>
<td>$Value</td>
<% end_control %>
<td width="18">
<a class="popuplink editlink" href="$EditLink" target="_blank" title="Edit asset"><img src="cms/images/edit.gif" alt="edit" /></a>
</td>
<% if Can(delete) %>
<td width="18">
<a class="deletelink" href="$DeleteLink" title="Delete this file"><img src="cms/images/delete.gif" alt="delete" /></a>
</td>
<% end_if %>
</tr>
<% end_control %>
</tbody>
</table>
</div>
</div>
<script type="text/javascript">
new CheckBoxRange(document.getElementById('Form_EditForm'), 'Files[]');
</script>