Combine upload action into the "New" menutags/v8.2beta1
@@ -18,11 +18,6 @@ | |||
z-index: -30; | |||
} | |||
#new { | |||
z-index: 1010; | |||
float: left; | |||
padding: 0 !important; /* override default control bar button padding */ | |||
} | |||
#trash { | |||
margin-right: 8px; | |||
float: right; | |||
@@ -30,49 +25,8 @@ | |||
padding: 10px; | |||
font-weight: normal; | |||
} | |||
#new > a { | |||
padding: 14px 10px; | |||
position: relative; | |||
top: 7px; | |||
} | |||
#new.active { | |||
border-bottom-left-radius: 0; | |||
border-bottom-right-radius: 0; | |||
border-bottom: none; | |||
background: #f8f8f8; | |||
} | |||
#new > ul { | |||
display: none; | |||
position: fixed; | |||
min-width: 112px; | |||
z-index: -10; | |||
padding: 8px; | |||
padding-bottom: 0; | |||
margin-top: 13.5px; | |||
margin-left: -1px; | |||
text-align: left; | |||
background: #f8f8f8; | |||
border: 1px solid #ddd; | |||
border: 1px solid rgba(240, 240, 240, 0.9); | |||
border-radius: 5px; | |||
border-top-left-radius: 0; | |||
box-shadow: 0 2px 7px rgba(170,170,170,.4); | |||
} | |||
#new > ul > li { | |||
height: 36px; | |||
margin: 5px; | |||
padding-left: 42px; | |||
padding-bottom: 2px; | |||
background-position: left center; | |||
cursor: pointer; | |||
} | |||
#new > ul > li > p { | |||
cursor: pointer; | |||
padding-top: 7px; | |||
padding-bottom: 7px; | |||
} | |||
#new .error, #fileList .error { | |||
.newFileMenu .error, #fileList .error { | |||
color: #e9322d; | |||
border-color: #e9322d; | |||
-webkit-box-shadow: 0 0 6px #f8b9b7; | |||
@@ -144,6 +98,30 @@ | |||
margin-bottom: 44px; | |||
} | |||
#app-navigation .nav-files a.nav-icon-files { | |||
width: auto; | |||
} | |||
/* button needs overrides due to navigation styles */ | |||
#app-navigation .nav-files a.new { | |||
width: 40px; | |||
height: 32px; | |||
padding: 0 10px; | |||
margin: 0; | |||
cursor: pointer; | |||
} | |||
#app-navigation .nav-files a { | |||
display: inline-block; | |||
} | |||
#app-navigation .nav-files a.new.hidden { | |||
display: none; | |||
} | |||
#app-navigation .nav-files a.new.disabled { | |||
opacity: 0.3; | |||
} | |||
#filestable tbody tr { | |||
background-color: #fff; | |||
height: 40px; | |||
@@ -600,7 +578,8 @@ a.action > img { | |||
#fileList a.action.action-menu { | |||
padding: 17px 14px; | |||
} | |||
#fileList .fileActionsMenu { | |||
#fileList .popovermenu { | |||
margin-right: 21px; | |||
} | |||
@@ -655,13 +634,13 @@ a.action > img { | |||
} | |||
/* properly display actions in the popover menu */ | |||
#fileList .fileActionsMenu .action { | |||
#fileList .popovermenu .action { | |||
-ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=50)" !important; | |||
filter: alpha(opacity=50) !important; | |||
opacity: .5 !important; | |||
} | |||
#fileList .fileActionsMenu .action:hover, | |||
#fileList .fileActionsMenu .action:focus { | |||
#fileList .popovermenu .action:hover, | |||
#fileList .popovermenu .action:focus { | |||
-ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=100)" !important; | |||
filter: alpha(opacity=100) !important; | |||
opacity: 1 !important; | |||
@@ -745,43 +724,56 @@ table.dragshadow td.size { | |||
opacity: 0; | |||
} | |||
.fileActionsMenu { | |||
padding: 4px 12px; | |||
} | |||
.fileActionsMenu li { | |||
padding: 5px 0; | |||
} | |||
#fileList .fileActionsMenu a.action img { | |||
#fileList .popovermenu a.action img { | |||
padding: initial; | |||
} | |||
#fileList .fileActionsMenu a.action { | |||
#fileList .popovermenu a.action { | |||
padding: 10px; | |||
margin: -10px; | |||
} | |||
.fileActionsMenu.hidden { | |||
display: none; | |||
.newFileMenu { | |||
width: 140px; | |||
margin-left: -56px; | |||
margin-top: 25px; | |||
} | |||
#fileList .fileActionsMenu .action { | |||
display: block; | |||
line-height: 30px; | |||
padding-left: 5px; | |||
color: #000; | |||
.newFileMenu .menuitem { | |||
white-space: nowrap; | |||
overflow: hidden; | |||
} | |||
.newFileMenu.popovermenu .menuitem .icon { | |||
margin-bottom: -2px; | |||
} | |||
.newFileMenu.popovermenu a.menuitem, | |||
.newFileMenu.popovermenu label.menuitem, | |||
.newFileMenu.popovermenu .menuitem { | |||
padding: 0; | |||
margin: 0; | |||
} | |||
.newFileMenu.bubble:after { | |||
left: 75px; | |||
right: auto; | |||
} | |||
.newFileMenu.bubble:before { | |||
left: 75px; | |||
right: auto; | |||
} | |||
.fileActionsMenu .action img, | |||
.fileActionsMenu .action .no-icon { | |||
.newFileMenu .filenameform { | |||
display: inline-block; | |||
width: 16px; | |||
margin-right: 5px; | |||
} | |||
.fileActionsMenu .action { | |||
opacity: 0.5; | |||
.newFileMenu .filenameform input { | |||
width: 100px; | |||
} | |||
.fileActionsMenu li:hover .action { | |||
opacity: 1; | |||
#fileList .popovermenu .action { | |||
display: block; | |||
line-height: 30px; | |||
padding-left: 5px; | |||
color: #000; | |||
padding: 0; | |||
} |
@@ -24,20 +24,6 @@ | |||
} | |||
.file_upload_target { display:none; } | |||
.file_upload_form { display:inline; float:left; margin:0; padding:0; cursor:pointer; overflow:visible; } | |||
#file_upload_start { | |||
position: relative; | |||
left: 0; | |||
top: 0; | |||
width: 44px; | |||
height: 44px; | |||
margin: -5px -3px; | |||
padding: 0; | |||
font-size: 16px; | |||
-ms-filter:"progid:DXImageTransform.Microsoft.Alpha(Opacity=0)"; filter:alpha(opacity=0); opacity:0; | |||
z-index: 20; | |||
cursor: pointer; | |||
overflow: hidden; | |||
} | |||
#uploadprogresswrapper, #uploadprogresswrapper * { | |||
-moz-box-sizing: border-box; |
@@ -38,6 +38,7 @@ OCP\Util::addStyle('files', 'upload'); | |||
OCP\Util::addStyle('files', 'mobile'); | |||
OCP\Util::addscript('files', 'app'); | |||
OCP\Util::addscript('files', 'file-upload'); | |||
OCP\Util::addscript('files', 'newfilemenu'); | |||
OCP\Util::addscript('files', 'jquery.iframe-transport'); | |||
OCP\Util::addscript('files', 'jquery.fileupload'); | |||
OCP\Util::addscript('files', 'jquery-visibility'); |
@@ -551,155 +551,6 @@ OC.Upload = { | |||
$('#file_upload_start').attr('multiple', 'multiple'); | |||
} | |||
$(document).click(function(ev) { | |||
// do not close when clicking in the dropdown | |||
if ($(ev.target).closest('#new').length){ | |||
return; | |||
} | |||
$('#new>ul').hide(); | |||
$('#new').removeClass('active'); | |||
if ($('#new .error').length > 0) { | |||
$('#new .error').tipsy('hide'); | |||
} | |||
$('#new li').each(function(i,element) { | |||
if ($(element).children('p').length === 0) { | |||
$(element).children('form').remove(); | |||
$(element).append('<p>' + $(element).data('text') + '</p>'); | |||
} | |||
}); | |||
}); | |||
$('#new').click(function(event) { | |||
event.stopPropagation(); | |||
}); | |||
$('#new>a').click(function() { | |||
$('#new>ul').toggle(); | |||
$('#new').toggleClass('active'); | |||
}); | |||
$('#new li').click(function() { | |||
if ($(this).children('p').length === 0) { | |||
return; | |||
} | |||
$('#new .error').tipsy('hide'); | |||
$('#new li').each(function(i, element) { | |||
if ($(element).children('p').length === 0) { | |||
$(element).children('form').remove(); | |||
$(element).append('<p>' + $(element).data('text') + '</p>'); | |||
} | |||
}); | |||
var type = $(this).data('type'); | |||
var text = $(this).children('p').text(); | |||
$(this).data('text', text); | |||
$(this).children('p').remove(); | |||
// add input field | |||
var form = $('<form></form>'); | |||
var input = $('<input type="text">'); | |||
var newName = $(this).attr('data-newname') || ''; | |||
var fileType = 'input-' + $(this).attr('data-type'); | |||
if (newName) { | |||
input.val(newName); | |||
input.attr('id', fileType); | |||
} | |||
var label = $('<label class="hidden-visually" for="">' + escapeHTML(newName) + '</label>'); | |||
label.attr('for', fileType); | |||
form.append(label).append(input); | |||
$(this).append(form); | |||
var lastPos; | |||
var checkInput = function () { | |||
var filename = input.val(); | |||
if (!Files.isFileNameValid(filename)) { | |||
// Files.isFileNameValid(filename) throws an exception itself | |||
} else if (FileList.inList(filename)) { | |||
throw t('files', '{new_name} already exists', {new_name: filename}); | |||
} else { | |||
return true; | |||
} | |||
}; | |||
// verify filename on typing | |||
input.keyup(function(event) { | |||
try { | |||
checkInput(); | |||
input.tipsy('hide'); | |||
input.removeClass('error'); | |||
} catch (error) { | |||
input.attr('title', error); | |||
input.tipsy({gravity: 'w', trigger: 'manual'}); | |||
input.tipsy('show'); | |||
input.addClass('error'); | |||
} | |||
}); | |||
input.focus(); | |||
// pre select name up to the extension | |||
lastPos = newName.lastIndexOf('.'); | |||
if (lastPos === -1) { | |||
lastPos = newName.length; | |||
} | |||
input.selectRange(0, lastPos); | |||
form.submit(function(event) { | |||
event.stopPropagation(); | |||
event.preventDefault(); | |||
try { | |||
checkInput(); | |||
var newname = input.val(); | |||
if (FileList.lastAction) { | |||
FileList.lastAction(); | |||
} | |||
var name = FileList.getUniqueName(newname); | |||
switch(type) { | |||
case 'file': | |||
$.post( | |||
OC.filePath('files', 'ajax', 'newfile.php'), | |||
{ | |||
dir: FileList.getCurrentDirectory(), | |||
filename: name | |||
}, | |||
function(result) { | |||
if (result.status === 'success') { | |||
FileList.add(result.data, {animate: true, scrollTo: true}); | |||
} else { | |||
OC.dialogs.alert(result.data.message, t('core', 'Could not create file')); | |||
} | |||
} | |||
); | |||
break; | |||
case 'folder': | |||
$.post( | |||
OC.filePath('files','ajax','newfolder.php'), | |||
{ | |||
dir: FileList.getCurrentDirectory(), | |||
foldername: name | |||
}, | |||
function(result) { | |||
if (result.status === 'success') { | |||
FileList.add(result.data, {animate: true, scrollTo: true}); | |||
} else { | |||
OC.dialogs.alert(result.data.message, t('core', 'Could not create folder')); | |||
} | |||
} | |||
); | |||
break; | |||
} | |||
var li = form.parent(); | |||
form.remove(); | |||
/* workaround for IE 9&10 click event trap, 2 lines: */ | |||
$('input').first().focus(); | |||
$('#content').focus(); | |||
li.append('<p>' + li.data('text') + '</p>'); | |||
$('#new>a').click(); | |||
} catch (error) { | |||
input.attr('title', error); | |||
input.tipsy({gravity: 'w', trigger: 'manual'}); | |||
input.tipsy('show'); | |||
input.addClass('error'); | |||
} | |||
}); | |||
}); | |||
window.file_upload_param = file_upload_param; | |||
return file_upload_param; | |||
} |
@@ -14,7 +14,7 @@ | |||
'<ul>' + | |||
'{{#each items}}' + | |||
'<li>' + | |||
'<a href="#" class="action action-{{nameLowerCase}} permanent" data-action="{{name}}">{{#if icon}}<img src="{{icon}}"/>{{else}}<span class="no-icon"></span>{{/if}}<span>{{displayName}}</span></a>' + | |||
'<a href="#" class="menuitem action action-{{nameLowerCase}} permanent" data-action="{{name}}">{{#if icon}}<img class="icon" src="{{icon}}"/>{{else}}<span class="no-icon"></span>{{/if}}<span>{{displayName}}</span></a>' + | |||
'</li>' + | |||
'{{/each}}' + | |||
'</ul>'; | |||
@@ -26,7 +26,7 @@ | |||
*/ | |||
var FileActionsMenu = OC.Backbone.View.extend({ | |||
tagName: 'div', | |||
className: 'fileActionsMenu bubble hidden open menu', | |||
className: 'fileActionsMenu popovermenu bubble hidden open menu', | |||
/** | |||
* Current context |
@@ -9,6 +9,9 @@ | |||
*/ | |||
(function() { | |||
var TEMPLATE_ADDBUTTON = '<a href="#" class="button new" title="{{addText}}"><img src="{{iconUrl}}"></img></a>'; | |||
/** | |||
* @class OCA.Files.FileList | |||
* @classdesc | |||
@@ -220,6 +223,8 @@ | |||
this.$el.find('#controls').prepend(this.breadcrumb.$el); | |||
this._renderNewButton(); | |||
this.$el.find('thead th .columntitle').click(_.bind(this._onClickHeader, this)); | |||
this._onResize = _.debounce(_.bind(this._onResize, this), 100); | |||
@@ -262,6 +267,12 @@ | |||
* Destroy / uninitialize this instance. | |||
*/ | |||
destroy: function() { | |||
if (this._newFileMenu) { | |||
this._newFileMenu.remove(); | |||
} | |||
if (this._newButton) { | |||
this._newButton.remove(); | |||
} | |||
// TODO: also unregister other event handlers | |||
this.fileActions.off('registerAction', this._onFileActionsUpdated); | |||
this.fileActions.off('setDefault', this._onFileActionsUpdated); | |||
@@ -1730,6 +1741,106 @@ | |||
form.trigger('submit'); | |||
}); | |||
}, | |||
/** | |||
* Create an empty file inside the current directory. | |||
* | |||
* @param {string} name name of the file | |||
* | |||
* @return {Promise} promise that will be resolved after the | |||
* file was created | |||
* | |||
* @since 8.2 | |||
*/ | |||
createFile: function(name) { | |||
var self = this; | |||
var deferred = $.Deferred(); | |||
var promise = deferred.promise(); | |||
OCA.Files.Files.isFileNameValid(name); | |||
name = this.getUniqueName(name); | |||
if (this.lastAction) { | |||
this.lastAction(); | |||
} | |||
$.post( | |||
OC.generateUrl('/apps/files/ajax/newfile.php'), | |||
{ | |||
dir: this.getCurrentDirectory(), | |||
filename: name | |||
}, | |||
function(result) { | |||
if (result.status === 'success') { | |||
self.add(result.data, {animate: true, scrollTo: true}); | |||
deferred.resolve(result.status, result.data); | |||
} else { | |||
if (result.data && result.data.message) { | |||
OC.Notification.showTemporary(result.data.message); | |||
} else { | |||
OC.Notification.showTemporary(t('core', 'Could not create file')); | |||
} | |||
deferred.reject(result.status, result.data); | |||
} | |||
} | |||
); | |||
return promise; | |||
}, | |||
/** | |||
* Create a directory inside the current directory. | |||
* | |||
* @param {string} name name of the directory | |||
* | |||
* @return {Promise} promise that will be resolved after the | |||
* directory was created | |||
* | |||
* @since 8.2 | |||
*/ | |||
createDirectory: function(name) { | |||
var self = this; | |||
var deferred = $.Deferred(); | |||
var promise = deferred.promise(); | |||
OCA.Files.Files.isFileNameValid(name); | |||
name = this.getUniqueName(name); | |||
if (this.lastAction) { | |||
this.lastAction(); | |||
} | |||
$.post( | |||
OC.generateUrl('/apps/files/ajax/newfolder.php'), | |||
{ | |||
dir: this.getCurrentDirectory(), | |||
foldername: name | |||
}, | |||
function(result) { | |||
if (result.status === 'success') { | |||
self.add(result.data, {animate: true, scrollTo: true}); | |||
deferred.resolve(result.status, result.data); | |||
} else { | |||
if (result.data && result.data.message) { | |||
OC.Notification.showTemporary(result.data.message); | |||
} else { | |||
OC.Notification.showTemporary(t('core', 'Could not create folder')); | |||
} | |||
deferred.reject(result.status); | |||
} | |||
} | |||
); | |||
return promise; | |||
}, | |||
/** | |||
* Returns whether the given file name exists in the list | |||
* | |||
* @param {string} file file name | |||
* | |||
* @return {bool} true if the file exists in the list, false otherwise | |||
*/ | |||
inList:function(file) { | |||
return this.findFileEl(file).length; | |||
}, | |||
@@ -2391,6 +2502,47 @@ | |||
}); | |||
}, | |||
_renderNewButton: function() { | |||
// if an upload button (legacy) already exists, skip | |||
if ($('#controls .button.upload').length) { | |||
return; | |||
} | |||
if (!this._addButtonTemplate) { | |||
this._addButtonTemplate = Handlebars.compile(TEMPLATE_ADDBUTTON); | |||
} | |||
var $newButton = $(this._addButtonTemplate({ | |||
addText: t('files', 'New'), | |||
iconUrl: OC.imagePath('core', 'actions/add') | |||
})); | |||
$('#controls .actions').prepend($newButton); | |||
$newButton.tooltip({'placement': 'bottom'}); | |||
$newButton.click(_.bind(this._onClickNewButton, this)); | |||
this._newButton = $newButton; | |||
}, | |||
_onClickNewButton: function(event) { | |||
var $target = $(event.target); | |||
if (!$target.hasClass('.button')) { | |||
$target = $target.closest('.button'); | |||
} | |||
this._newButton.tooltip('hide'); | |||
event.preventDefault(); | |||
if ($target.hasClass('disabled')) { | |||
return false; | |||
} | |||
if (!this._newFileMenu) { | |||
this._newFileMenu = new OCA.Files.NewFileMenu({ | |||
fileList: this | |||
}); | |||
$('body').append(this._newFileMenu.$el); | |||
} | |||
this._newFileMenu.showAt($target); | |||
return false; | |||
}, | |||
/** | |||
* Register a tab view to be added to all views | |||
*/ |
@@ -0,0 +1,235 @@ | |||
/* | |||
* Copyright (c) 2014 | |||
* | |||
* This file is licensed under the Affero General Public License version 3 | |||
* or later. | |||
* | |||
* See the COPYING-README file. | |||
* | |||
*/ | |||
/* global Files */ | |||
(function() { | |||
var TEMPLATE_MENU = | |||
'<ul>' + | |||
'<li>' + | |||
'<label for="file_upload_start" class="menuitem" data-action="upload" title="{{uploadMaxHumanFilesize}}"><span class="svg icon icon-upload"></span><span class="displayname">{{uploadLabel}}</span></label>' + | |||
'</li>' + | |||
'{{#each items}}' + | |||
'<li>' + | |||
'<a href="#" class="menuitem" data-templatename="{{templateName}}" data-filetype="{{fileType}}" data-action="{{id}}"><span class="icon {{iconClass}} svg"></span><span class="displayname">{{displayName}}</span></a>' + | |||
'</li>' + | |||
'{{/each}}' + | |||
'</ul>'; | |||
var TEMPLATE_FILENAME_FORM = | |||
'<form class="filenameform">' + | |||
'<label class="hidden-visually" for="{{cid}}-input-{{fileType}}">{{fileName}}</label>' + | |||
'<input id="{{cid}}-input-{{fileType}}" type="text" value="{{fileName}}">' + | |||
'</form>'; | |||
/** | |||
* Construct a new NewFileMenu instance | |||
* @constructs NewFileMenu | |||
* | |||
* @memberof OCA.Files | |||
*/ | |||
var NewFileMenu = OC.Backbone.View.extend({ | |||
tagName: 'div', | |||
className: 'newFileMenu popovermenu bubble hidden open menu', | |||
events: { | |||
'click .menuitem': '_onClickAction' | |||
}, | |||
initialize: function(options) { | |||
var self = this; | |||
var $uploadEl = $('#file_upload_start'); | |||
if ($uploadEl.length) { | |||
$uploadEl.on('fileuploadstart', function() { | |||
self.trigger('actionPerformed', 'upload'); | |||
}); | |||
} else { | |||
console.warn('Missing upload element "file_upload_start"'); | |||
} | |||
this._fileList = options && options.fileList; | |||
}, | |||
template: function(data) { | |||
if (!OCA.Files.NewFileMenu._TEMPLATE) { | |||
OCA.Files.NewFileMenu._TEMPLATE = Handlebars.compile(TEMPLATE_MENU); | |||
} | |||
return OCA.Files.NewFileMenu._TEMPLATE(data); | |||
}, | |||
/** | |||
* Event handler whenever an action has been clicked within the menu | |||
* | |||
* @param {Object} event event object | |||
*/ | |||
_onClickAction: function(event) { | |||
var $target = $(event.target); | |||
if (!$target.hasClass('menuitem')) { | |||
$target = $target.closest('.menuitem'); | |||
} | |||
var action = $target.attr('data-action'); | |||
// note: clicking the upload label will automatically | |||
// set the focus on the "file_upload_start" hidden field | |||
// which itself triggers the upload dialog. | |||
// Currently the upload logic is still in file-upload.js and filelist.js | |||
if (action === 'upload') { | |||
OC.hideMenus(); | |||
} else { | |||
event.preventDefault(); | |||
this._promptFileName($target); | |||
} | |||
}, | |||
_promptFileName: function($target) { | |||
var self = this; | |||
if (!OCA.Files.NewFileMenu._TEMPLATE_FORM) { | |||
OCA.Files.NewFileMenu._TEMPLATE_FORM = Handlebars.compile(TEMPLATE_FILENAME_FORM); | |||
} | |||
if ($target.find('form').length) { | |||
$target.find('input').focus(); | |||
return; | |||
} | |||
// discard other forms | |||
this.$el.find('form').remove(); | |||
this.$el.find('.displayname').removeClass('hidden'); | |||
$target.find('.displayname').addClass('hidden'); | |||
var newName = $target.attr('data-templatename'); | |||
var fileType = $target.attr('data-filetype'); | |||
var $form = $(OCA.Files.NewFileMenu._TEMPLATE_FORM({ | |||
fileName: newName, | |||
cid: this.cid, | |||
fileType: fileType | |||
})); | |||
//this.trigger('actionPerformed', action); | |||
$target.append($form); | |||
// here comes the OLD code | |||
var $input = $form.find('input'); | |||
var lastPos; | |||
var checkInput = function () { | |||
var filename = $input.val(); | |||
try { | |||
if (!Files.isFileNameValid(filename)) { | |||
// Files.isFileNameValid(filename) throws an exception itself | |||
} else if (self._fileList.inList(filename)) { | |||
throw t('files', '{newname} already exists', {newname: filename}); | |||
} else { | |||
return true; | |||
} | |||
} catch (error) { | |||
$input.attr('title', error); | |||
$input.tooltip({placement: 'right', trigger: 'manual'}); | |||
$input.tooltip('show'); | |||
$input.addClass('error'); | |||
} | |||
return false; | |||
}; | |||
// verify filename on typing | |||
$input.keyup(function() { | |||
if (checkInput()) { | |||
$input.tooltip('hide'); | |||
$input.removeClass('error'); | |||
} | |||
}); | |||
$input.focus(); | |||
// pre select name up to the extension | |||
lastPos = newName.lastIndexOf('.'); | |||
if (lastPos === -1) { | |||
lastPos = newName.length; | |||
} | |||
$input.selectRange(0, lastPos); | |||
$form.submit(function(event) { | |||
event.stopPropagation(); | |||
event.preventDefault(); | |||
if (checkInput()) { | |||
var newname = $input.val(); | |||
self._createFile(fileType, newname); | |||
$form.remove(); | |||
$target.find('.displayname').removeClass('hidden'); | |||
OC.hideMenus(); | |||
} | |||
}); | |||
}, | |||
/** | |||
* Creates a file with the given type and name. | |||
* This calls the matching methods on the attached file list. | |||
* | |||
* @param {string} fileType file type | |||
* @param {string} name file name | |||
*/ | |||
_createFile: function(fileType, name) { | |||
switch(fileType) { | |||
case 'file': | |||
this._fileList.createFile(name); | |||
break; | |||
case 'folder': | |||
this._fileList.createDirectory(name); | |||
break; | |||
default: | |||
console.warn('Unknown file type "' + fileType + '"'); | |||
} | |||
}, | |||
/** | |||
* Renders the menu with the currently set items | |||
*/ | |||
render: function() { | |||
this.$el.html(this.template({ | |||
uploadMaxHumanFileSize: 'TODO', | |||
uploadLabel: t('files', 'Upload'), | |||
items: [{ | |||
id: 'file', | |||
displayName: t('files', 'Text file'), | |||
templateName: t('files', 'New text file.txt'), | |||
iconClass: 'icon-filetype-text', | |||
fileType: 'file' | |||
}, { | |||
id: 'folder', | |||
displayName: t('files', 'Folder'), | |||
templateName: t('files', 'New folder'), | |||
iconClass: 'icon-folder', | |||
fileType: 'folder' | |||
}] | |||
})); | |||
}, | |||
/** | |||
* Displays the menu under the given element | |||
* | |||
* @param {Object} $target target element | |||
*/ | |||
showAt: function($target) { | |||
this.render(); | |||
var targetOffset = $target.offset(); | |||
this.$el.css({ | |||
left: targetOffset.left, | |||
top: targetOffset.top + $target.height() | |||
}); | |||
this.$el.removeClass('hidden'); | |||
OC.showMenu(null, this.$el); | |||
} | |||
}); | |||
OCA.Files.NewFileMenu = NewFileMenu; | |||
})(); |
@@ -1,40 +1,16 @@ | |||
<div id="controls"> | |||
<div class="actions creatable hidden"> | |||
<?php if(!isset($_['dirToken'])):?> | |||
<div id="new" class="button"> | |||
<a><?php p($l->t('New'));?></a> | |||
<ul> | |||
<li class="icon-filetype-text svg" | |||
data-type="file" data-newname="<?php p($l->t('New text file')) ?>.txt"> | |||
<p><?php p($l->t('Text file'));?></p> | |||
</li> | |||
<li class="icon-filetype-folder svg" | |||
data-type="folder" data-newname="<?php p($l->t('New folder')) ?>"> | |||
<p><?php p($l->t('Folder'));?></p> | |||
</li> | |||
</ul> | |||
</div> | |||
<?php endif;?> | |||
<?php /* Note: the template attributes are here only for the public page. These are normally loaded | |||
through ajax instead (updateStorageStatistics). | |||
*/ ?> | |||
<div id="upload" class="button" | |||
<?php /* | |||
Only show upload button for public page | |||
*/ ?> | |||
<?php if(isset($_['dirToken'])):?> | |||
<div id="upload" class="button upload" | |||
title="<?php isset($_['uploadMaxHumanFilesize']) ? p($l->t('Upload (max. %s)', array($_['uploadMaxHumanFilesize']))) : '' ?>"> | |||
<input type="hidden" id="max_upload" name="MAX_FILE_SIZE" value="<?php isset($_['uploadMaxFilesize']) ? p($_['uploadMaxFilesize']) : '' ?>"> | |||
<input type="hidden" id="upload_limit" value="<?php isset($_['uploadLimit']) ? p($_['uploadLimit']) : '' ?>"> | |||
<input type="hidden" id="free_space" value="<?php isset($_['freeSpace']) ? p($_['freeSpace']) : '' ?>"> | |||
<?php if(isset($_['dirToken'])):?> | |||
<input type="hidden" id="publicUploadRequestToken" name="requesttoken" value="<?php p($_['requesttoken']) ?>" /> | |||
<input type="hidden" id="dirToken" name="dirToken" value="<?php p($_['dirToken']) ?>" /> | |||
<?php endif;?> | |||
<input type="hidden" class="max_human_file_size" | |||
value="(max <?php isset($_['uploadMaxHumanFilesize']) ? p($_['uploadMaxHumanFilesize']) : ''; ?>)"> | |||
<input type="file" id="file_upload_start" name='files[]' | |||
data-url="<?php print_unescaped(OCP\Util::linkTo('files', 'ajax/upload.php')); ?>" /> | |||
<label for="file_upload_start" class="svg icon-upload"> | |||
<span class="hidden-visually"><?php p($l->t('Upload'))?></span> | |||
</label> | |||
</div> | |||
<?php endif; ?> | |||
<div id="uploadprogresswrapper"> | |||
<div id="uploadprogressbar"></div> | |||
<button class="stop icon-close" style="display:none"> | |||
@@ -48,7 +24,19 @@ | |||
<div class="notCreatable notPublic hidden"> | |||
<?php p($l->t('You don’t have permission to upload or create files here'))?> | |||
</div> | |||
<?php /* Note: the template attributes are here only for the public page. These are normally loaded | |||
through ajax instead (updateStorageStatistics). | |||
*/ ?> | |||
<input type="hidden" name="permissions" value="" id="permissions"> | |||
<input type="hidden" id="max_upload" name="MAX_FILE_SIZE" value="<?php isset($_['uploadMaxFilesize']) ? p($_['uploadMaxFilesize']) : '' ?>"> | |||
<input type="hidden" id="upload_limit" value="<?php isset($_['uploadLimit']) ? p($_['uploadLimit']) : '' ?>"> | |||
<input type="hidden" id="free_space" value="<?php isset($_['freeSpace']) ? p($_['freeSpace']) : '' ?>"> | |||
<?php if(isset($_['dirToken'])):?> | |||
<input type="hidden" id="publicUploadRequestToken" name="requesttoken" value="<?php p($_['requesttoken']) ?>" /> | |||
<input type="hidden" id="dirToken" name="dirToken" value="<?php p($_['dirToken']) ?>" /> | |||
<?php endif;?> | |||
<input type="hidden" class="max_human_file_size" | |||
value="(max <?php isset($_['uploadMaxHumanFilesize']) ? p($_['uploadMaxHumanFilesize']) : ''; ?>)"> | |||
</div> | |||
<div id="emptycontent" class="hidden"> | |||
@@ -101,6 +89,10 @@ | |||
</tfoot> | |||
</table> | |||
<input type="hidden" name="dir" id="dir" value="" /> | |||
<div class="hiddenuploadfield"> | |||
<input type="file" id="file_upload_start" class="hiddenuploadfield" name="files[]" | |||
data-url="<?php print_unescaped(OCP\Util::linkTo('files', 'ajax/upload.php')); ?>" /> | |||
</div> | |||
<div id="editor"></div><!-- FIXME Do not use this div in your app! It is deprecated and will be removed in the future! --> | |||
<div id="uploadsize-message" title="<?php p($l->t('Upload too large'))?>"> | |||
<p> |
@@ -19,7 +19,6 @@ | |||
* | |||
*/ | |||
/* global OC */ | |||
describe('OC.Upload tests', function() { | |||
var $dummyUploader; | |||
var testFile; | |||
@@ -118,54 +117,4 @@ describe('OC.Upload tests', function() { | |||
); | |||
}); | |||
}); | |||
describe('New file', function() { | |||
var $input; | |||
var currentDirStub; | |||
beforeEach(function() { | |||
OC.Upload.init(); | |||
$('#new>a').click(); | |||
$('#new li[data-type=file]').click(); | |||
$input = $('#new input[type=text]'); | |||
currentDirStub = sinon.stub(FileList, 'getCurrentDirectory'); | |||
currentDirStub.returns('testdir'); | |||
}); | |||
afterEach(function() { | |||
currentDirStub.restore(); | |||
}); | |||
it('sets default text in field', function() { | |||
expect($input.length).toEqual(1); | |||
expect($input.val()).toEqual('New text file.txt'); | |||
}); | |||
it('creates file when enter is pressed', function() { | |||
$input.val('somefile.txt'); | |||
$input.trigger(new $.Event('keyup', {keyCode: 13})); | |||
$input.parent('form').submit(); | |||
expect(fakeServer.requests.length).toEqual(2); | |||
var request = fakeServer.requests[1]; | |||
expect(request.method).toEqual('POST'); | |||
expect(request.url).toEqual(OC.webroot + '/index.php/apps/files/ajax/newfile.php'); | |||
var query = OC.parseQueryString(request.requestBody); | |||
expect(query).toEqual({ | |||
dir: 'testdir', | |||
filename: 'somefile.txt' | |||
}); | |||
}); | |||
it('prevents entering invalid file names', function() { | |||
$input.val('..'); | |||
$input.trigger(new $.Event('keyup', {keyCode: 13})); | |||
$input.parent('form').submit(); | |||
expect(fakeServer.requests.length).toEqual(1); | |||
}); | |||
it('prevents entering file names that already exist', function() { | |||
var inListStub = sinon.stub(FileList, 'inList').returns(true); | |||
$input.val('existing.txt'); | |||
$input.trigger(new $.Event('keyup', {keyCode: 13})); | |||
$input.parent('form').submit(); | |||
expect(fakeServer.requests.length).toEqual(1); | |||
inListStub.restore(); | |||
}); | |||
}); | |||
}); |
@@ -2157,6 +2157,93 @@ describe('OCA.Files.FileList tests', function() { | |||
expect(fileList.$fileList.find('tr').length).toEqual(5); | |||
}); | |||
}); | |||
describe('create file', function() { | |||
var deferredCreate; | |||
beforeEach(function() { | |||
deferredCreate = $.Deferred(); | |||
}); | |||
it('creates file with given name and adds it to the list', function() { | |||
var deferred = fileList.createFile('test file.txt'); | |||
var successStub = sinon.stub(); | |||
var failureStub = sinon.stub(); | |||
deferred.done(successStub); | |||
deferred.fail(failureStub); | |||
expect(fakeServer.requests.length).toEqual(1); | |||
expect(fakeServer.requests[0].url).toEqual(OC.generateUrl('/apps/files/ajax/newfile.php')); | |||
var query = fakeServer.requests[0].requestBody; | |||
expect(OC.parseQueryString(query)).toEqual({ | |||
dir: '/subdir', | |||
filename: 'test file.txt' | |||
}); | |||
fakeServer.requests[0].respond( | |||
200, | |||
{ 'Content-Type': 'application/json' }, | |||
JSON.stringify({ | |||
status: 'success', | |||
data: { | |||
path: '/subdir', | |||
name: 'test file.txt', | |||
mimetype: 'text/plain' | |||
} | |||
}) | |||
); | |||
var $tr = fileList.findFileEl('test file.txt'); | |||
expect($tr.length).toEqual(1); | |||
expect($tr.attr('data-mime')).toEqual('text/plain'); | |||
expect(successStub.calledOnce).toEqual(true); | |||
expect(failureStub.notCalled).toEqual(true); | |||
}); | |||
// TODO: error cases | |||
// TODO: unique name cases | |||
}); | |||
describe('create directory', function() { | |||
it('creates directory with given name and adds it to the list', function() { | |||
var deferred = fileList.createDirectory('test directory'); | |||
var successStub = sinon.stub(); | |||
var failureStub = sinon.stub(); | |||
deferred.done(successStub); | |||
deferred.fail(failureStub); | |||
expect(fakeServer.requests.length).toEqual(1); | |||
expect(fakeServer.requests[0].url).toEqual(OC.generateUrl('/apps/files/ajax/newfolder.php')); | |||
var query = fakeServer.requests[0].requestBody; | |||
expect(OC.parseQueryString(query)).toEqual({ | |||
dir: '/subdir', | |||
foldername: 'test directory' | |||
}); | |||
fakeServer.requests[0].respond( | |||
200, | |||
{ 'Content-Type': 'application/json' }, | |||
JSON.stringify({ | |||
status: 'success', | |||
data: { | |||
path: '/subdir', | |||
name: 'test directory', | |||
mimetype: 'httpd/unix-directory' | |||
} | |||
}) | |||
); | |||
var $tr = fileList.findFileEl('test directory'); | |||
expect($tr.length).toEqual(1); | |||
expect($tr.attr('data-mime')).toEqual('httpd/unix-directory'); | |||
expect(successStub.calledOnce).toEqual(true); | |||
expect(failureStub.notCalled).toEqual(true); | |||
}); | |||
// TODO: error cases | |||
// TODO: unique name cases | |||
}); | |||
/** | |||
* Test upload mostly by testing the code inside the event handlers | |||
* that were registered on the magic upload object | |||
@@ -2359,4 +2446,36 @@ describe('OCA.Files.FileList tests', function() { | |||
expect(fileInfo.type).toEqual('file'); | |||
}); | |||
}); | |||
describe('new file menu', function() { | |||
var newFileMenuStub; | |||
beforeEach(function() { | |||
newFileMenuStub = sinon.stub(OCA.Files.NewFileMenu.prototype, 'showAt'); | |||
}); | |||
afterEach(function() { | |||
newFileMenuStub.restore(); | |||
}) | |||
it('renders new button when no legacy upload button exists', function() { | |||
expect(fileList.$el.find('.button.upload').length).toEqual(0); | |||
expect(fileList.$el.find('.button.new').length).toEqual(1); | |||
}); | |||
it('does not render new button when no legacy upload button exists (public page)', function() { | |||
fileList.destroy(); | |||
$('#controls').append('<input type="button" class="button upload" />'); | |||
fileList = new OCA.Files.FileList($('#app-content-files')); | |||
expect(fileList.$el.find('.button.upload').length).toEqual(1); | |||
expect(fileList.$el.find('.button.new').length).toEqual(0); | |||
}); | |||
it('opens the new file menu when clicking on the "New" button', function() { | |||
var $button = fileList.$el.find('.button.new'); | |||
$button.click(); | |||
expect(newFileMenuStub.calledOnce).toEqual(true); | |||
}); | |||
it('does not open the new file menu when button is disabled', function() { | |||
var $button = fileList.$el.find('.button.new'); | |||
$button.addClass('disabled'); | |||
$button.click(); | |||
expect(newFileMenuStub.notCalled).toEqual(true); | |||
}); | |||
}); | |||
}); |
@@ -0,0 +1,119 @@ | |||
/** | |||
* ownCloud | |||
* | |||
* @author Vincent Petry | |||
* @copyright 2015 Vincent Petry <pvince81@owncloud.com> | |||
* | |||
* This library is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or any later version. | |||
* | |||
* This library is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |||
* GNU AFFERO GENERAL PUBLIC LICENSE for more details. | |||
* | |||
* You should have received a copy of the GNU Affero General Public | |||
* License along with this library. If not, see <http://www.gnu.org/licenses/>. | |||
* | |||
*/ | |||
describe('OCA.Files.NewFileMenu', function() { | |||
var FileList = OCA.Files.FileList; | |||
var menu, fileList, $uploadField, $trigger; | |||
beforeEach(function() { | |||
// dummy upload button | |||
var $container = $('<div id="app-content-files"></div>'); | |||
$uploadField = $('<input id="file_upload_start"></input>'); | |||
$trigger = $('<a href="#">Menu</a>'); | |||
$container.append($uploadField).append($trigger); | |||
$('#testArea').append($container); | |||
fileList = new FileList($container); | |||
menu = new OCA.Files.NewFileMenu({ | |||
fileList: fileList | |||
}); | |||
menu.showAt($trigger); | |||
}); | |||
afterEach(function() { | |||
OC.hideMenus(); | |||
fileList = null; | |||
menu = null; | |||
}); | |||
describe('rendering', function() { | |||
it('renders menu items', function() { | |||
var $items = menu.$el.find('.menuitem'); | |||
expect($items.length).toEqual(3); | |||
// label points to the file_upload_start item | |||
var $item = $items.eq(0); | |||
expect($item.is('label')).toEqual(true); | |||
expect($item.attr('for')).toEqual('file_upload_start'); | |||
}); | |||
}); | |||
describe('New file/folder', function() { | |||
var $input; | |||
var createFileStub; | |||
var createDirectoryStub; | |||
beforeEach(function() { | |||
createFileStub = sinon.stub(FileList.prototype, 'createFile'); | |||
createDirectoryStub = sinon.stub(FileList.prototype, 'createDirectory'); | |||
menu.$el.find('.menuitem').eq(1).click(); | |||
$input = menu.$el.find('form.filenameform input'); | |||
}); | |||
afterEach(function() { | |||
createFileStub.restore(); | |||
createDirectoryStub.restore(); | |||
}); | |||
it('sets default text in field', function() { | |||
expect($input.length).toEqual(1); | |||
expect($input.val()).toEqual('New text file.txt'); | |||
}); | |||
it('creates file when enter is pressed', function() { | |||
$input.val('somefile.txt'); | |||
$input.trigger(new $.Event('keyup', {keyCode: 13})); | |||
$input.parent('form').submit(); | |||
expect(createFileStub.calledOnce).toEqual(true); | |||
expect(createFileStub.getCall(0).args[0]).toEqual('somefile.txt'); | |||
expect(createDirectoryStub.notCalled).toEqual(true); | |||
}); | |||
it('prevents entering invalid file names', function() { | |||
$input.val('..'); | |||
$input.trigger(new $.Event('keyup', {keyCode: 13})); | |||
$input.closest('form').submit(); | |||
expect(createFileStub.notCalled).toEqual(true); | |||
expect(createDirectoryStub.notCalled).toEqual(true); | |||
}); | |||
it('prevents entering file names that already exist', function() { | |||
var inListStub = sinon.stub(fileList, 'inList').returns(true); | |||
$input.val('existing.txt'); | |||
$input.trigger(new $.Event('keyup', {keyCode: 13})); | |||
$input.closest('form').submit(); | |||
expect(createFileStub.notCalled).toEqual(true); | |||
expect(createDirectoryStub.notCalled).toEqual(true); | |||
inListStub.restore(); | |||
}); | |||
it('switching fields removes the previous form', function() { | |||
menu.$el.find('.menuitem').eq(2).click(); | |||
expect(menu.$el.find('form').length).toEqual(1); | |||
}); | |||
it('creates directory when clicking on create directory field', function() { | |||
menu.$el.find('.menuitem').eq(2).click(); | |||
$input = menu.$el.find('form.filenameform input'); | |||
$input.val('some folder'); | |||
$input.trigger(new $.Event('keyup', {keyCode: 13})); | |||
$input.closest('form').submit(); | |||
expect(createDirectoryStub.calledOnce).toEqual(true); | |||
expect(createDirectoryStub.getCall(0).args[0]).toEqual('some folder'); | |||
expect(createFileStub.notCalled).toEqual(true); | |||
}); | |||
}); | |||
}); |
@@ -17,6 +17,7 @@ OCP\Util::addStyle('files', 'upload'); | |||
OCP\Util::addScript('files', 'filesummary'); | |||
OCP\Util::addScript('files', 'breadcrumb'); | |||
OCP\Util::addScript('files', 'fileinfomodel'); | |||
OCP\Util::addScript('files', 'newfilemenu'); | |||
OCP\Util::addScript('files', 'files'); | |||
OCP\Util::addScript('files', 'filelist'); | |||
OCP\Util::addscript('files', 'keyboardshortcuts'); |
@@ -629,3 +629,68 @@ em { | |||
.tabsContainer .tab { | |||
padding: 15px; | |||
} | |||
/* popover menu styles (use together with "bubble" class) */ | |||
.popovermenu .menuitem, | |||
.popovermenu .menuitem>span { | |||
cursor: pointer; | |||
} | |||
.popovermenu .menuitem { | |||
-ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=50)"; | |||
filter: alpha(opacity=50); | |||
opacity: .5; | |||
} | |||
.popovermenu .menuitem:hover, | |||
.popovermenu .menuitem:focus { | |||
-ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=100)"; | |||
filter: alpha(opacity=100); | |||
opacity: 1; | |||
} | |||
.popovermenu { | |||
padding: 4px 12px; | |||
} | |||
.popovermenu li { | |||
padding: 5px 0; | |||
} | |||
.popovermenu .menuitem img { | |||
padding: initial; | |||
} | |||
.popovermenu a.menuitem, | |||
.popovermenu label.menuitem, | |||
.popovermenu .menuitem { | |||
padding: 10px; | |||
margin: -10px; | |||
} | |||
.popovermenu.hidden { | |||
display: none; | |||
} | |||
.popovermenu .menuitem { | |||
display: block; | |||
line-height: 30px; | |||
padding-left: 5px; | |||
color: #000; | |||
padding: 0; | |||
} | |||
.popovermenu .menuitem .icon, | |||
.popovermenu .menuitem .no-icon { | |||
display: inline-block; | |||
width: 16px; | |||
margin-right: 10px; | |||
} | |||
.popovermenu .menuitem { | |||
opacity: 0.5; | |||
} | |||
.popovermenu li:hover .menuitem { | |||
opacity: 1; | |||
} |
@@ -955,6 +955,7 @@ a.bookmarklet { background-color:#ddd; border:1px solid #ccc; padding:5px;paddin | |||
.ui-icon-circle-triangle-e{ background-image:url('../img/actions/play-next.svg'); } | |||
.ui-icon-circle-triangle-w{ background-image:url('../img/actions/play-previous.svg'); } | |||
.ui-datepicker-prev,.ui-datepicker-next{ border:1px solid #ddd; background:#fff; } | |||
/* ---- DIALOGS ---- */ | |||
@@ -1142,3 +1143,12 @@ fieldset.warning legend + p, fieldset.update legend + p { | |||
@-ms-viewport { | |||
width: device-width; | |||
} | |||
/* hidden input type=file field */ | |||
.hiddenuploadfield { | |||
width: 0; | |||
height: 0; | |||
opacity: 0; | |||
-ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=0)"; | |||
} | |||