From e709ddd29ca87dec945a21f4d2a0cbd7efdcadbe Mon Sep 17 00:00:00 2001 From: Marina Glancy Date: Wed, 2 May 2012 14:59:09 +0800 Subject: [PATCH] MDL-31901: Allow FileManager to re-use FilePicker JS code for view modes: - when loading core_filepicker we extend the Node element with functions necessary to display files list in different view modes; - repository/filepicker.js is changed to use those functions; - FileManager now has three different view modes (icon view, tree view and table view), JS code is re-used from FilePicker; - files in FileManager no longer have context menu, they have one popup Widget with different actions instead; - Added more templates for JS code to FileManager, use similar CSS class names as FilePicker; - lib/filelib.php : fixed double slashes in path, return more data about files; - lib/form/filemanager.php : pass information to FileManager about available licenses and default author; - repository/draftfiles_ajax.php : return information about directory tree for file moving UI in FileManager, return formatted information; - repository/lib.php : function repository::prepare_listing() now can work also with draftarea files listing; --- files/renderer.php | 205 ++++++-- lang/en/moodle.php | 1 + lib/filelib.php | 11 +- lib/form/filemanager.js | 840 +++++++++++++++---------------- lib/form/filemanager.php | 21 +- lib/outputrequirementslib.php | 2 +- repository/draftfiles_ajax.php | 11 +- repository/filepicker.js | 689 +++++++++++++++---------- repository/lib.php | 43 +- repository/local/lib.php | 1 + theme/base/style/filemanager.css | 78 ++- 11 files changed, 1110 insertions(+), 792 deletions(-) diff --git a/files/renderer.php b/files/renderer.php index 544364d79ac..024397f42cf 100644 --- a/files/renderer.php +++ b/files/renderer.php @@ -116,9 +116,7 @@ class core_files_renderer extends plugin_renderer_base { if (empty($filemanagertemplateloaded)) { $filemanagertemplateloaded = true; $this->page->requires->js_init_call('M.form_filemanager.set_templates', - array(array( - 'onefile' => '___fullname___ ___action___' - )), true, $module); + array($this->filemanager_js_templates()), true, $module); } $this->page->requires->js_init_call('M.form_filemanager.init', array($fm->options), true, $module); @@ -156,11 +154,21 @@ class core_files_renderer extends plugin_renderer_base { * If browser supports Drag-and-drop, the body element will have class 'dndsupported', * otherwise - 'dndnotsupported'; * - * Element with class 'fm-filelist' will be populated with files list; - * Element with class 'fm-breadcrumb' will be populated with the path or have class 'fm-empty' when empty; - * Element with class 'fm-btn-add' will hold onclick event for adding a file (opening filepicker); - * Element with class 'fm-btn-mkdir' will hold onclick event for adding new folder; - * Element with class 'fm-btn-download' will hold onclick event for download action; + * Element with class 'fp-content' will be populated with files list; + * Element with class 'fp-btn-add' will hold onclick event for adding a file (opening filepicker); + * Element with class 'fp-btn-mkdir' will hold onclick event for adding new folder; + * Element with class 'fp-btn-download' will hold onclick event for download action; + * + * Element with class 'fp-path-folder' is a template for one folder in path toolbar. + * It will hold mouse click event and will be assigned classes first/last/even/odd respectfully. + * Parent element will receive class 'empty' when there are no folders to be displayed; + * The content of subelement with class 'fp-path-folder-name' will be substituted with folder name; + * + * Element with class 'fp-viewbar' will have the class 'enabled' or 'disabled' when view mode + * can be changed or not; + * Inside element with class 'fp-viewbar' there are expected elements with classes + * 'fp-vb-icons', 'fp-vb-tree' and 'fp-vb-details'. They will handle onclick events to switch + * between the view modes, the last clicked element will have the class 'checked'; * * @param form_filemanager $fm * @return string @@ -181,30 +189,144 @@ class core_files_renderer extends plugin_renderer_base { $strdndenabledinbox = get_string('dndenabled_inbox', 'moodle'); $loading = get_string('loading', 'repository'); - $html .= << -
{$icon_progress}
-
+ $html = ' +
+
'.$icon_progress.'
+
+ / +
- - - - {$restrictions} - - $strdndenabled + + + + '.$restrictions.' + - '.$strdndenabled.' +
+ + + +
-
    -
    {$strnofilesattached} - {$strdndenabledinbox} +
    +
      +
      '.$strnofilesattached.' + '.$strdndenabledinbox.' +
      +
      '.$strdroptoupload.'
      +
      '.$icon_progress.'
      -
      {$strdroptoupload}
      -
      {$icon_progress}
      -
      {$icon_progress}
      +
      '.$icon_progress.'
      -FMHTML; - return $html; +'; + return preg_replace('/\{\!\}/', '', $html); + } + + /** + * FileManager JS template for displaying one file in 'icon view' mode. + * + * @see fp_js_template_iconfilename() + * @return string + */ + private function fm_js_template_iconfilename() { + return $this->fp_js_template_iconfilename(); + } + + /** + * FileManager JS template for displaying file name in 'table view' and 'tree view' modes. + * + * @see fp_js_template_listfilename() + * @return string + */ + private function fm_js_template_listfilename() { + return $this->fp_js_template_listfilename(); + } + + /** + * FileManager JS template for window with file information/actions. + * + * All content must be enclosed in an element with class 'fp-select', CSS for this class + * must define width and height of the window; + * + * Thumbnail image will be added as content to the element with class 'fp-thumbnail'; + * + * Inside the window the elements with the following classnames must be present: + * 'fp-saveas', 'fp-author', 'fp-license', 'fp-path'. Inside each of them must be + * one input element (or select in case of fp-license and fp-path). They may also have labels. + * The elements will be assign with class 'uneditable' and input/select element will become + * disabled if they are not applicable for the particular file; + * + * There may be present elements with classes 'fp-origpath', 'fp-datemodified', 'fp-datecreated', + * 'fp-size', 'fp-dimensions'. They will receive additional class 'fp-unknown' if information + * is unavailable. If there is information available, the content of embedded element + * with class 'fp-value' will be substituted with the value; + * + * Elements with classes 'fp-file-update', 'fp-file-download', 'fp-file-delete', 'fp-file-zip', + * 'fp-file-unzip', 'fp-file-setmain' and 'fp-file-cancel' will hold corresponding onclick + * events (there may be several elements with class 'fp-file-cancel'); + * + * When confirm button is pressed and file is being selected, the top element receives + * additional class 'loading'. It is removed when response from server is received. + * + * When any of the input fields is changed, the top element receives class 'fp-changed'; + * When current file can be set as main - top element receives class 'fp-cansetmain'; + * When current file is folder/zip/file - top element receives respectfully class + * 'fp-folder'/'fp-zip'/'fp-file'; + * + * @return string + */ + private function fm_js_template_fileselectlayout() { + $rv = '
      +
      + +

      '.get_string('loading', 'repository').'

      +
      +
      +

      + + + + + + + + + + + +
      :
      :
      :
      :
      :
      +

      + + + + + + +

      +
      +
      '.get_string('lastmodified', 'moodle').':
      +
      '.get_string('datecreated', 'repository').':
      +
      '.get_string('size', 'repository').':
      +
      '.get_string('dimensions', 'repository').':
      +
      '; + return preg_replace('/\{\!\}/', '', $rv); + } + + /** + * Returns all FileManager JavaScript templates as an array. + * + * @return array + */ + public function filemanager_js_templates() { + $class_methods = get_class_methods($this); + $templates = array(); + foreach ($class_methods as $method_name) { + if (preg_match('/^fm_js_template_(.*)$/', $method_name, $matches)) + $templates[$matches[1]] = $this->$method_name(); + } + return $templates; } /** @@ -251,10 +373,10 @@ FMHTML; * * Element with class 'fp-paging' will contain page navigation (will be deprecated soon); * - * Element with class 'fp-path-folder' will contain template for one folder in path toolbar. + * Element with class 'fp-path-folder' is a template for one folder in path toolbar. * It will hold mouse click event and will be assigned classes first/last/even/odd respectfully. - * The content of element with class 'fp-path-folder-name' will be substituted with folder name; * Parent element will receive class 'empty' when there are no folders to be displayed; + * The content of subelement with class 'fp-path-folder-name' will be substituted with folder name; * * Element with class 'fp-toolbar' will have class 'empty' if all 'Back', 'Search', 'Refresh', * 'Logout', 'Manage' and 'Help' are unavailable for this repo; @@ -310,10 +432,7 @@ FMHTML; } /** - * FilePicker JS template for displaying list of files in 'icon view' mode. - * - * Element with class 'fp-file' is a template for displaying one file and indicates a place - * where files shall be output. It also will hold mouse events (click, over, out, etc.); + * FilePicker JS template for displaying one file in 'icon view' mode. * * the element with class 'fp-thumbnail' will be resized to the repository thumbnail size * (both width and height, unless min-width and/or min-height is set in CSS) and the content of @@ -323,15 +442,17 @@ FMHTML; * (unless min-width is set in css) and the content of an element will be replaced with filename * supplied by repository; * + * top element(s) will have class fp-folder if the element is a folder; + * + * List of files will have parent
      element with class 'fp-iconview' + * * @return string */ - private function fp_js_template_iconview() { - $rv = '
      -
      + private function fp_js_template_iconfilename() { + $rv = '
      -
      -
      '; +
      '; return preg_replace('/\{\!\}/', '', $rv); } @@ -343,6 +464,8 @@ FMHTML; * content of element with class 'fp-filename' will be replaced with filename supplied by * repository; * + * top element(s) will have class fp-folder if the element is a folder; + * * Note that tree view and table view are the YUI widgets and therefore there are no * other templates. The widgets will be wrapped in
      with class fp-treeview or * fp-tableview (respectfully). @@ -350,7 +473,7 @@ FMHTML; * @return string */ private function fp_js_template_listfilename() { - $rv = ' '; + $rv = ' '; return preg_replace('/\{\!\}/', '', $rv); } @@ -379,7 +502,7 @@ FMHTML; } /** - * Template for window appearing to select a file. + * FilePicker JS template for window appearing to select a file. * * All content must be enclosed in an element with class 'fp-select', CSS for this class * must define width and height of the window; @@ -393,9 +516,9 @@ FMHTML; * disabled if they are not applicable for the particular file; * * There may be present elements with classes 'fp-datemodified', 'fp-datecreated', 'fp-size', - * 'fp-license', 'fp-author'. They will receive additional class 'fp-unknown' if information - * is unavailable. If there is information available, the content of embedded element - * with class 'fp-value' will be substituted with the value; + * 'fp-license', 'fp-author', 'fp-dimensions'. They will receive additional class 'fp-unknown' + * if information is unavailable. If there is information available, the content of embedded + * element with class 'fp-value' will be substituted with the value; * * Elements with classes 'fp-select-confirm' and 'fp-select-cancel' will hold corresponding * onclick events; diff --git a/lang/en/moodle.php b/lang/en/moodle.php index b7cc79e707c..fece43f2a2e 100644 --- a/lang/en/moodle.php +++ b/lang/en/moodle.php @@ -1242,6 +1242,7 @@ $string['olduserdirectory'] = 'This is the OLD users directory, and is no longer $string['opentoguests'] = 'Guest access'; $string['optional'] = 'optional'; $string['order'] = 'Order'; +$string['originalpath'] = 'Original path'; $string['orphanedactivities'] = 'Orphaned activities'; $string['other'] = 'Other'; $string['outline'] = 'Outline'; diff --git a/lib/filelib.php b/lib/filelib.php index 78af2871e18..29307f9e57d 100644 --- a/lib/filelib.php +++ b/lib/filelib.php @@ -566,13 +566,13 @@ function file_get_drafarea_files($draftitemid, $filepath = '/') { $data->path[] = array('name'=>get_string('files'), 'path'=>'/'); // will be used to build breadcrumb - $trail = ''; + $trail = '/'; if ($filepath !== '/') { $filepath = file_correct_filepath($filepath); $parts = explode('/', $filepath); foreach ($parts as $part) { if ($part != '' && $part != null) { - $trail .= ('/'.$part.'/'); + $trail .= ($part.'/'); $data->path[] = array('name'=>$part, 'path'=>$trail); } } @@ -587,11 +587,16 @@ function file_get_drafarea_files($draftitemid, $filepath = '/') { $item->filepath = $file->get_filepath(); $item->fullname = trim($item->filename, '/'); $filesize = $file->get_filesize(); + $item->size = $filesize ? $filesize : null; $item->filesize = $filesize ? display_size($filesize) : ''; $icon = mimeinfo_from_type('icon', $file->get_mimetype()); $item->icon = $OUTPUT->pix_url('f/' . $icon)->out(); $item->sortorder = $file->get_sortorder(); + $item->author = $file->get_author(); + $item->license = $file->get_license(); + $item->datemodified = $file->get_timemodified(); + $item->datecreated = $file->get_timecreated(); if ($icon == 'zip') { $item->type = 'zip'; @@ -605,9 +610,11 @@ function file_get_drafarea_files($draftitemid, $filepath = '/') { $item->type = 'folder'; $foldername = explode('/', trim($item->filepath, '/')); $item->fullname = trim(array_pop($foldername), '/'); + $item->thumbnail = $OUTPUT->pix_url('f/folder-32')->out(false); } else { // do NOT use file browser here! $item->url = moodle_url::make_draftfile_url($draftitemid, $item->filepath, $item->filename)->out(); + $item->thumbnail = $OUTPUT->pix_url(file_extension_icon($item->filename, 32))->out(false); } $list[] = $item; } diff --git a/lib/form/filemanager.js b/lib/form/filemanager.js index c2dec8ed24a..aa0b2ecad1a 100644 --- a/lib/form/filemanager.js +++ b/lib/form/filemanager.js @@ -80,6 +80,7 @@ M.form_filemanager.init = function(Y, options) { } else { this.filecount = 0; } + // prepare filemanager for drag-and-drop upload this.filemanager = Y.one('#filemanager-'+options.client_id); if (this.filemanager.hasClass('filemanager-container') || !this.filemanager.one('.filemanager-container')) { this.dndcontainer = this.filemanager; @@ -89,7 +90,31 @@ M.form_filemanager.init = function(Y, options) { this.dndcontainer.generateID(); } } + // save template for one path element and location of path bar + if (this.filemanager.one('.fp-path-folder')) { + this.pathnode = this.filemanager.one('.fp-path-folder'); + this.pathbar = this.pathnode.get('parentNode'); + this.pathbar.removeChild(this.pathnode); + } + // initialize 'select file' panel + var fpselectnode = Y.Node.create(M.form_filemanager.templates.fileselectlayout); + this.filemanager.appendChild(fpselectnode); + this.selectui = new Y.Panel({ + srcNode : fpselectnode, + zIndex : 600000, + centered : true, + modal : true, + close : true, + render : true + }); + this.selectui.hide(); + this.setup_select_file(); + // setup buttons onclick events this.setup_buttons(); + // display files + this.viewmode = 1; // TODO take from cookies? + this.filemanager.all('.fp-vb-icons,.fp-vb-tree,.fp-vb-details').removeClass('checked') + this.filemanager.all('.fp-vb-icons').addClass('checked') this.refresh(this.currentpath); // MDL-31113 get latest list from server }, @@ -120,7 +145,18 @@ M.form_filemanager.init = function(Y, options) { alert('IO FATAL'); return; } - var data = Y.JSON.parse(o.responseText); + var data = null; + try { + data = Y.JSON.parse(o.responseText); + } catch(e) { + // TODO display error + //scope.print_msg(M.str.repository.invalidjson, 'error'); + //scope.display_error(M.str.repository.invalidjson+'
      '+stripHTML(o.responseText)+'
      ', 'invalidjson') + return; + } + if (data && data.tree && scope.set_current_tree) { + scope.set_current_tree(data.tree); + } args.callback(id,data,p); } }, @@ -177,9 +213,9 @@ M.form_filemanager.init = function(Y, options) { }, true); }, setup_buttons: function() { - var button_download = this.filemanager.one('.fm-btn-download'); - var button_create = this.filemanager.one('.fm-btn-mkdir'); - var button_addfile = this.filemanager.one('.fm-btn-add'); + var button_download = this.filemanager.one('.fp-btn-download'); + var button_create = this.filemanager.one('.fp-btn-mkdir'); + var button_addfile = this.filemanager.one('.fp-btn-add'); // setup 'add file' button // if maxfiles == -1, the no limit @@ -263,484 +299,428 @@ M.form_filemanager.init = function(Y, options) { } }); }, this); - }, - render: function() { - var options = this.options; - var path = this.options.path; - var list = this.options.list; - var breadcrumb = this.filemanager.one('.fm-breadcrumb'); - // empty breadcrumb - breadcrumb.set('innerHTML', '').addClass('fm-empty'); - // build breadcrumb - if (path && path.length) { - breadcrumb.removeClass('fm-empty'); - var count = 0; - for(var p in path) { - var arrow = ''; - if (count==0) { - arrow = Y.Node.create(''+M.str.moodle.path + ': '); - } else { - arrow = Y.Node.create(''); + + this.filemanager.all('.fp-vb-icons,.fp-vb-tree,.fp-vb-details'). + on('click', function(e) { + e.preventDefault(); + var viewbar = this.filemanager.one('.fp-viewbar') + if (!viewbar || !viewbar.hasClass('disabled')) { + this.filemanager.all('.fp-vb-icons,.fp-vb-tree,.fp-vb-details').removeClass('checked') + if (e.currentTarget.hasClass('fp-vb-tree')) { + this.viewmode = 2; + } else if (e.currentTarget.hasClass('fp-vb-details')) { + this.viewmode = 3; + } else { + this.viewmode = 1; + } + e.currentTarget.addClass('checked') + this.render(); + //Y.Cookie.set('recentviewmode', this.viewmode); } - count++; - - var pathid = 'fm-path-node-'+this.client_id; - pathid += ('-'+count); - - var crumb = Y.Node.create(''+path[p].name+''); - breadcrumb.appendChild(arrow); - breadcrumb.appendChild(crumb); - - var args = {}; - args.requestpath = path[p].path; - args.client_id = this.client_id; - Y.one('#'+pathid).on('click', function(e, args) { - var scope = this; - var params = {}; - params['filepath'] = args.requestpath; - this.currentpath = args.requestpath; - this.request({ - action: 'list', - scope: scope, - params: params, - callback: function(id, obj, args) { - scope.filecount = obj.filecount; - scope.check_buttons(); - scope.options = obj; - scope.render(obj); - } - }, true); - }, this, args); + }, this); + }, + print_path: function() { + var p = this.options.path; + this.pathbar.setContent('').addClass('empty'); + if (p && p.length!=0 && this.viewmode != 2) { + for(var i = 0; i < p.length; i++) { + var el = this.pathnode.cloneNode(true); + this.pathbar.appendChild(el); + if (i == 0) {el.addClass('first');} + if (i == p.length-1) {el.addClass('last');} + if (i%2) {el.addClass('even');} else {el.addClass('odd');} + el.one('.fp-path-folder-name').setContent(p[i].name). + on('click', function(e, path) { + e.preventDefault(); + var scope = this; + this.currentpath = path; + this.request({ + action: 'list', + scope: scope, + params: {filepath:path}, + callback: function(id, obj, args) { + scope.filecount = obj.filecount; + scope.check_buttons(); + scope.options = obj; + scope.render(obj); + } + }, true); + }, this, p[i].path); + } + this.pathbar.removeClass('empty'); + } + }, + get_filepath: function(obj) { + if (obj.path && obj.path.length) { + return obj.path[obj.path.length-1].path; + } + return ''; + }, + treeview_dynload: function(node, cb) { + var retrieved_children = {}; + if (node.children) { + for (var i in node.children) { + retrieved_children[node.children[i].path] = node.children[i]; } } - var listhtml = ''; - - // folder list items - var folder_ids = []; - var folder_data = {}; - - // normal file list items - var file_ids = []; - var file_data = {}; - - // archives list items - var zip_ids = []; - var zip_data = {}; - - var html_ids = []; - var html_data = {}; - - file_data.itemid = folder_data.itemid = zip_data.itemid = options.itemid; - file_data.client_id = folder_data.client_id = zip_data.client_id = this.client_id; - - var foldername_ids = []; + this.request({ + action:'list', + params: {filepath:node.path?node.path:''}, + scope:this, + callback: function(id, obj, args) { + var list = obj.list; + var scope = args.scope; + // check that user did not leave the view mode before recieving this response + if (!(scope.viewmode == 2 && node && node.getChildrenEl())) { + return; + } + if (cb != null) { // (in manual mode do not update current path) + scope.options = obj; + } + node.highlight(false); + node.origlist = obj.list?obj.list:null; + node.origpath = obj.path?obj.path:null; + node.children = []; + for(k in list) { + if (list[k].type == 'folder' && retrieved_children[list[k].filepath]) { + // if this child is a folder and has already been retrieved + node.children[node.children.length] = retrieved_children[list[k].filepath]; + } else { + // append new file to the list + scope.view_files([list[k]]); + } + } + if (cb == null) { + node.refresh(); + } else { + // invoke callback requested by TreeView component + cb(); + } + //scope.content_scrolled(); + } + }, false); + }, + view_files: function(appendfiles) { this.filemanager.removeClass('fm-updating').removeClass('fm-noitems'); - if (!list || list.length == 0) { + if ((appendfiles == null) && (!this.options.list || this.options.list.length == 0)) { this.filemanager.addClass('fm-noitems'); return; } - - var count = 0; - for(var i in list) { - count++; - // the li html element - var htmlid = 'fileitem-'+this.client_id+'-'+count; - // link to file - var fileid = 'filename-'+this.client_id+'-'+count; - // file menu - var action = 'action-' +this.client_id+'-'+count; - - var html = M.form_filemanager.templates.onefile; - - html_ids.push('#'+htmlid); - html_data[htmlid] = action; - - list[i].htmlid = htmlid; - list[i].fileid = fileid; - list[i].action = action; - - var url = "###"; - - switch (list[i].type) { - case 'folder': - // click folder name - foldername_ids.push('#'+fileid); - // click folder menu - folder_ids.push('#'+action); - folder_data[action] = list[i]; - folder_data[fileid] = list[i]; - break; - case 'file': - file_ids.push('#'+action); - // click file name - file_ids.push('#'+fileid); - file_data[action] = list[i]; - file_data[fileid] = list[i]; - if (list[i].url) { - url = list[i].url; - } - break; - case 'zip': - zip_ids.push('#'+action); - zip_ids.push('#'+fileid); - zip_data[action] = list[i]; - zip_data[fileid] = list[i]; - if (list[i].url) { - url = list[i].url; - } - break; + var list = (appendfiles != null) ? appendfiles : this.options.list; + var element_template; + if (this.viewmode == 2 || this.viewmode == 3) { + element_template = Y.Node.create(M.form_filemanager.templates.listfilename); + } else { + this.viewmode = 1; + element_template = Y.Node.create(M.form_filemanager.templates.iconfilename); + } + var options = { + viewmode : this.viewmode, + appendonly : appendfiles != null, + filenode : element_template, + callbackcontext : this, + callback : function(e, node) { + e.preventDefault(); + if (node.type == 'folder') { + this.refresh(node.filepath); + } else { + this.select_file(node); + } + }, + rightclickcallback : function(e, node) { + this.select_file(node); + e.preventDefault(); } - var fullname = list[i].fullname; + }; + if (this.viewmode == 2) { + options.dynload = true; + options.filepath = this.options.path; + options.treeview_dynload = this.treeview_dynload; + options.callback = function(e, node) { + if (node.type != 'folder') { + if (e.node.parent && e.node.parent.origpath) { + // set the current path + this.options.path = e.node.parent.origpath; + this.options.list = e.node.parent.origlist; + this.print_path(); + } + this.select_file(node); + } else { + // save current path and filelist (in case we want to jump to other viewmode) + this.options.path = e.node.origpath; + this.options.list = e.node.origlist; + this.print_path(); + //this.content_scrolled(); + } + }; + } + if (!this.lazyloading) {this.lazyloading={};} + this.filemanager.one('.fp-content').fp_display_filelist(options, list, this.lazyloading); + }, + populate_licenses_select: function(node) { + if (!node) {return;} + node.setContent(''); + var licenses = this.options.licenses; + for (var i in licenses) { + var option = Y.Node.create('