// YUI3 File Picker module for moodle // Author: Dongsheng Cai /** * * File Picker UI * ===== * this.fpnode, contains reference to filepicker Node, non-empty if and only if rendered * this.api, stores the URL to make ajax request * this.mainui, YUI Panel * this.selectnode, contains reference to select-file Node * this.selectui, YUI Panel for selecting particular file * this.msg_dlg, YUI Panel for error or info message * this.process_dlg, YUI Panel for processing existing filename * this.treeview, YUI Treeview * this.viewmode, store current view mode * this.pathbar, reference to the Node with path bar * this.pathnode, a Node element representing one folder in a path bar (not attached anywhere, just used for template) * this.currentpath, the current path in the repository (or last requested path) * * Filepicker options: * ===== * this.options.client_id, the instance id * this.options.contextid * this.options.itemid * this.options.repositories, stores all repositories displayed in file picker * this.options.formcallback * * Active repository options * ===== * this.active_repo.id * this.active_repo.defaultreturntype * this.active_repo.nosearch * this.active_repo.norefresh * this.active_repo.nologin * this.active_repo.help * this.active_repo.manage * * Server responses * ===== * this.filelist, cached filelist * this.pages * this.page * this.filepath, current path (each element of the array is a part of the breadcrumb) * this.logindata, cached login form */ YUI.add('moodle-core_filepicker', function(Y) { /** help function to extract width/height style as a number, not as a string */ Y.Node.prototype.getStylePx = function(attr) { var style = this.getStyle(attr); if (''+style == '0' || ''+style == '0px') { return 0; } var matches = style.match(/^([\d\.]+)px$/) if (matches && parseFloat(matches[1])) { return parseFloat(matches[1]); } return null; } /** if condition is met, the class is added to the node, otherwise - removed */ Y.Node.prototype.addClassIf = function(className, condition) { if (condition) { this.addClass(className); } else { this.removeClass(className); } return this; } /** sets the width(height) of the node considering existing minWidth(minHeight) */ Y.Node.prototype.setStyleAdv = function(stylename, value) { var stylenameCap = stylename.substr(0,1).toUpperCase() + stylename.substr(1, stylename.length-1).toLowerCase(); this.setStyle(stylename, '' + Math.max(value, this.getStylePx('min'+stylenameCap)) + 'px') return this; } /** set image source to src, if there is preview, remember it in lazyloading. * If there is a preview and it was already loaded, use it. */ Y.Node.prototype.setImgSrc = function(src, realsrc, lazyloading) { if (realsrc) { if (M.core_filepicker.loadedpreviews[realsrc]) { this.set('src', realsrc).addClass('realpreview'); return this; } else { if (!this.get('id')) { this.generateID(); } lazyloading[this.get('id')] = realsrc; } } this.set('src', src); return this; } /** * Replaces the image source with preview. If the image is inside the treeview, we need * also to update the html property of corresponding YAHOO.widget.HTMLNode * @param array lazyloading array containing associations of imgnodeid->realsrc */ Y.Node.prototype.setImgRealSrc = function(lazyloading) { if (this.get('id') && lazyloading[this.get('id')]) { var newsrc = lazyloading[this.get('id')]; M.core_filepicker.loadedpreviews[newsrc] = true; this.set('src', newsrc).addClass('realpreview'); delete lazyloading[this.get('id')]; var treenode = this.ancestor('.fp-treeview') if (treenode && treenode.get('parentNode').treeview) { treenode.get('parentNode').treeview.getRoot().refreshPreviews(this.get('id'), newsrc); } } return this; } /** scan TreeView to find which node contains image with id=imgid and replace it's html * with the new image source. */ Y.YUI2.widget.Node.prototype.refreshPreviews = function(imgid, newsrc, regex) { if (!regex) { regex = new RegExp("]*id=\""+imgid+"\"[^>]*?(/?)>", "im"); } if (this.expanded || this.isLeaf) { var html = this.getContentHtml(); if (html && this.setHtml && regex.test(html)) { var newhtml = this.html.replace(regex, "", html); this.setHtml(newhtml); return true; } if (!this.isLeaf && this.children) { for(var c in this.children) { if (this.children[c].refreshPreviews(imgid, newsrc, regex)) { return true; } } } } return false; } /** * Displays a list of files (used by filepicker, filemanager) inside the Node * * @param array options * viewmode : 1 - icons, 2 - tree, 3 - table * appendonly : whether fileslist need to be appended instead of replacing the existing content * filenode : Node element that contains template for displaying one file * callback : On click callback. The element of the fileslist array will be passed as argument * rightclickcallback : On right click callback (optional). * callbackcontext : context where callbacks are executed * sortable : whether content may be sortable (in table mode) * dynload : allow dynamic load for tree view * filepath : for pre-building of tree view - the path to the current directory in filepicker format * treeview_dynload : callback to function to dynamically load the folder in tree view * classnamecallback : callback to function that returns the class name for an element * @param array fileslist array of files to show, each array element may have attributes: * title or fullname : file name * shorttitle (optional) : display file name * thumbnail : url of image * icon : url of icon image * thumbnail_width : width of thumbnail, default 90 * thumbnail_height : height of thumbnail, default 90 * thumbnail_alt : TODO not needed! * description or thumbnail_title : alt text * @param array lazyloading : reference to the array with lazy loading images */ Y.Node.prototype.fp_display_filelist = function(options, fileslist, lazyloading) { var viewmodeclassnames = {1:'fp-iconview', 2:'fp-treeview', 3:'fp-tableview'}; var classname = viewmodeclassnames[options.viewmode]; var scope = this; /** return whether file is a folder (different attributes in FileManager and FilePicker) */ var file_is_folder = function(node) { if (node.children) {return true;} if (node.type && node.type == 'folder') {return true;} return false; }; /** return the name of the file (different attributes in FileManager and FilePicker) */ var file_get_filename = function(node) { return node.title ? node.title : node.fullname; }; /** return display name of the file (different attributes in FileManager and FilePicker) */ var file_get_displayname = function(node) { var displayname = node.shorttitle ? node.shorttitle : file_get_filename(node); return Y.Escape.html(displayname); }; /** return file description (different attributes in FileManager and FilePicker) */ var file_get_description = function(node) { var description = ''; if (node.description) { description = node.description; } else if (node.thumbnail_title) { description = node.thumbnail_title; } else { description = file_get_filename(node); } return Y.Escape.html(description); }; /** help funciton for tree view */ var build_tree = function(node, level) { // prepare file name with icon var el = Y.Node.create('
'); el.appendChild(options.filenode.cloneNode(true)); el.one('.fp-filename').setContent(file_get_displayname(node)); // TODO add tooltip with node.title or node.thumbnail_title var tmpnodedata = {className:options.classnamecallback(node)}; el.get('children').addClass(tmpnodedata.className); if (node.icon) { el.one('.fp-icon').appendChild(Y.Node.create('')); el.one('.fp-icon img').setImgSrc(node.icon, node.realicon, lazyloading); } // create node tmpnodedata.html = el.getContent(); var tmpNode = new Y.YUI2.widget.HTMLNode(tmpnodedata, level, false); if (node.dynamicLoadComplete) { tmpNode.dynamicLoadComplete = true; } tmpNode.fileinfo = node; tmpNode.isLeaf = !file_is_folder(node); if (!tmpNode.isLeaf) { if(node.expanded) { tmpNode.expand(); } tmpNode.path = node.path ? node.path : (node.filepath ? node.filepath : ''); for(var c in node.children) { build_tree(node.children[c], tmpNode); } } }; /** initialize tree view */ var initialize_tree_view = function() { var parentid = scope.one('.'+classname).get('id'); // TODO MDL-32736 use YUI3 gallery TreeView scope.treeview = new Y.YUI2.widget.TreeView(parentid); if (options.dynload) { scope.treeview.setDynamicLoad(Y.bind(options.treeview_dynload, options.callbackcontext), 1); } scope.treeview.singleNodeHighlight = true; if (options.filepath && options.filepath.length) { // we just jumped from icon/details view, we need to show all parents // we extract as much information as possible from filepath and filelist // and send additional requests to retrieve siblings for parent folders var mytree = {}; var mytreeel = null; for (var i in options.filepath) { if (mytreeel == null) { mytreeel = mytree; } else { mytreeel.children = [{}]; mytreeel = mytreeel.children[0]; } var pathelement = options.filepath[i]; mytreeel.path = pathelement.path; mytreeel.title = pathelement.name; mytreeel.icon = pathelement.icon; mytreeel.dynamicLoadComplete = true; // we will call it manually mytreeel.expanded = true; } mytreeel.children = fileslist; build_tree(mytree, scope.treeview.getRoot()); // manually call dynload for parent elements in the tree so we can load other siblings if (options.dynload) { var root = scope.treeview.getRoot(); // Whether search results are currently displayed in the active repository in the filepicker. // We do not want to load siblings of parent elements when displaying search tree results. var isSearchResult = typeof options.callbackcontext.active_repo !== 'undefined' && options.callbackcontext.active_repo.issearchresult; while (root && root.children && root.children.length) { root = root.children[0]; if (root.path == mytreeel.path) { root.origpath = options.filepath; root.origlist = fileslist; } else if (!root.isLeaf && root.expanded && !isSearchResult) { Y.bind(options.treeview_dynload, options.callbackcontext)(root, null); } } } } else { // there is no path information, just display all elements as a list, without hierarchy for(k in fileslist) { build_tree(fileslist[k], scope.treeview.getRoot()); } } scope.treeview.subscribe('clickEvent', function(e){ e.node.highlight(false); var callback = options.callback; if (options.rightclickcallback && e.event.target && Y.Node(e.event.target).ancestor('.fp-treeview .fp-contextmenu', true)) { callback = options.rightclickcallback; } Y.bind(callback, options.callbackcontext)(e, e.node.fileinfo); Y.YUI2.util.Event.stopEvent(e.event) }); // Simulate click on file not folder. scope.treeview.subscribe('enterKeyPressed', function(node) { if (node.children.length === 0) { Y.one(node.getContentEl()).one('a').simulate('click'); } }); // TODO MDL-32736 support right click /*if (options.rightclickcallback) { scope.treeview.subscribe('dblClickEvent', function(e){ e.node.highlight(false); Y.bind(options.rightclickcallback, options.callbackcontext)(e, e.node.fileinfo); }); }*/ scope.treeview.draw(); }; /** formatting function for table view */ var formatValue = function (o){ if (o.data[''+o.column.key+'_f_s']) {return o.data[''+o.column.key+'_f_s'];} else if (o.data[''+o.column.key+'_f']) {return o.data[''+o.column.key+'_f'];} else if (o.value) {return o.value;} else {return '';} }; /** formatting function for table view */ var formatTitle = function(o) { var el = Y.Node.create('
'); el.appendChild(options.filenode.cloneNode(true)); // TODO not node but string! el.get('children').addClass(o.data['classname']); el.one('.fp-filename').setContent(o.value); if (o.data['icon']) { el.one('.fp-icon').appendChild(Y.Node.create('')); el.one('.fp-icon img').setImgSrc(o.data['icon'], o.data['realicon'], lazyloading); } if (options.rightclickcallback) { el.get('children').addClass('fp-hascontextmenu'); } // TODO add tooltip with o.data['title'] (o.value) or o.data['thumbnail_title'] return el.getContent(); } /** * Generate slave checkboxes based on toggleall's specification * @param {object} o An object reprsenting the record for the current row. * @return {html} The checkbox html */ var formatCheckbox = function(o) { var el = Y.Node.create('
'); var parentid = scope.one('.' + classname).get('id'); var checkbox = Y.Node.create('') .setAttribute('type', 'checkbox') .setAttribute('data-fieldtype', 'checkbox') .setAttribute('data-fullname', o.data.fullname) .setAttribute('data-action', 'toggle') .setAttribute('data-toggle', 'slave') .setAttribute('data-togglegroup', 'file-selections-' + parentid); var checkboxLabel = Y.Node.create('