diff --git a/Gruntfile.js b/Gruntfile.js index a7f651a196..b4a41dbb68 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -195,7 +195,8 @@ module.exports = function (grunt) { "api/js/etemplate/et2_widget_itempicker.js", "api/js/etemplate/et2_widget_script.js", "api/js/etemplate/et2_core_legacyJSFunctions.js", - "api/js/etemplate/etemplate2.js" + "api/js/etemplate/etemplate2.js", + "api/js/etemplate/vfsSelectUI.js" ] } }, diff --git a/api/js/etemplate/et2_extension_customfields.js b/api/js/etemplate/et2_extension_customfields.js index 435c0d0422..5bdbd1daaf 100644 --- a/api/js/etemplate/et2_extension_customfields.js +++ b/api/js/etemplate/et2_extension_customfields.js @@ -633,7 +633,7 @@ var et2_customfields_list = (function(){ "use strict"; return et2_valueWidget.ex { label: '', mode: widget.options.multiple ? 'open-multiple' : 'open', - method: 'EGroupware\\Api\\Etemplate\\Widget\\Link::link_existing', + method: 'EGroupware\\Api\\Etemplate\\Widget\\Link::ajax_link_existing', method_id: attrs.path, button_label: egw.lang('Link') },{type: 'vfs-select'}); diff --git a/api/js/etemplate/et2_widget_link.js b/api/js/etemplate/et2_widget_link.js index 74c809ad09..69a66bcbf8 100644 --- a/api/js/etemplate/et2_widget_link.js +++ b/api/js/etemplate/et2_widget_link.js @@ -183,40 +183,40 @@ var et2_link_to = (function(){ "use strict"; return et2_inputWidget.extend( var select_attrs = { button_label: egw.lang('Link'), button_caption: '', - readonly: this.options.readonly + readonly: this.options.readonly, + onchange: function() { + var values = true; + // If entry not yet saved, store for linking on server + if(!self.options.value.to_id || typeof self.options.value.to_id == 'object') + { + values = self.options.value.to_id || {}; + var files = self.vfs_select.getValue(); + if(typeof files !== 'undefined') + { + for(var i = 0; i < files.length; i++) + { + values['link:'+files[i]] = { + app: 'link', + id: files[i], + type: 'unknown', + icon: 'link', + remark: '', + title: files[i] + }; + } + } + } + self._link_result(values); + } }; // only set server-side callback, if we have a real application-id (not null or array) // otherwise it only gives an error on server-side if (self.options.value && self.options.value.to_id && typeof self.options.value.to_id != 'object') { - select_attrs.method = 'EGroupware\\Api\\Etemplate\\Widget\\Link::link_existing'; + select_attrs.method = 'EGroupware\\Api\\Etemplate\\Widget\\Link::ajax_link_existing'; select_attrs.method_id = self.options.value.to_app + ':' + self.options.value.to_id; } this.vfs_select = et2_createWidget("vfs-select", select_attrs,this); this.vfs_select.set_readonly(this.options.readonly); - jQuery(this.vfs_select.getDOMNode()).change( function() { - var values = true; - // If entry not yet saved, store for linking on server - if(!self.options.value.to_id || typeof self.options.value.to_id == 'object') - { - values = self.options.value.to_id || {}; - var files = self.vfs_select.getValue(); - if(typeof files !== 'undefined') - { - for(var i = 0; i < files.length; i++) - { - values['link:'+files[i]] = { - app: 'link', - id: files[i], - type: 'unknown', - icon: 'link', - remark: '', - title: files[i] - }; - } - } - } - self._link_result(values); - }); // File upload var file_attrs = { @@ -633,7 +633,7 @@ var et2_link_entry = (function(){ "use strict"; return et2_inputWidget.extend( var buttonItem = jQuery( "", { "class": "ui-selectmenu-text", title: value - }) + }); jQuery('.ui-selectmenu-text', this.button).replaceWith(buttonItem); buttonItem.css('background-image', 'url('+url+')'); @@ -786,7 +786,7 @@ var et2_link_entry = (function(){ "use strict"; return et2_inputWidget.extend( // Normal stuff li.append(jQuery( "" ).text( item.label )) .appendTo(ul); - window.setTimeout(function(){ul.css('max-width', jQuery('.et2_container').width()-ul.offset().left)}, 300); + window.setTimeout(function(){ul.css('max-width', jQuery('.et2_container').width()-ul.offset().left);}, 300); return li; }; diff --git a/api/js/etemplate/et2_widget_vfs.js b/api/js/etemplate/et2_widget_vfs.js index 53ca5b1baf..f16e27362e 100644 --- a/api/js/etemplate/et2_widget_vfs.js +++ b/api/js/etemplate/et2_widget_vfs.js @@ -12,6 +12,7 @@ /*egw:uses /vendor/bower-asset/jquery/dist/jquery.js; + vfsSelectUI; et2_core_inputWidget; et2_core_valueWidget; et2_widget_description; @@ -867,12 +868,15 @@ var et2_vfsSelect = (function(){ "use strict"; return et2_inputWidget.extend( "method": { name: "Server side callback", type: "string", - description: "Server side callback to process selected value(s) in app.class.method or class::method format. The first parameter will be Method ID, the second the file list." + description: "Server side callback to process selected value(s) in \n\ + app.class.method or class::method format. The first parameter will \n\ + be Method ID, the second the file list." }, "method_id": { name: "Method ID", type: "any", - description: "optional parameter passed to server side callback. Can be a string or a function.", + description: "optional parameter passed to server side callback.\n\ + Can be a string or a function.", default: "" }, "path": { @@ -926,7 +930,7 @@ var et2_vfsSelect = (function(){ "use strict"; return et2_inputWidget.extend( { this.button.hide(); } - + if (this.options.button_caption != "") { this.button.text(this.options.button_caption); @@ -934,61 +938,117 @@ var et2_vfsSelect = (function(){ "use strict"; return et2_inputWidget.extend( this.setDOMNode(egw.app('filemanager') ? this.button[0]:document.createElement('span')); }, - click: function(e) { - - // No permission - if(!egw.app('filemanager')) return; - + _content: function (_content, _callback) + { + egw(window).loading_prompt('vfs-select', true, '', 'body'); var self = this; - - var attrs = { - menuaction: 'filemanager.filemanager_select.select', - mode: this.options.mode, - method: this.options.method, - label: this.options.button_label, - id: typeof this.options.method_id == "function" ? this.options.method_id.call(): this.options.method_id - }; - if(this.options.path) + if (typeof app.vfsSelectUI !="undefined") { - attrs.path = this.options.path; + if (this.dialog && this.dialog.div) this.dialog.div.dialog('close'); + delete app.vfsSelectUI; } - if(this.options.mime) - { - attrs.mime = this.options.mime; + var attrs = { + mode: this.options.mode, + label: this.options.button_label, + path: this.options.path || null, + mime: this.options.mime || null }; + var callback = _callback || this._buildDialog; + egw(window).json( + 'EGroupware\\Api\\Etemplate\\Widget\\Vfs::ajax_vfsSelect_content', + [_content, attrs], + function(_content){ + egw(window).loading_prompt('vfs-select', false); + callback.apply(self, arguments); + } + ).sendRequest(true); + }, - // Open the filemanager select in a popup - var popup = this.egw(window).open_link( - this.egw().link('/index.php', attrs), - 'link_existing', - '680x400' - ); - if(popup) + /** + * Builds file navigator dialog + * + * @param {object} _data content + */ + _buildDialog: function (_data) + { + if (!_data.content.mode.match(/open|open-multiple|saveas|select-dir/)) { + egw.debug('Mode is not matched!'); + return; + } + var self = this; + var buttons = [ + {text: egw.lang(_data.content.label), id:"submit"}, + {text: egw.lang("Close"), id:"close"} + ]; + var data = jQuery.extend(_data, {'currentapp': egw.app_name()}); + + // define a mini app object for vfs select UI + app.vfsSelectUI = new app.classes.vfsSelectUI; + + this.dialog = et2_createWidget("dialog", { - // Safari and IE lose reference to global variables after window close - // Try to get updated data before window is closed then later we trigger - // change event on widget - self.egw().window.setTimeout(function(){ - jQuery(popup).bind('unload',function(){ - // Set selected files to widget - self.value = this.selected_files; - - // Update path to where the user wound up] - if (typeof this.etemplate2 !='undefined') self.options.path = this.etemplate2.getByApplication('filemanager')[0].widgetContainer.getArrayMgr("content").getEntry('path'); - }); - },1000); - - // Update on close doesn't always (ever, in chrome) work, so poll - var poll = self.egw().window.setInterval( - function() { - if(popup.closed) { - self.egw().window.clearInterval(poll); - // Fire a change event so any handlers run + callback: function(_button_id, _value) + { + if (_button_id == 'submit' && _value) + { + var files = []; + switch(_data.content.mode) + { + case 'open-multiple': + if (_value.dir && _value.dir.selected) + { + Object.keys(_value.dir.selected) + .forEach((key) => (_value.dir.selected[key] != "") + && files.push(_value.path+'/'+_value.dir.selected[key])); + } + break; + case 'select-dir': + files = _value.path; + break; + default: + files = _value.path+'/'+_value.name; + break; + } + self.value = files; + if (self.options.method) + { + egw(window).json( + self.options.method, + [self.options.method_id, files], + function(){ + jQuery(self.node).change(); + } + ).sendRequest(true); + } + else + { jQuery(self.node).change(); } - },1000 - ); - } + delete app.vfsSelectUI; + } + }, + title: egw.lang('Save File'), + buttons: buttons, + minWidth: 500, + minHeight: 400, + value: data, + template: egw.webserverUrl+'/api/templates/default/vfsSelectUI.xet?1', + resizable: false + }, et2_dialog._create_parent('api')); + this.dialog.template.uniqueId = 'api.vfsSelectUI'; + app.vfsSelectUI.et2 = this.dialog.template.widgetContainer; + app.vfsSelectUI.vfsSelectWidget = this; + this.dialog.div.on('load', function(e) { + app.vfsSelectUI.et2_ready(app.vfsSelectUI.et2, 'api.vfsSelectUI'); + }); + }, + + /** + * click handler + * @param {event object} e + */ + click: function(e) { + this._content.call(this, null); }, /** @@ -1038,7 +1098,7 @@ var et2_vfsSelect = (function(){ "use strict"; return et2_inputWidget.extend( set_readonly: function(readonly) { this.options.readonly = Boolean(readonly); - + if(this.options.readonly) { this.button.hide(); diff --git a/api/js/etemplate/vfsSelectUI.js b/api/js/etemplate/vfsSelectUI.js new file mode 100644 index 0000000000..81460f905e --- /dev/null +++ b/api/js/etemplate/vfsSelectUI.js @@ -0,0 +1,315 @@ +/** + * EGroupware - VFS SELECT Widget UI + * + * @link http://www.egroupware.org + * @package et2_vfsSelect + * @author Hadi Nategh + * @copyright (c) 2013-2017 by Hadi Nategh + * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License + * @version $Id$ + */ + +"use strict"; +/** + * UI for VFS Select widget + * + * @augments AppJS + */ +app.classes.vfsSelectUI = AppJS.extend({ + + vfsSelectWidget: {}, + path_widget: {}, + /** + * Constructor + * + * @memberOf app.filemanager + */ + init: function() + { + // call parent + this._super.apply(this, arguments); + + }, + + /** + * Destructor + */ + destroy: function() + { + delete this.path_widget; + delete this.vfsSelectWidget; + // call parent + this._super.apply(this, arguments); + }, + + /** + * This function is called when the etemplate2 object is loaded + * and ready. If you must store a reference to the et2 object, + * make sure to clean it up in destroy(). + * + * @param et2 etemplate2 Newly ready object + * @param {string} name template name + */ + et2_ready: function(et2,name) + { + this.path_widget = this.et2.getWidgetById('path'); + }, + + /** + * Get directory of a path + * + * @param {string} _path + * @returns string + */ + dirname: function(_path) + { + var parts = _path.split('/'); + parts.pop(); + return parts.join('/') || '/'; + }, + + /** + * Get name of a path + * + * @param {string} _path + * @returns string + */ + basename: function(_path) + { + return _path.split('/').pop(); + }, + + /** + * Get current working directory + * + * @return string + */ + get_path: function() + { + return this.path_widget.get_value(); + }, + + /** + * Send names of uploaded files (again) to server, to process them: either copy to vfs or ask overwrite/rename + * + * @param {event} _event + * @param {number} _file_count + * @param {string=} _path where the file is uploaded to, default current directory + */ + upload: function(_event, _file_count, _path) + { + if(typeof _path == 'undefined') + { + _path = this.get_path(); + } + if (_file_count && !jQuery.isEmptyObject(_event.data.getValue())) + { + var widget = _event.data; + egw(window).json('filemanager_ui::ajax_action', ['upload', widget.getValue(), _path], + this._upload_callback, this, true, this + ).sendRequest(true); + widget.set_value(''); + } + }, + + /** + * Callback for server response to upload request: + * - display message and refresh list + * - ask use to confirm overwritting existing files or rename upload + * + * @param {object} _data values for attributes msg, files, ... + */ + _upload_callback: function(_data) + { + if (_data.msg || _data.uploaded) window.egw_refresh(_data.msg, this.appname); + + var that = this; + for(var file in _data.uploaded) + { + if (_data.uploaded[file].confirm && !_data.uploaded[file].confirmed) + { + var buttons = [ + {text: this.egw.lang("Yes"), id: "overwrite", class: "ui-priority-primary", "default": true, image: 'check'}, + {text: this.egw.lang("Rename"), id:"rename", image: 'edit'}, + {text: this.egw.lang("Cancel"), id:"cancel"} + ]; + if (_data.uploaded[file].confirm === "is_dir") + buttons.shift(); + var dialog = et2_dialog.show_prompt(function(_button_id, _value) { + var uploaded = {}; + uploaded[this.my_data.file] = this.my_data.data; + switch (_button_id) + { + case "overwrite": + uploaded[this.my_data.file].confirmed = true; + // fall through + case "rename": + uploaded[this.my_data.file].name = _value; + delete uploaded[this.my_data.file].confirm; + // send overwrite-confirmation and/or rename request to server + egw.json('filemanager_ui::ajax_action', [this.my_data.action, uploaded, this.my_data.path, this.my_data.props], + that._upload_callback, that, true, that + ).sendRequest(); + return; + case "cancel": + // Remove that file from every file widget... + that.et2.iterateOver(function(_widget) { + _widget.remove_file(this.my_data.data.name); + }, this, et2_file); + } + }, + _data.uploaded[file].confirm === "is_dir" ? + this.egw.lang("There's already a directory with that name!") : + this.egw.lang('Do you want to overwrite existing file %1 in directory %2?', _data.uploaded[file].name, _data.path), + this.egw.lang('File %1 already exists', _data.uploaded[file].name), + _data.uploaded[file].name, buttons, file); + // setting required data for callback in as my_data + dialog.my_data = { + action: _data.action, + file: file, + path: _data.path, + data: _data.uploaded[file], + props: _data.props + }; + } + } + }, + + /** + * Prompt user for directory to create + * + * @param {egwAction|undefined|jQuery.Event} action Action, event or undefined if called directly + * @param {egwActionObject[] | undefined} selected Selected row, or undefined if called directly + */ + createdir: function(action, selected) + { + var self = this; + et2_dialog.show_prompt(function(button, dir){ + if (button && dir) + { + var path = self.get_path(); + self.egw.json('EGroupware\\Api\\Etemplate\\Widget\\Vfs::ajax_create_dir', [dir, path], function(msg){ + self.egw.message(msg); + self.change_dir((path == '/' ? '' : path)+'/'+ dir); + }).sendRequest(false); + } + },this.egw.lang('New directory'),this.egw.lang('Create directory')); + }, + + /** + * Change directory + * + * @param {string} _dir directory to change to incl. '..' for one up + * @param {et2_widget} widget + */ + change_dir: function(_dir, widget) + { + switch (_dir) + { + case '..': + _dir = this.dirname(this.get_path()); + break; + + } + + this.path_widget.set_value(_dir); + }, + + /** + * Row or filename in select-file dialog clicked + * + * @param {jQuery.event} event + * @param {et2_widget} widget + */ + select_clicked: function(event, widget) + { + if (!widget || typeof widget.value != 'object') + { + + } + else if (widget.value.is_dir) // true for "httpd/unix-directory" and "egw/*" + { + var path = null; + // Cannot do this, there are multiple widgets named path + // widget.getRoot().getWidgetById("path"); + widget.getRoot().iterateOver(function(widget) { + if(widget.id == "path") path = widget; + },null, et2_textbox); + if(path) + { + path.set_value(widget.value.path); + } + } + else if (this.et2 && this.et2.getArrayMgr('content').getEntry('mode') != 'open-multiple') + { + var editfield = this.et2.getWidgetById('name'); + if(editfield) + { + editfield.set_value(widget.value.name); + } + } + else + { + var file = widget.value.name; + widget.getParent().iterateOver(function(widget) + { + if(widget.options.selected_value == file) + { + widget.set_value(widget.get_value() == file ? widget.options.unselected_value : file); + } + }, null, et2_checkbox); + + } + // Stop event or it will toggle back off + event.preventDefault(); + event.stopPropagation(); + return false; + }, + + /** + * Handles action and offer it to the submit + * + * @param {string} action action name + * @param {object} widget widget which action was called from + */ + do_action: function (action, widget) + { + if (!action) return; + var field = '', value = ''; + switch (action) + { + case 'path': field = 'path'; value = widget.getValue(); break; + case 'home': field = 'action'; value = 'home'; break; + case 'app': field = 'app'; value = widget.getValue(); break; + } + this.submit(field, value); + }, + + /** + * Sumbits content value after modification + * + * @param {string} _field content field to be modified + * @param {any} _val value of field + * @param {function} _callback + */ + submit: function(_field, _val, _callback) + { + var arrMgrs = this.et2.getArrayMgrs(); + arrMgrs.content.data[_field] = _val; + if (_field == 'dir') arrMgrs.content.data['button'] = 'ok'; + jQuery.extend(arrMgrs.content.data, arrMgrs.modifications.data); + this.et2.setArrayMgrs(arrMgrs); + this.vfsSelectWidget._content(arrMgrs.content.data, _callback); + }, + + /** + * + * @param {type} _widget + * @returns {undefined} + * @todo: implementation of upload file + */ + uploaded: function (_widget) + { + + } +}); diff --git a/api/src/Etemplate/Widget/Link.php b/api/src/Etemplate/Widget/Link.php index 9b302a237e..195386549d 100644 --- a/api/src/Etemplate/Widget/Link.php +++ b/api/src/Etemplate/Widget/Link.php @@ -247,7 +247,7 @@ class Link extends Etemplate\Widget /** * Symlink an existing file in filemanager */ - public static function link_existing($app_id, $files) + public static function ajax_link_existing($app_id, $files) { list($app, $id, $dest_file) = explode(':', $app_id); diff --git a/api/src/Etemplate/Widget/Vfs.php b/api/src/Etemplate/Widget/Vfs.php index f131785a0e..974bc4fc44 100644 --- a/api/src/Etemplate/Widget/Vfs.php +++ b/api/src/Etemplate/Widget/Vfs.php @@ -14,6 +14,8 @@ namespace EGroupware\Api\Etemplate\Widget; use EGroupware\Api\Etemplate; +use EGroupware\Api\Framework; +use EGroupware\Api\Json; use EGroupware\Api; /** @@ -368,4 +370,208 @@ class Vfs extends File if (!empty($relpath)) $path .= '/'.$relpath; return $path; } + + /** + * This function behaves like etemplate app function for set/get content of + * VFS Select Widget UI + * + * There are the following ($params) parameters: + * + * - mode=(open|open-multiple|saveas|select-dir) (required) + * - method=app.class.method (optional callback, gets called with id and selected file(s)) + * - id=... (optional parameter passed to callback) + * - path=... (optional start path in VFS) + * - mime=... (optional mime-type to limit display to given type) + * - label=... (optional label for submit button, default "Open") + * + * @param array $content + * @param array $params + * @throws Api\Exception\WrongParameter + */ + public static function ajax_vfsSelect_content (array $content=null, $params = null) + { + $response = Json\Response::get(); + $readonlys = $sel_options = array(); + if (!is_array($content)) + { + $content = array_merge($params, array( + 'label' => isset($params['label']) ? $params['label'] : lang('Open'), + 'name' => (string)$params['name'], + 'path' => empty($params['path']) ? + Api\Cache::getSession('filemanger', 'select_path'): $params['path'], + )); + + if (!in_array($content['mode'],array('open','open-multiple','saveas','select-dir'))) + { + throw new Api\Exception\WrongParameter("Wrong or unset required mode parameter!"); + } + if (isset($content['options-mime'])) + { + $sel_options['mime'] = array(); + foreach((array)$params['mime'] as $key => $value) + { + if (is_numeric($key)) + { + $sel_options['mime'][$value] = lang('%1 files',strtoupper(Api\MimeMagic::mime2ext($value))).' ('.$value.')'; + } + else + { + $sel_options['mime'][$key] = lang('%1 files',strtoupper($value)).' ('.$key.')'; + } + } + list($content['mime']) = each($sel_options['mime']); + } + } + elseif(isset($content['action'])) + { + $action = $content['action']; + unset($content['action']); + switch($action) + { + case 'home': + $content['path'] = \EGroupware\Api\Vfs::get_home_dir(); + break; + } + } + if (!empty($content['app']) && $content['old_app'] != $content['app']) + { + $content['path'] = $content['app'] == 'home'? \EGroupware\Api\Vfs::get_home_dir(): + '/apps/'.$content['app']; + } + + $favorites_flag = substr($content['path'],0,strlen('/apps/favorites')) == '/apps/favorites'; + if (!$favorites_flag && (!$content['path'] || !\EGroupware\Api\Vfs::is_dir($content['path']))) + { + $content['path'] = \EGroupware\Api\Vfs::get_home_dir(); + } + elseif ($favorites_flag) + { + // Display favorites as if they were folders + $files = array(); + $favorites = \EGroupware\Api\Framework\Favorites::get_favorites('filemanager'); + $n = 0; + foreach($favorites as $favorite) + { + $path = $favorite['state']['path']; + // Just directories + if(!$path) continue; + if ($path == $content['path']) continue; // remove directory itself + + $mime = \EGroupware\Api\Vfs::mime_content_type($path); + $content['dir'][$n] = array( + 'name' => $favorite['name'], + 'path' => $path, + 'mime' => $mime, + 'is_dir' => true + ); + if ($content['mode'] == 'open-multiple') + { + $readonlys['selected['.$favorite['name'].']'] = true; + } + ++$n; + } + } + else if (!($files = \EGroupware\Api\Vfs::find($content['path'],array( + 'dirsontop' => true, + 'order' => 'name', + 'sort' => 'ASC', + 'maxdepth' => 1, + )))) + { + $content['msg'] = lang("Can't open directory %1!",$content['path']); + } + else + { + $n = 0; + $content['dir'] = array('mode' => $content['mode']); + foreach($files as $path) + { + if ($path == $content['path']) continue; // remove directory itself + + $name = \EGroupware\Api\Vfs::basename($path); + $is_dir = \EGroupware\Api\Vfs::is_dir($path); + $mime = \EGroupware\Api\Vfs::mime_content_type($path); + if ($content['mime'] && !$is_dir && $mime != $content['mime']) + { + continue; // does not match mime-filter --> ignore + } + $content['dir'][$n] = array( + 'name' => $name, + 'path' => $path, + 'mime' => $mime, + 'is_dir' => $is_dir + ); + if ($is_dir && $content['mode'] == 'open-multiple') + { + $readonlys['selected['.$name.']'] = true; + } + ++$n; + } + if (!$n) $readonlys['selected[]'] = true; // remove checkbox from empty line + } + $readonlys = array_merge($readonlys, array( + 'createdir' => !\EGroupware\Api\Vfs::is_writable($content['path']), + 'upload_file' => !\EGroupware\Api\Vfs::is_writable($content['path']) || + !in_array($content['mode'],array('open', 'open-multiple')), + )); + + $sel_options = array_merge($sel_options, array( + 'app' => self::get_apps() + )); + Api\Cache::setSession('filemanger', 'select_path', $content['path']); + // Response + $response->data(array( + 'content' => $content, + 'sel_options' => $sel_options, + 'readonlys' => $readonlys, + 'modifications' => array ( + 'mode' => $content['mode'], + 'method' => $content['method'], + 'id' => $content['id'], + 'label' => $content['label'], + 'mime' => $content['mime'], + 'options-mime' => $sel_options['mime'], + 'old_path' => $content['path'], + 'old_app' => $content['app'] + ) + )); + } + + /** + * function to create directory in the given path + * + * @param type $dir name of the directory + * @param type $path path to create directory in it + */ + public static function ajax_create_dir ($dir, $path) + { + $response = Json\Response::get(); + $msg = ''; + if (!empty($dir) && !empty($path)) + { + $dst = \EGroupware\Api\Vfs::concat($path, $dir); + if (\EGroupware\Api\Vfs::mkdir($dst, null, STREAM_MKDIR_RECURSIVE)) + { + $msg = lang("Directory successfully created."); + } + $msg = lang("Error while creating directory."); + } + $response->data($msg); + } + + /** + * Get a list off all apps having an application directory in VFS + * + * @return array + */ + static function get_apps() + { + $apps = array(false); // index starting from 1 + if (isset($GLOBALS['egw_info']['apps']['stylite'])) $apps = array('favorites' => lang('Favorites')); + $apps += \EGroupware\Api\Link::app_list('query'); + // they do NOT support adding files to VFS + unset($apps['addressbook-email'], $apps['mydms'], $apps['wiki'], + $apps['api-accounts']); + return $apps; + } } diff --git a/api/templates/default/etemplate2.css b/api/templates/default/etemplate2.css index bb69165717..924562e1ea 100644 --- a/api/templates/default/etemplate2.css +++ b/api/templates/default/etemplate2.css @@ -1286,7 +1286,7 @@ button.et2_vfs_btn { background-position: 4px center; background-size: 20px auto; } -img.vfsMimeIcon, #filemanager-select img.vfsMimeIcon { +img.vfsMimeIcon, #api\.vfsSelectUI img.vfsMimeIcon { max-height: 16px; height: auto; } @@ -1945,7 +1945,9 @@ div.ui-dialog div.ui-dialog-content > div[id] { padding-left: 3px; background-image: none; } -.et2_toolbar button, .nextmatch_header_row button, .et2_toolbar div.et2_progress, .nextmatch_header_row div.et2_progress { +.et2_toolbar button, .nextmatch_header_row button, +.et2_toolbar div.et2_progress, +.nextmatch_header_row div.et2_progress { margin: 1px 4px 1px 0; height: 24px; border-radius: 3px; @@ -2827,4 +2829,40 @@ div.eml div.emlDelete { div.eml div.emlDelete:hover, div.eml div.emlEdit:hover { opacity:1; filter:contrast(9); -} \ No newline at end of file +} + +.vfs-select-container .et2_toolbar *{ + float: left; +} +.vfs-select-toggle {display:none;} +.vfs-select-app { + display: inline-block; + padding: 5px; +} +.vfs-select-container .selectFiles { + height: 250px; + overflow-x: hidden; + overflow-y: auto; +} +.vfs-select-container div.et2_file span { + width: 22px !important; + height: 22px !important; + padding: 0; + background-position-x: 3px !important; + background-image: url(../../../pixelegg/images/add.png); + overflow: hidden !important; + text-indent: 100px !important; +} +.vfs-select-container div.et2_file { + width: 24px; +} +.vfs-select-container div.et2_file .progress { + width: 252px; + max-height: 12em; + overflow: auto; + padding: 0px; + float: right !important; + margin-left: 0; +} +#api\.vfsSelectUI_app {width:205px} +#api\.vfsSelectUI_name {padding-right: 4px;} \ No newline at end of file diff --git a/api/templates/default/vfsSelectUI.xet b/api/templates/default/vfsSelectUI.xet new file mode 100644 index 0000000000..1f4a0cc4d9 --- /dev/null +++ b/api/templates/default/vfsSelectUI.xet @@ -0,0 +1,52 @@ + + + + + + diff --git a/filemanager/inc/class.filemanager_select.inc.php b/filemanager/inc/class.filemanager_select.inc.php deleted file mode 100644 index fc84a91ac2..0000000000 --- a/filemanager/inc/class.filemanager_select.inc.php +++ /dev/null @@ -1,366 +0,0 @@ - - * @copyright (c) 2009-2016 by Ralf Becker - * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License - * @version $Id$ - */ - -use EGroupware\Api; -use EGroupware\Api\Link; -use EGroupware\Api\Framework; -use EGroupware\Api\Egw; -use EGroupware\Api\Vfs; -use EGroupware\Api\Etemplate; - -/** - * Select file to open or save dialog - * - * This dialog can be called from applications to open or store files from the VFS. - * - * There are the following ($_GET) parameters: - * - menuaction=filemanager.filemanager_select.select (required) - * - mode=(open|open-multiple|saveas|select-dir) (required) - * - method=app.class.method (required callback, gets called with id and selected file(s)) - * - id=... (optional parameter passed to callback) - * - path=... (optional start path in VFS) - * - mime=... (optional mime-type to limit display to given type) - * - label=... (optional label for submit button, default "Open") - * - * The application calls this method in a popup with size: 640x580 px - * After the user selected one or more files (depending on the mode parameter), the "method" callback gets - * called on server (!) side. Parameters are the id plus the selected files as 1. and 2. parameter. - * The callback returns javascript to eg. update it's UI AND (!) to close the current popup ("window.close();"). - */ -class filemanager_select -{ - /** - * Methods callable via menuaction - * - * @var array - */ - var $public_functions = array( - 'select' => true, - ); - - /** - * Constructor - * - */ - function __construct() - { - // strip slashes from _GET parameters, if someone still has magic_quotes_gpc on - if (get_magic_quotes_gpc() && $_GET) - { - $_GET = array_stripslashes($_GET); - } - } - - /** - * File selector - * - * @param array $content - */ - function select(array $content=null) - { - if (!is_array($content)) - { - $content = array(); - // recover from a failed upload in CkEditor, eg. > max_uploadsize - if ($_GET['failed_upload'] && $_GET['msg']) - { - $content['msg'] = $_GET['msg']; - $_GET['mode'] = 'open'; - $_GET['method'] = 'ckeditor_return'; - $_GET['CKEditorFuncNum'] = Api\Cache::getSession('filemanager','ckeditorfuncnum'); - } - $content['mode'] = $_GET['mode']; - if (!in_array($content['mode'],array('open','open-multiple','saveas','select-dir'))) - { - throw new Api\Exception\WrongParameter("Wrong or unset required mode parameter!"); - } - $content['path'] = $_GET['path']; - if (empty($content['path'])) - { - $content['path'] = Api\Cache::getSession('filemanger', 'select_path'); - } - $content['name'] = (string)$_GET['name']; - $content['method'] = $_GET['method']; - if ($content['method'] == 'ckeditor_return') - { - if (isset($_GET['CKEditorFuncNum']) && is_numeric($_GET['CKEditorFuncNum'])) - { - Api\Cache::setSession('filemanager','ckeditorfuncnum', - $content['ckeditorfuncnum'] = $_GET['CKEditorFuncNum']); - } - else - { - throw new Api\Exception\WrongParameter("chkeditor_return has been specified as a method but some parameters are missing or invalid."); - } - } - $content['id'] = $_GET['id']; - $content['label'] = isset($_GET['label']) ? $_GET['label'] : lang('Open'); - if (($content['options-mime'] = isset($_GET['mime']))) - { - $sel_options['mime'] = array(); - foreach((array)$_GET['mime'] as $key => $value) - { - if (is_numeric($key)) - { - $sel_options['mime'][$value] = lang('%1 files',strtoupper(Api\MimeMagic::mime2ext($value))).' ('.$value.')'; - } - else - { - $sel_options['mime'][$key] = lang('%1 files',strtoupper($value)).' ('.$key.')'; - } - } - - list($content['mime']) = each($sel_options['mime']); - error_log(array2string($content['options-mime'])); - } - } - elseif(isset($content['button'])) - { - list($button) = each($content['button']); - unset($content['button']); - switch($button) - { - case 'home': - $content['path'] = filemanager_ui::get_home_dir(); - break; - case 'ok': - $copy_result = null; - if (isset($content['file_upload']['name']) && file_exists($content['file_upload']['tmp_name'])) - { - //Set the "content" name filed accordingly to the uploaded file - // encode chars which special meaning in url/vfs (some like / get removed!) - $content['name'] = Vfs::encodePathComponent($content['file_upload']['name']); - $to_path = Vfs::concat($content['path'],$content['name']); - - $copy_result = (Vfs::is_writable($content['path']) || Vfs::is_writable($to_path)) && - copy($content['file_upload']['tmp_name'],Vfs::PREFIX.$to_path); - } - - //Break on an error condition - if ((($content['mode'] == 'open' || $content['mode'] == 'saveas') && ($content['name'] == '')) || ($copy_result === false)) - { - if ($copy_result === false) - { - $content['msg'] = lang('Error uploading file!'); - } - else - { - $content['msg'] = lang('Filename must not be empty!'); - } - $content['name'] = ''; - - break; - } - - switch($content['mode']) - { - case 'open-multiple': - foreach((array)$content['dir']['selected'] as $name) - { - $files[] = Vfs::concat($content['path'],$name); - } - //Add an uploaded file to the files result array2string - if ($copy_result === true) $files[] = $to_path; - break; - - case 'select-dir': - $files = $content['path']; - break; - - case 'saveas': - // Don't trust the name the user gives, encode it - $content['name'] = Vfs::encodePathComponent($content['name']); - // Fall through - - default: - $files = Vfs::concat($content['path'],$content['name']); - break; - } - - if ($content['method'] && $content['method'] != 'ckeditor_return') - { - $js = ExecMethod2($content['method'],$content['id'],$files); - } - else if ($content['method'] == 'ckeditor_return') - { - $download_url = Vfs::download_url(Vfs::concat($content['path'],$content['name'])); - if ($download_url[0] == '/') $download_url = Egw::link($download_url); - - $response = Api\Json\Response::get(); - $response->apply('window.opener.CKEDITOR.tools.callFunction', array( - $content['ckeditorfuncnum'], - str_replace("'", "\\'", $download_url) - )); - Framework::window_close(); - exit(); - } - if(Api\Json\Response::isJSONResponse()) - { - $response = Api\Json\Response::get(); - if($js) - { - $response->script($js); - } - // Ahh! - // The vfs-select widget looks for this - $response->script('this.selected_files = '.json_encode($files) . ';'); - Framework::window_close(); - } - else - { - header('Content-type: text/html; charset='.Api\Translation::charset()); - echo "\n\n\n\n\n"; - } - exit(); - } - - $sel_options['mime'] = $content['options-mime']; - } - elseif(isset($content['apps'])) - { - list($app) = each($content['apps']); - if ($app == 'home') $content['path'] = filemanager_ui::get_home_dir(); - } - - //Deactivate the opload field if the current directory is not writeable or - //we're currently not in the single file open mode. - $content['no_upload'] = !Vfs::is_writable($content['path']) || - !in_array($content['mode'],array('open')); - - $content['apps'] = array_keys(self::get_apps()); - - if (isset($app)) - { - $content['path'] = '/apps/'.(isset($content['apps'][$app]) ? $content['apps'][$app] : $app); - } - - // Set a flag for easy detection as we go - $favorites_flag = substr($content['path'],0,strlen('/apps/favorites')) == '/apps/favorites'; - - if (!$favorites_flag && (!$content['path'] || !Vfs::is_dir($content['path']))) - { - $content['path'] = filemanager_ui::get_home_dir(); - } - $tpl = new Etemplate('filemanager.select'); - - if ($favorites_flag) - { - // Display favorites as if they were folders - $files = array(); - $favorites = Framework\Favorites::get_favorites('filemanager'); - $n = 0; - foreach($favorites as $favorite) - { - $path = $favorite['state']['path']; - // Just directories - if(!$path) continue; - if ($path == $content['path']) continue; // remove directory itself - - $mime = Vfs::mime_content_type($path); - $content['dir'][$n] = array( - 'name' => $favorite['name'], - 'path' => $path, - 'mime' => $mime, - 'is_dir' => true - ); - if ($content['mode'] == 'open-multiple') - { - $readonlys['selected['.$favorite['name'].']'] = true; - } - ++$n; - } - } - else if (!($files = Vfs::find($content['path'],array( - 'dirsontop' => true, - 'order' => 'name', - 'sort' => 'ASC', - 'maxdepth' => 1, - )))) - { - $content['msg'] = lang("Can't open directory %1!",$content['path']); - } - else - { - $n = 0; - $content['dir'] = array('mode' => $content['mode']); - foreach($files as $path) - { - if ($path == $content['path']) continue; // remove directory itself - - $name = Vfs::basename($path); - $is_dir = Vfs::is_dir($path); - $mime = Vfs::mime_content_type($path); - if ($content['mime'] && !$is_dir && $mime != $content['mime']) - { - continue; // does not match mime-filter --> ignore - } - $content['dir'][$n] = array( - 'name' => $name, - 'path' => $path, - 'mime' => $mime, - 'is_dir' => $is_dir - ); - if ($is_dir && $content['mode'] == 'open-multiple') - { - $readonlys['selected['.$name.']'] = true; - } - ++$n; - } - if (!$n) $readonlys['selected[]'] = true; // remove checkbox from empty line - } - $readonlys['button[createdir]'] = !Vfs::is_writable($content['path']); - - //_debug_array($readonlys); - Api\Cache::setSession('filemanger', 'select_path', $content['path']); - $preserve = array( - 'mode' => $content['mode'], - 'method' => $content['method'], - 'id' => $content['id'], - 'label' => $content['label'], - 'mime' => $content['mime'], - 'options-mime' => $sel_options['mime'], - 'old_path' => $content['path'], - ); - - if (isset($content['ckeditorfuncnum'])) - { - $preserve['ckeditorfuncnum'] = $content['ckeditorfuncnum']; - $preserve['ckeditor'] = $content['ckeditor']; - } - - // tell framework we need inline javascript for ckeditor_return - if ($content['method'] == 'ckeditor_return') - { - Api\Header\ContentSecurityPolicy::add('script-src', 'unsafe-inline'); - } - $tpl->exec('filemanager.filemanager_select.select',$content,$sel_options,$readonlys,$preserve,2); - } - - /** - * Get a list off all apps having an application directory in VFS - * - * @return array - */ - static function get_apps() - { - $apps = array(false); // index starting from 1 - if (isset($GLOBALS['egw_info']['apps']['stylite'])) $apps = array('favorites' => lang('Favorites')); - $apps += Link::app_list('query'); - - unset($apps['mydms']); // they do NOT support adding files to VFS - unset($apps['wiki']); - unset($apps['api-accounts']); - unset($apps['addressbook-email']); - - return $apps; - } -} diff --git a/filemanager/js/collab.js b/filemanager/js/collab.js index 3990adc8ea..a010804718 100644 --- a/filemanager/js/collab.js +++ b/filemanager/js/collab.js @@ -290,31 +290,31 @@ app.classes.filemanager = app.classes.filemanager.extend({ // new file else { - // create file selector - var vfs_select = et2_createWidget('vfs-select', { + + var vfs_attrs = { id:'savefile', mode: 'saveas', button_caption:"", button_label:_egwAction.id == 'saveas'?"save as":"save", - value: "doc.odt" - }, this.et2); - - // bind change handler for setting the selected path and calling save - jQuery(vfs_select.getDOMNode()).on('change', function (){ - file_path = vfs_select.get_value(); - if (vfs_select.get_value()) - { - // Add odt extension if not exist - if (!file_path.match(/\.odt$/,'ig')) file_path += '.odt'; - widgetFilePath.set_value(file_path); - self.editor.getDocumentAsByteArray(saveByteArrayLocally); - self.editor_leaveSession(function(){ - var path = window.location.href.split('&path='); - window.location.href = path[0]+'&path='+self.editor_getFilePath(); - }); - egw.refresh('','filemanager'); + value: "doc.odt", + onchange: function (){ + file_path = vfs_select.get_value(); + if (vfs_select.get_value()) + { + // Add odt extension if not exist + if (!file_path.match(/\.odt$/,'ig')) file_path += '.odt'; + widgetFilePath.set_value(file_path); + self.editor.getDocumentAsByteArray(saveByteArrayLocally); + self.editor_leaveSession(function(){ + var path = window.location.href.split('&path='); + window.location.href = path[0]+'&path='+self.editor_getFilePath(); + }); + egw.refresh('','filemanager'); + } } - }); + }; + // create file selector + var vfs_select = et2_createWidget('vfs-select', vfs_attrs, this.et2); // start the file selector dialog jQuery(vfs_select.getDOMNode()).click(); } diff --git a/filemanager/templates/default/select.xet b/filemanager/templates/default/select.xet deleted file mode 100644 index bc39053042..0000000000 --- a/filemanager/templates/default/select.xet +++ /dev/null @@ -1,85 +0,0 @@ - - - - - -