blob: d8e67277418a6864f8e0720fee882b8bc1fb9d6b [file] [log] [blame]
/*
---
description: FileManager
authors: Christoph Pojer (@cpojer), Fabian Vogelsteller (@frozeman)
license: MIT-style license
requires:
core/1.3.2: '*'
more/1.3.2.1: [Request.Queue, Array.Extras, String.QueryString, Hash, Element.Delegation, Element.Measure, Fx.Scroll, Fx.SmoothScroll, Drag, Drag.Move, Assets, Tips, Scroller ]
provides: Filemanager
...
*/
var FileManager = new Class({
Implements: [Options, Events],
Request: null,
RequestQueue: null,
Directory: null,
Current: null,
ID: null,
options: {
/*
* onComplete: function( // Fired when the 'Select' button is clicked
* path, // URLencoded absolute URL path to selected file
* file, // the file specs object: .name, .path, .size, .date, .mime, .icon, .icon48, .thumb48, .thumb250
* fmobj // reference to the FileManager instance which fired the event
* )
*
* onModify: function( // Fired when either the 'Rename' or 'Delete' icons are clicked or when a file is drag&dropped.
* // Fired AFTER the action is executed.
* file, // a CLONE of the file specs object: .name, .path, .size, .date, .mime, .icon, .icon48, .thumb48, .thumb250
* json, // The JSON data as sent by the server for this 'destroy/rename/move/copy' request
* mode, // string specifying the action: 'destroy', 'rename', 'move', 'copy'
* fmobj // reference to the FileManager instance which fired the event
* )
*
* onShow: function( // Fired AFTER the file manager is rendered
* fmobj // reference to the FileManager instance which fired the event
* )
*
* onHide: function( // Fired AFTER the file manager is removed from the DOM
* fmobj // reference to the FileManager instance which fired the event
* )
*
* onScroll: function( // Cascade of the window scroll event
* e, // reference to the event object (argument passed from the window.scroll event)
* fmobj // reference to the FileManager instance which fired the event
* )
*
* onPreview: function( // Fired when the preview thumbnail image is clicked
* src, // this.get('src') ???
* fmobj, // reference to the FileManager instance which fired the event
* el // reference to the 'this' ~ the element which was clicked
* )
*
* onDetails: function( // Fired when an item is picked from the files list to be previewed
* // Fired AFTER the server request is completed and BEFORE the preview is rendered.
* json, // The JSON data as sent by the server for this 'detail' request
* fmobj // reference to the FileManager instance which fired the event
* )
*
* onHidePreview: function( // Fired when the preview is hidden (e.g. when uploading)
* // Fired BEFORE the preview is removed from the DOM.
* fmobj // reference to the FileManager instance which fired the event
* )
*/
directory: '', // (string) the directory (relative path) which should be loaded on startup (show).
url: null,
assetBasePath: null,
language: 'en',
selectable: false,
destroy: false,
rename: false,
move_or_copy: false,
download: false,
createFolders: false,
filter: '',
listType: 'thumb', // the standard list type can be 'list' or 'thumb'
keyboardNavigation: true, // set to false to turn off keyboard navigation (tab, up/dn/pageup/pagedn etc)
detailInfoMode: '', // (string) whether you want to receive extra metadata on select/etc. and/or view this metadata in the preview pane (modes: '', '+metaHTML', '+metaJSON'. Modes may be combined)
previewHandlers: {}, // [partial] mimetype: function, function is called with previewArea (DOM element, put preview in here), fileDetails
// eg { 'audio': function(previewArea,fileDetails){ previewArea.adopt(new Element('div', {text:'Hello World'});} }
deliverPathAsLegalURL: false, // (boolean) TRUE: deliver 'legal URL' paths, i.e. 'directory'-rooted, FALSE: deliver absolute URI paths.
hideOnClick: false,
hideClose: false,
hideOverlay: false,
hideOnDelete: false,
hideOnSelect: true, // (boolean). Default to true. If set to false, it leavers the FM open after a picture select.
showDirGallery: true,
thumbSize4DirGallery: 120, // To set the thumb gallery container size for each thumb (dir-gal-thumb-bg); depending on size, it will pick either the small or large thumbnail provided by the backend and scale that one
zIndex: 1000,
styles: {},
listPaginationSize: 100, // add pagination per N items for huge directories (speed up interaction)
listPaginationAvgWaitTime: 2000, // adaptive pagination: strive to, on average, not spend more than this on rendering a directory chunk
listMaxSuggestedDirSizeForThumbnails: 30, // if a directory load has more than this many items (dirs+files), and the view mode is currently thumbs
// it is dropped back to the listing view mode, the user can still switch back, at their own risk!
standalone: true, // (boolean). Default to true. If set to false, returns the Filemanager without enclosing window / overlay.
parentContainer: null, // (string). ID of the parent container. If not set, FM will consider its first container parent for fitSizes();
propagateData: {}, // extra query parameters sent with every request to the backend
verbose: false,
mkServerRequestURL: null // (function) specify your own alternative URL/POST data constructor when you use a framework/system which requires such. function([object] fm_obj, [string] request_code, [assoc.array] post_data)
},
/*
* hook items are objects (kinda associative arrays, as they are used here), where each
* key item is called when the hook is invoked.
*/
hooks: {
show: {}, // invoked after the 'show' event
cleanup: {}, // invoked before the 'hide' event
cleanupPreview: {}, // invoked before the 'hidePreview' event
fill: {} // invoked after the fill operation has completed
},
initialize: function(options) {
this.options.mkServerRequestURL = this.mkServerRequestURL;
this.setOptions(options);
if(typeof this.options.previewHandlers.audio === 'undefined')
{
this.options.previewHandlers.audio = this.audioPreview.bind(this);
}
this.diag.verbose = this.options.verbose;
this.ID = String.uniqueID();
this.droppables = [];
this.assetBasePath = this.options.assetBasePath.replace(/(\/|\\)*$/, '/');
this.root = null;
this.CurrentDir = null;
this.listType = this.options.listType;
this.dialogOpen = false;
this.storeHistory = false;
this.fmShown = false;
this.drop_pending = 0; // state: 0: no drop pending, 1: copy pending, 2: move pending
this.view_fill_timer = null; // timer reference when fill() is working chunk-by-chunk.
this.view_fill_startindex = 0; // offset into the view JSON array: which part of the entire view are we currently watching?
this.view_fill_json = null; // the latest JSON array describing the entire list; used with pagination to hop through huge dirs without repeatedly consulting the server.
this.listPaginationLastSize = this.options.listPaginationSize;
this.Request = null;
this.downloadIframe = null;
this.downloadForm = null;
this.drag_is_active = false;
this.ctrl_key_pressed = false;
this.pending_error_dialog = null;
// timer for dir-gallery click / dblclick events:
this.dir_gallery_click_timer = null;
var dbg_cnt = 0;
this.RequestQueue = new Request.Queue({
concurrent: 3, // 3 --> 75% max load on a quad core server
autoAdvance: true,
stopOnFailure: false,
onRequest: (function(){
//this.diag.log('request queue: onRequest: ', arguments);
}).bind(this),
onComplete: (function(name){
//this.diag.log('request queue: onComplete: ', arguments);
// clean out the item from the queue; doesn't seem to happen automatically :-(
var cnt = 0;
Object.each(this.RequestQueue.requests, function() {
cnt++;
});
// cut down on the number of reports:
if (Math.abs(cnt - dbg_cnt) >= 25)
{
this.diag.log('request queue: name counts: ', cnt, ', queue length: ', this.RequestQueue.queue.length);
dbg_cnt = cnt;
}
}).bind(this),
onCancel: (function(){
this.diag.log('request queue: onCancel: ', arguments);
}).bind(this),
onSuccess: (function(){
//this.diag.log('request queue: onSuccess: ', arguments);
}).bind(this),
onFailure: (function(name){
this.diag.log('request queue: onFailure: ', arguments);
}).bind(this),
onException: (function(name){
this.diag.log('request queue: onException: ', arguments);
}).bind(this)
});
// add a special custom routine to the queue object: we want to be able to clear PART OF the queue!
this.RequestQueue.cancel_bulk = (function(marker)
{
Object.each(this.requests, function(q, name)
{
var n = name.split(':');
if (n[0] === marker)
{
// match! revert by removing the request (and cancelling it!)
this.cancel(name);
this.removeRequest(name);
this.clear(name); // eek, a full table scan! yech.
delete this.requests[name];
delete this.reqBinders[name];
}
}, this);
// now that we have cleared out all those requests, some of which may have been running at the time, we need to resume the loading:
this.resume();
}).bind(this.RequestQueue);
this.language = Object.clone(FileManager.Language.en);
if (this.options.language !== 'en') {
this.language = Object.merge(this.language, FileManager.Language[this.options.language]);
}
// Partikule
if (!this.options.standalone)
{
this.options.hideOverlay = true;
this.options.hideClose = true;
}
// /Partikule
this.container = new Element('div', {
'class': 'filemanager-container' + (Browser.opera ? ' filemanager-engine-presto' : '') + (Browser.ie ? ' filemanager-engine-trident' : '') + (Browser.ie8 ? '4' : '') + (Browser.ie9 ? '5' : ''),
styles:
{
'z-index': this.options.zIndex
}
});
this.filemanager = new Element('div', {
'class': 'filemanager',
styles: Object.append({},
this.options.styles,
{
'z-index': this.options.zIndex + 1
})
}).inject(this.container);
this.header = new Element('div', {
'class': 'filemanager-header' /* ,
styles:
{
'z-index': this.options.zIndex + 3
} */
}).inject(this.filemanager);
this.menu = new Element('div', {
'class': 'filemanager-menu' /* ,
styles:
{
'z-index': this.options.zIndex + 2
} */
}).inject(this.filemanager);
this.loader = new Element('div', {'class': 'loader', opacity: 0, tween: {duration: 'short'}}).inject(this.header);
this.previewLoader = new Element('div', {'class': 'loader', opacity: 0, tween: {duration: 'short'}});
this.browserLoader = new Element('div', {'class': 'loader', opacity: 0, tween: {duration: 'short'}});
// switch the path, from clickable to input text
this.clickablePath = new Element('span', {'class': 'filemanager-dir'});
this.selectablePath = new Element('input',{'type': 'text', 'class': 'filemanager-dir', 'readonly': 'readonly'});
this.pathTitle = new Element('a', {href:'#','class': 'filemanager-dir-title',text: this.language.dir}).addEvent('click',(function(e) {
this.diag.log('pathTitle-click event: ', e, ' @ ', e.target.outerHTML);
e.stop();
if (this.header.getElement('span.filemanager-dir') !== null) {
this.selectablePath.setStyle('width',(this.header.getSize().x - this.pathTitle.getSize().x - 55));
this.selectablePath.replaces(this.clickablePath);
}
else {
this.clickablePath.replaces(this.selectablePath);
}
}).bind(this));
this.header.adopt(this.pathTitle,this.clickablePath);
// Partikule
// Because the header is positioned -30px before the container, we hide it for the moment if the FM isn't standalone.
// Need to think about a better integration
if (!this.options.standalone)
{
this.header.hide();
this.filemanager.setStyle('width', '100%');
}
// /Partikule
var self = this;
this.browsercontainer = new Element('div',{'class': 'filemanager-browsercontainer'}).inject(this.filemanager);
this.browserheader = new Element('div',{'class': 'filemanager-browserheader'}).inject(this.browsercontainer);
this.browserheader.adopt(this.browserLoader);
this.browserScroll = new Element('div', {'class': 'filemanager-browserscroll'}).inject(this.browsercontainer).addEvents({
'mouseover': (function(e) {
//this.diag.log('mouseover: ', e);
// sync mouse and keyboard-driven browsing: the keyboard requires that we keep track of the hovered item,
// so we cannot simply leave it to a :hover CSS style. Instead, we find out which element is currently
// hovered:
var row = null;
if (e.target)
{
row = (e.target.hasClass('fi') ? e.target : e.target.getParent('span.fi'));
if (row)
{
row.addClass('hover');
}
}
this.browser.getElements('span.fi.hover').each(function(span) {
// prevent screen flicker: only remove the class for /other/ nodes:
if (span != row) {
span.removeClass('hover');
var rowicons = span.getElements('img.browser-icon');
if (rowicons)
{
rowicons.each(function(icon) {
icon.set('tween', {duration: 'short'}).fade(0);
});
}
}
});
if (row)
{
var icons = row.getElements('img.browser-icon');
if (icons)
{
icons.each(function(icon) {
if (e.target == icon)
{
icon.set('tween', {duration: 'short'}).fade(1);
}
else
{
icon.set('tween', {duration: 'short'}).fade(0.5);
}
});
}
}
}).bind(this),
/* 'mouseout' */
'mouseleave': (function(e) {
//this.diag.log('mouseout: ', e);
// only bother us when the mouse cursor has just left the browser area; anything inside there is handled
// by the recurring 'mouseover' event above...
//
// - do NOT remove the 'hover' marker from the row; it will be used by the keyboard!
// - DO fade out the action icons, though!
this.browser.getElements('span.fi.hover').each(function(span) {
var rowicons = span.getElements('img.browser-icon');
if (rowicons)
{
rowicons.each(function(icon) {
icon.set('tween', {duration: 'short'}).fade(0);
});
}
});
}).bind(this)
});
this.browserMenu_thumb = new Element('a',{
'id':'toggle_side_boxes',
'class':'listType',
'style' : 'margin-right: 10px;',
'title': this.language.toggle_side_boxes
}).setStyle('opacity',0.5).addEvents({
click: this.toggleList.bind(this)
});
this.browserMenu_list = new Element('a',{
'id':'toggle_side_list',
'class':'listType',
'title': this.language.toggle_side_list
}).setStyle('opacity',1).addEvents({
click: this.toggleList.bind(this)
});
if(this.listType == 'thumb') {
this.browserMenu_thumb.setStyle('opacity',1);
this.browserMenu_list.setStyle('opacity',0.5);
this.browserMenu_thumb.store('set', true);
this.browserMenu_list.store('set', false);
}
else
{
this.browserMenu_thumb.setStyle('opacity',0.5);
this.browserMenu_list.setStyle('opacity',1);
this.browserMenu_thumb.store('set', false);
this.browserMenu_list.store('set', true);
}
// Add a scroller to scroll the browser list when dragging a file
this.scroller = new Scroller(this.browserScroll, {
onChange: function(x, y)
{
// restrict scrolling to Y direction only!
//this.element.scrollTo(x, y);
var scroll = this.element.getScroll();
this.element.scrollTo(scroll.x, y);
}
});
if(this.options.showDirGallery)
{
// Partikule : Thumbs list in preview panel
this.browserMenu_thumbList = new Element('a',{
'id': 'show_dir_thumb_gallery',
'title': this.language.show_dir_thumb_gallery
}).addEvent('click', function()
{
// do NOT change the jsGET history carrying our browsing so far; the fact we want to view the dirtree should
// *NOT* blow away the recall in which directory we are (and what item is currently selected):
//if (typeof jsGET !== 'undefined')
// jsGET.clear();
// no need to request the dirscan again: after all, we only wish to render another view of the same directory.
// (This means, however, that we MAY requesting any deferred thumbnails)
//self.load(self.options.directory, true);
//return self.deselect(); // nothing to return on a click event, anyway. And do NOT loose the selection!
// the code you need here is identical to clicking on the current directory in the top path bar:
// show the 'directory' info in the detail pane again (this is a way to get back from previewing single files to previewing the directory as a gallery)
this.diag.log('show_dir_Thumb_gallery button click: current directory!', this.CurrentDir, ', startdir: ', this.options.directory);
this.fillInfo();
}.bind(this));
// /Partikule
}
this.browser_dragndrop_info = new Element('a',{
'id':'drag_n_drop',
'title': this.language.drag_n_drop_disabled
}); // .setStyle('visibility', 'hidden');
this.browser_paging = new Element('div',{
'id':'fm_view_paging'
}).setStyle('opacity', 0); // .setStyle('visibility', 'hidden');
this.browser_paging_first = new Element('a',{
'id':'paging_goto_first'
}).setStyle('opacity', 1).addEvents({
click: this.paging_goto_first.bind(this)
});
this.browser_paging_prev = new Element('a',{
'id':'paging_goto_previous'
}).setStyle('opacity', 1).addEvents({
click: this.paging_goto_prev.bind(this)
});
this.browser_paging_next = new Element('a',{
'id':'paging_goto_next'
}).setStyle('opacity', 1).addEvents({
click: this.paging_goto_next.bind(this)
});
this.browser_paging_last = new Element('a',{
'id':'paging_goto_last'
}).setStyle('opacity', 1).addEvents({
click: this.paging_goto_last.bind(this)
});
this.browser_paging_info = new Element('span',{
'id':'paging_info',
'text': ''
});
this.browser_paging.adopt([this.browser_paging_first, this.browser_paging_prev, this.browser_paging_info, this.browser_paging_next, this.browser_paging_last]);
// Partikule : Added the browserMenu_thumbList to the browserheader
this.browserheader.adopt([this.browserMenu_thumbList, this.browserMenu_thumb, this.browserMenu_list, this.browser_dragndrop_info, this.browser_paging]);
// /Partikule
this.browser = new Element('ul', {'class': 'filemanager-browser'}).inject(this.browserScroll);
if (this.options.createFolders) this.addMenuButton('create');
if (this.options.download) this.addMenuButton('download');
if (this.options.selectable) this.addMenuButton('open');
this.info = new Element('div', {'class': 'filemanager-infos'});
this.info_head = new Element('div', {
'class': 'filemanager-head',
styles:
{
opacity: 0
}
}).adopt([
new Element('img', {'class': 'filemanager-icon'}),
new Element('h1')
]);
this.preview = new Element('div', {'class': 'filemanager-preview'}).addEvent('click:relay(img.preview)', function() {
self.fireEvent('preview', [this.get('src'), self, this]);
});
// We need to group the headers and lists together because we may
// use some CSS to reorganise it a bit in the custom event handler. So we create "filemanager-preview-area" which
// will contain the h2 for the preview and also the preview content returned from
// Backend/FileManager.php
this.preview_area = new Element('div', {'class': 'filemanager-preview-area',
styles:
{
opacity: 0
}
});
// Partikule. Removed new Element('h2', {'class': 'filemanager-headline' :
// 1. To gain more vertical space for preview
// 2. Because the user knows this is info about the file
this.preview_area.adopt([
//new Element('h2', {'class': 'filemanager-headline', text: this.language.more}),
this.preview
]);
// Partikule.
// 1. To gain more vertical space for preview
// 2. Because the user knows this is info about the file
// 3. Less is more :-)
this.info.adopt([this.info_head, this.preview_area]).inject(this.filemanager);
// /Partikule
// Partikule
// Add of the thumbnail list in the preview panel
// We fill this one while we render the directory tree view to ensure that the deferred thumbnail loading system
// (using 'detail / mode=direct' requests to obtain the actual thumbnail paths) doesn't become a seriously complex
// mess.
// This way, any updates coming from the server are automatically edited into this list; whether it is shown or
// not depends on the decisions in fillInfo()
//
// Usage:
// - One doubleclick on one thumb in this list will select the file : quicker select
// - One click displays the preview, but with the file in bigger format : less clicks to see the picture wider.
// Thumbs list container (in preview panel)
this.dir_filelist = new Element('div', {'class': 'filemanager-filelist'});
// creates a list, HELPS to make the thumblist DRAGABLE
this.dir_filelist_thumbUl = new Element('ul');
this.dir_filelist_thumbUl.inject(this.dir_filelist);
// /Partikule
if (!this.options.hideClose) {
this.closeIcon = new Element('a', {
'class': 'filemanager-close',
opacity: 0.5,
title: this.language.close,
events: {click: this.hide.bind(this)}
}).inject(this.filemanager).addEvent('mouseover',function() {
this.fade(1);
}).addEvent('mouseout',function() {
this.fade(0.5);
});
}
this.tips = new Tips({
className: 'tip-filebrowser',
offsets: {x: 15, y: 0},
text: null,
showDelay: 50,
hideDelay: 50,
onShow: function() {
this.tip.setStyle('z-index', self.options.zIndex + 501).set('tween', {duration: 'short'}).setStyle('display', 'block').fade(1);
},
onHide: function() {
this.tip.fade(0).get('tween').chain(function() {
this.element.setStyle('display', 'none');
});
}
});
// add toolTips
if (!this.options.hideClose) {
this.tips.attach(this.closeIcon);
}
this.tips.attach(this.browserMenu_thumb);
this.tips.attach(this.browserMenu_list);
this.tips.attach(this.browserMenu_thumbList);
this.imageadd = Asset.image(this.assetBasePath + 'Images/add.png', {
'class': 'browser-add',
styles:
{
'z-index': this.options.zIndex + 1600
}
}).setStyle('opacity', 0).set('tween', {duration: 'short'}).inject(this.container);
if (!this.options.hideOverlay) {
this.overlay = new Overlay(Object.append((this.options.hideOnClick ? {
events: {
click: this.hide.bind(this)
}
} : {}),
{
styles:
{
'z-index': this.options.zIndex - 1
}
}));
}
this.bound = {
keydown: (function(e)
{
// at least FF on Win will trigger this function multiple times when keys are depressed for a long time. Hence time consuming actions are don in 'keyup' whenever possible.
this.diag.log('keydown: key press: ', e);
if (e.control || e.meta)
{
if (this.drag_is_active && !this.ctrl_key_pressed)
{
// only init the fade when actually switching CONTROL key states!
this.imageadd.fade(1);
}
this.ctrl_key_pressed = true;
}
}).bind(this),
keyup: (function(e)
{
this.diag.log('keyup: key press: ', e);
if (!e.control && !e.meta)
{
if (/* this.drag_is_active && */ this.ctrl_key_pressed)
{
// only init the fade when actually switching CONTROL key states!
this.imageadd.fade(0);
}
this.ctrl_key_pressed = false;
}
if (!this.dialogOpen)
{
switch (e.key)
{
case 'tab':
if(this.options.keyboardNavigation)
{
e.stop();
this.toggleList();
}
break;
case 'esc':
e.stop();
this.hide();
break;
}
}
}).bind(this),
keyboardInput: (function(e)
{
this.diag.log('keyboardInput key press: ', e);
if (this.dialogOpen) return;
switch (e.key)
{
case 'up':
case 'down':
case 'pageup':
case 'pagedown':
case 'home':
case 'end':
case 'enter':
case 'delete':
if(this.options.keyboardNavigation)
{
e.preventDefault();
this.browserSelection(e.key);
break;
}
}
}).bind(this),
scroll: (function(e)
{
this.fireEvent('scroll', [e, this]);
this.fitSizes();
}).bind(this)
};
if (this.options.standalone)
{
this.container.inject(document.body);
// ->> autostart filemanager when set
this.initialShow();
}
else
{
this.options.hideOverlay = true;
}
return this;
},
initialShowBase: function() {
if (typeof jsGET !== 'undefined' && jsGET.get('fmID') == this.ID) {
this.show();
}
else {
window.addEvent('jsGETloaded',(function() {
if (typeof jsGET !== 'undefined' && jsGET.get('fmID') == this.ID)
this.show();
}).bind(this));
}
},
// overridable method:
initialShow: function() {
this.initialShowBase();
},
allow_DnD: function(j, pagesize)
{
if (!this.options.move_or_copy)
return false;
if (!j || !j.dirs || !j.files || !pagesize)
return true;
return (j.dirs.length + j.files.length <= pagesize * 4);
},
/*
* default method to produce a suitable request URL/POST; as several frameworks out there employ url rewriting, one way or another,
* we now allow users to provide their own construction method to replace this one: simply provide your own method in
* options.mkServerRequestURL
* Like this one, it MUST return an object, containing two properties:
*
* url: (string) contains the URL sent to the server for the given event/request (which is always transmitted as a POST request)
* data: (assoc. array): extra parameters added to this POST. (Mainly there in case a framework wants to have the 'event' parameter
* transmitted as a POST data element, rather than having it included in the request URL itself in some form.
*
* WARNING: 'this' in here is actually **NOT** pointing at the FM instance; use 'fm_obj' for that!
*
* In fact, 'this' points at the 'fm_obj.options' object, but consider that an 'undocumented feature'
* as it may change in the future without notice!
*/
mkServerRequestURL: function(fm_obj, request_code, post_data)
{
// HACK: Encode the post_data to get around mod_security issues
function rot13(s)
{
return (s ? s : this).split('').map(function(_)
{
if (!_.match(/[A-Za-z]/)) return _;
c = Math.floor(_.charCodeAt(0) / 97);
k = (_.toLowerCase().charCodeAt(0) - 83) % 26 || 26;
return String.fromCharCode(k + ((c == 0) ? 64 : 96));
}).join('');
}
// console.log(rot13(JSON.encode(post_data)));
post_data = { encoded_data: rot13(JSON.encode(post_data)) };
return {
url: (fm_obj.options.url + (fm_obj.options.url.indexOf('?') == -1 ? '?' : '&') + Object.toQueryString({
event: request_code
})).replace(/&&/, '&'),
data: post_data
};
},
fitSizes: function()
{
if (this.options.standalone)
{
this.filemanager.center(this.offsets);
}
else
{
var parent = (this.options.parentContainer !== null ? document.id(this.options.parentContainer) : this.container.getParent());
if (parent)
{
parentSize = parent.getSize();
this.filemanager.setStyle('height', parentSize.y);
}
}
var containerSize = this.filemanager.getSize();
var headerSize = this.browserheader.getSize();
var menuSize = this.menu.getSize();
this.browserScroll.setStyle('height',containerSize.y - headerSize.y);
this.info.setStyle('height',containerSize.y - menuSize.y);
},
// see also: http://cass-hacks.com/articles/discussion/js_url_encode_decode/
// and: http://xkr.us/articles/javascript/encode-compare/
// This is a much simplified version as we do not need exact PHP rawurlencode equivalence.
//
// We have one mistake to fix: + instead of %2B. We don't mind
// that * and / remain unencoded. Not exactly RFC3986, but there you have it...
//
// WARNING: given the above, we ASSUME this function will ONLY be used to encode the
// a single URI 'path', 'query' or 'fragment' component at a time!
escapeRFC3986: function(s) {
return encodeURI(s.toString()).replace(/\+/g, '%2B').replace(/#/g, '%23');
},
unescapeRFC3986: function(s) {
return decodeURI(s.toString().replace(/%23/g, '#').replace(/%2B/g, '+'));
},
// -> catch a click on an element in the file/folder browser
relayClick: function(e, el) {
if (e) e.stop();
// if the clicked elelement is from the preview gallery, get the corresponding element
if(el.retrieve('el_ref'))
el = el.retrieve('el_ref');
// ignore mouse clicks while drag&drop + resulting copy/move is pending.
//
// Theoretically only the first click originates from the same mouse event as the 'drop' event, so we
// COULD reset 'drop_pending' after processing that one.
if (this.drop_pending !== 0)
{
this.drop_pending = 0;
}
else
{
this.storeHistory = true;
var file = el.retrieve('file');
this.diag.log('on relayClick file = ', file, ', current directory: ', this.CurrentDir, '@ el = ', el);
if (el.retrieve('edit')) {
el.eliminate('edit');
return;
}
if (file.mime === 'text/directory')
{
el.addClass('selected');
// reset the paging to page #0 as we clicked to change directory
this.store_view_fill_startindex(0);
this.load(file.path);
return;
}
// when we're right smack in the middle of a drag&drop, which may end up as a MOVE, do NOT send a 'detail' request
// alongside (through fillInfo) as that may lock the file being moved, server-side.
// It's good enough to disable the detail view, if we want/need to.
//
// Note that this.drop_pending tracks the state of the drag&drop state machine -- more functions may check this one!
if (this.Current) {
this.Current.removeClass('selected');
}
// ONLY do this when we're doing a COPY or on a failed attempt...
// CORRECTION: as even a failed 'drop' action will have moved the cursor, we can't keep this one selected right now:
this.Current = el.addClass('selected');
// We need to have Current assigned before fillInfo because fillInfo adds to it
this.fillInfo(file);
this.switchButton4Current();
// // // now make sure we can see the selected item in the left pane: scroll there:
this.browserSelection('current');
}
},
// Partikule
/**
* Catches double clicks and open the file if selectable is true */
relayDblClick: function(e, el)
{
if(this.options.selectable === false)
return;
if (e) e.stop();
this.diag.log('on relayDblClick file = ', el.retrieve('file'), ', current dir: ', this.CurrentDir);
this.tips.hide();
this.CurrentFile = el.retrieve('file');
if (this.CurrentFile.mime !== 'text/directory')
this.open_on_click(null);
},
// /Partikule
toggleList: function(e) {
if (e) e.stop();
if(e && e.target && document.id(e.target).retrieve('set', false)) return; // Already Set
$$('.filemanager-browserheader a.listType').setStyle('opacity',0.5);
if (!this.browserMenu_thumb.retrieve('set',false)) {
this.browserMenu_list.store('set',false);
this.browserMenu_thumb.store('set',true).setStyle('opacity',1);
this.listType = 'thumb';
if (typeof jsGET !== 'undefined') jsGET.set('fmListType=thumb');
} else {
this.browserMenu_thumb.store('set',false);
this.browserMenu_list.store('set',true).setStyle('opacity',1);
this.listType = 'list';
if (typeof jsGET !== 'undefined') jsGET.set('fmListType=list');
}
this.diag.log('on toggleList dir = ', this.CurrentDir, e);
// abort any still running ('antiquated') fill chunks and reset the store before we set up a new one:
this.RequestQueue.cancel_bulk('fill');
clearTimeout(this.view_fill_timer);
this.view_fill_timer = null;
this.fill(null, this.get_view_fill_startindex(), this.listPaginationLastSize);
},
/*
* Gets called from the jsGET listener.
*
* Is fired for two reasons:
*
* 1) the user clicked on a file or directory to view and that change was also pushed to the history through one or more jsGET.set() calls.
* (In this case, we've already done what needed doing, so we should not redo that effort in here!)
*
* 2) the user went back in browser history or manually edited the URI hash section.
* (This is an 'change from the outside' and exactly what this listener is for. This time around, we should follow up on those changes!)
*/
hashHistory: function(vars)
{
this.storeHistory = false;
this.diag.log('hasHistory:', vars);
if (vars.changed['fmPath'] === '')
vars.changed['fmPath'] = '/';
Object.each(vars.changed, function(value, key) {
this.diag.log('on hashHistory key = ', key, 'value = ', value);
switch (key)
{
case 'fmPath':
if (this.CurrentDir && this.CurrentDir.path !== value)
{
this.load(value);
}
break;
case 'fmFile':
var hot_item = (this.Current && this.Current.retrieve('file'));
if (hot_item === null || value !== hot_item.name)
{
this.browser.getElements('span.fi span').each((function(current)
{
current.getParent('span.fi').removeClass('hover');
if (current.get('title') == value)
{
this.deselect(null);
this.Current = current.getParent('span.fi');
new Fx.Scroll(this.browserScroll,{duration: 'short', offset: {x: 0, y: -(this.browserScroll.getSize().y/4)}}).toElement(this.Current);
this.Current.addClass('selected');
this.diag.log('on hashHistory @ fillInfo key = ', key, 'value = ', value, 'source = ', current, 'file = ', current.getParent('span.fi').retrieve('file'));
this.fillInfo(this.Current.retrieve('file'));
}
}).bind(this));
}
break;
}
},this);
},
// Add the ability to specify a path (relative to the base directory) and a file to preselect
show: function(e, loaddir, preselect) {
if (e) e.stop();
this.diag.log('on show: ', e, ', loaddir:', loaddir, ', preselect: ', preselect);
if (this.fmShown) {
return;
}
this.fmShown = true;
if (typeof preselect === 'undefined') preselect = null;
if (typeof loaddir === 'undefined') loaddir = null;
if (loaddir === null && typeof jsGET !== 'undefined')
{
if (jsGET.get('fmPath') !== null)
{
loaddir = jsGET.get('fmPath');
}
}
if (loaddir === null)
{
if (this.CurrentDir)
{
loaddir = this.CurrentDir.path;
}
else
{
loaddir = this.options.directory;
}
}
// get and set history
if (typeof jsGET !== 'undefined') {
if (jsGET.get('fmFile')) {
this.diag.log('on show: set onShow on fmFile: ', jsGET.get('fmFile'));
}
if (jsGET.get('fmListType') !== null) {
$$('.filemanager-browserheader a.listType').setStyle('opacity',0.5);
this.listType = jsGET.get('fmListType');
if (this.listType === 'thumb')
this.browserMenu_thumb.store('set',true).setStyle('opacity',1);
else
this.browserMenu_list.store('set',true).setStyle('opacity',1);
}
jsGET.set({
'fmID': this.ID,
'fmPath': loaddir
});
this.hashListenerId = jsGET.addListener(this.hashHistory, false, this);
}
this.load(loaddir, preselect);
if (!this.options.hideOverlay) {
this.overlay.show();
}
this.show_our_info_sections(false);
this.container.fade(0).setStyles({
display: 'block'
});
window.addEvents({
'scroll': this.bound.scroll,
'resize': this.bound.scroll
});
// add keyboard navigation
this.diag.log('add keyboard nav on show file = ', loaddir);
document.addEvent('keydown', this.bound.keydown);
document.addEvent('keyup', this.bound.keyup);
if ((Browser.Engine && (Browser.Engine.trident || Browser.Engine.webkit)) || (Browser.ie || Browser.chrome || Browser.safari))
document.addEvent('keydown', this.bound.keyboardInput);
else
document.addEvent('keypress', this.bound.keyboardInput);
this.container.fade(1);
this.fitSizes();
this.fireEvent('show', [this]);
this.fireHooks('show');
// Partikule : If not standalone, returns the HTML content
if (!this.options.standalone)
{
return this.container;
}
// /Partikule
},
hide: function(e) {
if (e) e.stop();
this.diag.log('on hide', e, this);
if (!this.fmShown) {
return;
}
this.fmShown = false;
// stop hashListener
if (typeof jsGET !== 'undefined') {
jsGET.removeListener(this.hashListenerId);
jsGET.remove(['fmID','fmPath','fmFile','fmListType','fmPageIdx']);
}
if (!this.options.hideOverlay) {
this.overlay.hide();
}
this.tips.hide();
this.browser.empty();
this.container.setStyle('display', 'none');
// remove keyboard navigation
this.diag.log('REMOVE keyboard nav on hide');
window.removeEvent('scroll', this.bound.scroll).removeEvent('resize', this.bound.scroll);
document.removeEvent('keydown', this.bound.keydown);
document.removeEvent('keyup', this.bound.keyup);
if ((Browser.Engine && (Browser.Engine.trident || Browser.Engine.webkit)) || (Browser.ie || Browser.chrome || Browser.safari))
document.removeEvent('keydown', this.bound.keyboardInput);
else
document.removeEvent('keypress', this.bound.keyboardInput);
this.fireHooks('cleanup');
this.fireEvent('hide', [this]);
},
// hide the FM info <div>s. do NOT hide the outer info <div> itself, as the Uploader (and possibly other derivatives) may choose to show their own content there!
show_our_info_sections: function(state) {
if (!state)
{
this.info_head.fade(0).get('tween').chain(function() {
this.element.setStyle('display', 'none');
});
this.preview_area.fade(0).get('tween').chain(function() {
this.element.setStyle('display', 'none');
});
}
else
{
this.info_head.setStyle('display', 'block').fade(1);
this.preview_area.setStyle('display', 'block').fade(1);
}
},
open_on_click: function(e) {
if (e) e.stop();
if (!this.Current)
return;
var file = this.Current.retrieve('file');
this.fireEvent('complete', [
(this.options.deliverPathAsLegalURL ? file.path : this.escapeRFC3986(this.normalize('/' + this.root + file.path))), // the absolute URL for the selected file, rawURLencoded
file, // the file specs: .name, .path, .size, .date, .mime, .icon, .icon48, .thumb48, .thumb250
this
]);
// Only hide if hideOnSelect is true
if (this.options.hideOnSelect)
{
this.hide();
}
},
download_on_click: function(e) {
e.stop();
if (!this.Current) {
return;
}
this.diag.log('download: ', this.Current.retrieve('file'));
var file = this.Current.retrieve('file');
this.download(file);
},
download: function(file) {
var self = this;
var dummyframe_active = false;
// the chained display:none code inside the Tips class doesn't fire when the 'Save As' dialog box appears right away (happens in FF3.6.15 at least):
if (this.tips.tip) {
this.tips.tip.setStyle('display', 'none');
}
// discard old iframe, if it exists:
if (this.downloadIframe)
{
// remove from the menu (dispose) and trash it (destroy)
this.downloadIframe.dispose().destroy();
this.downloadIframe = null;
}
if (this.downloadForm)
{
// remove from the menu (dispose) and trash it (destroy)
this.downloadForm.dispose().destroy();
this.downloadForm = null;
}
this.downloadIframe = new IFrame({
src: 'about:blank',
name: '_downloadIframe',
styles: {
display: 'none'
},
events: {
load: function()
{
var iframe = this;
self.diag.log('download response: ', this, ', iframe: ', self.downloadIframe, ', ready: ', (1 * dummyframe_active));
// make sure we don't act on premature firing of the event in MSIE / Safari browsers:
if (!dummyframe_active)
return;
var response = null;
Function.attempt(function() {
response = iframe.contentDocument.documentElement.textContent;
},
function() {
response = iframe.contentWindow.document.innerText;
},
function() {
response = iframe.contentDocument.innerText;
},
function() {
response = "{status: 0, error: \"Download: download assumed okay: can't find response.\"}";
}
);
var j = JSON.decode(response);
if (j && !j.status)
{
self.showError('' + j.error);
}
else if (!j)
{
self.showError('bugger! No or faulty JSON response! ' + response);
}
}
}
});
this.menu.adopt(this.downloadIframe);
this.downloadForm = new Element('form', {target: '_downloadIframe', method: 'post', enctype: 'multipart/form-data'});
this.menu.adopt(this.downloadForm);
var tx_cfg = this.options.mkServerRequestURL(this, 'download', Object.merge({},
this.options.propagateData,
{
file: file.path,
filter: this.options.filter
}));
this.downloadForm.action = tx_cfg.url;
Object.each(tx_cfg.data,
function(v, k)
{
this.downloadForm.adopt((new Element('input')).set({type: 'hidden', name: k, value: v}));
}.bind(this));
dummyframe_active = true;
return this.downloadForm.submit();
},
create_on_click: function(e) {
e.stop();
var input = new Element('input', {'class': 'createDirectory'});
var click_ok_f = (function(e) {
this.diag.log('create on click: KEYBOARD handler: key press: ', e);
if (e.key === 'enter') {
e.stopPropagation();
e.target.getParent('div.filemanager-dialog').getElement('button.filemanager-dialog-confirm').fireEvent('click');
}
}).bind(this);
new FileManager.Dialog(this.language.createdir, {
language: {
confirm: this.language.create,
decline: this.language.cancel
},
content: [
input
],
autofocus_on: 'input.createDirectory',
zIndex: this.options.zIndex + 900,
onOpen: this.onDialogOpen.bind(this),
onClose: (function() {
input.removeEvent('keyup', click_ok_f);
this.onDialogClose();
}).bind(this),
onShow: (function() {
this.diag.log('add key up on create dialog:onShow');
input.addEvent('keyup', click_ok_f);
}).bind(this),
onConfirm: (function() {
if (this.Request) this.Request.cancel();
// abort any still running ('antiquated') fill chunks and reset the store before we set up a new one:
this.reset_view_fill_store();
var tx_cfg = this.options.mkServerRequestURL(this, 'create', {
file: input.get('value'),
directory: this.CurrentDir.path,
filter: this.options.filter
});
this.Request = new FileManager.Request({
url: tx_cfg.url,
data: tx_cfg.data,
onRequest: function() {},
onSuccess: (function(j) {
if (!j || !j.status) {
this.browserLoader.fade(0);
return;
}
this.deselect(null);
this.show_our_info_sections(false);
// make sure we store the JSON list!
this.reset_view_fill_store(j);
// the 'view' request may be an initial reload: keep the startindex (= page shown) intact then:
this.fill(j, this.get_view_fill_startindex());
}).bind(this),
onComplete: function() {},
onError: (function(text, error) {
this.browserLoader.fade(0);
}).bind(this),
onFailure: (function(xmlHttpRequest) {
this.browserLoader.fade(0);
}).bind(this)
}, this).send();
}).bind(this)
});
},
deselect: function(el) {
if (el && this.Current != el) {
return;
}
this.diag.log('deselect:Current', el);
if (el) {
this.fillInfo();
}
if (this.Current) {
this.Current.removeClass('selected');
}
this.Current = null;
this.switchButton4Current();
},
// add the ability to preselect a file in the dir
load: function(dir, preselect) {
if (typeof preselect === 'undefined') preselect = null;
this.deselect(null);
this.show_our_info_sections(false);
if (this.Request) this.Request.cancel();
this.diag.log("### 'view' request: onRequest invoked @ load(): ", dir, ', preselect: ', preselect);
// abort any still running ('antiquated') fill chunks and reset the store before we set up a new one:
this.reset_view_fill_store();
var tx_cfg = this.options.mkServerRequestURL(this, 'view', {
directory: dir,
filter: this.options.filter,
file_preselect: (preselect || '')
});
this.diag.log('load(): view URI: ', dir, this.listType, tx_cfg);
this.Request = new FileManager.Request({
url: tx_cfg.url,
data: tx_cfg.data,
onRequest: function() {},
onSuccess: (function(j) {
this.diag.log("### 'view' request: onSuccess invoked", j);
if (!j || !j.status) {
this.browserLoader.fade(0);
return;
}
if(this.listType == 'thumb' && (j.files.length + j.dirs.length) > this.options.listMaxSuggestedDirSizeForThumbnails)
{
this.listType = 'list';
this.browserMenu_thumb.setStyle('opacity',0.5);
this.browserMenu_list.setStyle('opacity',1);
this.browserMenu_thumb.store('set', false);
this.browserMenu_list.store('set', true);
}
// make sure we store the JSON list!
this.reset_view_fill_store(j);
// the 'view' request may be an initial reload: keep the startindex (= page shown) intact then:
// Xinha: add the ability to preselect a file in the dir
var start_idx = this.get_view_fill_startindex();
preselect = null;
if (j.preselect_index > 0)
{
start_idx = j.preselect_index - 1;
preselect = j.preselect_name;
}
this.fill(j, start_idx, null, null, preselect);
}).bind(this),
onComplete: (function() {
this.diag.log("### 'view' request: onComplete invoked");
this.fitSizes();
}).bind(this),
onError: (function(text, error) {
// a JSON error
this.diag.log("### 'view' request: onError invoked", text, error);
this.browserLoader.fade(0);
}).bind(this),
onFailure: (function(xmlHttpRequest) {
// a generic (non-JSON) communication failure
this.diag.log("### 'view' request: onFailure invoked", xmlHttpRequest);
this.browserLoader.fade(0);
}).bind(this)
}, this).send();
},
delete_from_dircache: function(file)
{
var items;
var i;
if (this.view_fill_json)
{
if (file.mime === 'text/directory')
{
items = this.view_fill_json.dirs;
}
else
{
items = this.view_fill_json.files;
}
for (i = items.length - 1; i >= 0; i--)
{
var item = items[i];
if (item.name === file.name)
{
items.splice(i, 1);
break;
}
}
}
},
destroy_noQasked: function(file) {
if (this.Request) this.Request.cancel();
this.browserLoader.fade(1);
if ((typeof jsGET !== 'undefined') && this.storeHistory)
{
if (file.mime !== 'text/directory')
{
// TODO: really, a full check should also check whether the fmPath equals the this.CurrentDir.path
if (file.name === jsGET.get('fmFile'))
{
// this will ensure the subsequent fill() action will revert the detail view to the directory details.
jsGET.remove(['fmFile']);
}
}
}
var tx_cfg = this.options.mkServerRequestURL(this, 'destroy', {
file: file.name,
directory: this.CurrentDir.path,
filter: this.options.filter
});
this.Request = new FileManager.Request({
url: tx_cfg.url,
data: tx_cfg.data,
onRequest: function() {},
onSuccess: (function(j) {
if (!j || !j.status) {
this.browserLoader.fade(0);
return;
}
this.fireEvent('modify', [Object.clone(file), j, 'destroy', this]);
// remove entry from cached JSON directory list and remove the item from the view.
// This is particularly important when working on a paginated directory and afterwards the pages are jumped back & forth:
// the next time around, this item should NOT appear in the list anymore!
this.deselect(file.element);
var rerendering_list = false;
if (this.view_fill_json)
{
this.delete_from_dircache(file); /* do NOT use j.name, as that one can be 'cleaned up' as part of the 'move' operation! */
// minor caveat: when we paginate the directory list, then browsing to the next page will skip one item (which would
// have been the first on the next page). The brute-force fix for this is to force a re-render of the page when in
// pagination view mode:
if (this.view_fill_json.dirs.length + this.view_fill_json.files.length > this.listPaginationLastSize)
{
// similar activity as load(), but without the server communication...
// abort any still running ('antiquated') fill chunks and reset the store before we set up a new one:
this.RequestQueue.cancel_bulk('fill');
clearTimeout(this.view_fill_timer);
this.view_fill_timer = null;
// was here before
}
}
// -> move this here, so it always reloads the thumbnail pane in the preview window
rerendering_list = true;
this.fill(null, this.get_view_fill_startindex(), this.listPaginationLastSize);
// make sure fade does not clash with parallel directory (re)load:
if (!rerendering_list)
{
var p = file.element.getParent();
if (p) {
p.fade(0).get('tween').chain(function() {
this.element.destroy();
});
}
}
this.browserLoader.fade(0);
// clear preview pane thumbnails
// this.dir_filelist.empty();
}).bind(this),
onComplete: function() {},
onError: (function(text, error) {
this.browserLoader.fade(0);
}).bind(this),
onFailure: (function(xmlHttpRequest) {
this.browserLoader.fade(0);
}).bind(this)
}, this).send();
},
destroy: function(file) {
if (this.options.hideOnDelete) {
this.destroy_noQasked(file);
}
else {
new FileManager.Dialog(this.language.destroyfile, {
language: {
confirm: this.language.destroy,
decline: this.language.cancel
},
zIndex: this.options.zIndex + 900,
onOpen: this.onDialogOpen.bind(this),
onClose: this.onDialogClose.bind(this),
onConfirm: (function() {
this.destroy_noQasked(file);
}).bind(this)
});
}
},
rename: function(file) {
var name = file.name;
var input = new Element('input', {'class': 'rename', value: name});
this.diag.log('### rename: ', Object.clone(file));
new FileManager.Dialog(this.language.renamefile, {
language: {
confirm: this.language.rename,
decline: this.language.cancel
},
content: [
input
],
autofocus_on: 'input.rename',
zIndex: this.options.zIndex + 900,
onOpen: this.onDialogOpen.bind(this),
onClose: this.onDialogClose.bind(this),
onShow: (function() {
this.diag.log('add key up on rename dialog:onShow');
input.addEvent('keyup', (function(e) {
this.diag.log('rename: KEYBOARD handler: key press: ', e);
if (e.key === 'enter') {
e.stopPropagation();
e.target.getParent('div.filemanager-dialog').getElement('button.filemanager-dialog-confirm').fireEvent('click');
}
}).bind(this));
}).bind(this),
onConfirm: (function() {
if (this.Request) this.Request.cancel();
this.browserLoader.fade(1);
this.diag.log('### rename: going to rename: ', Object.clone(file), ' to ', input.get('value'));
var tx_cfg = this.options.mkServerRequestURL(this, 'move', {
file: file.name,
name: input.get('value'),
directory: this.CurrentDir.path,
filter: this.options.filter
});
this.Request = new FileManager.Request({
url: tx_cfg.url,
data: tx_cfg.data,
onRequest: function() {},
onSuccess: (function(j) {
if (!j || !j.status) {
this.browserLoader.fade(0);
return;
}
this.diag.log('move : onSuccess: file = ', Object.clone(file), ', json: ', j);
this.fireEvent('modify', [Object.clone(file), j, 'rename', this]);
file.element.getElement('span.filemanager-filename').set('text', j.name).set('title', j.name);
file.element.addClass('selected');
file.name = j.name;
this.diag.log('move : onSuccess going to fillInfo: file = ', Object.clone(file), ', json: ', j);
this.fillInfo(file);
this.browserLoader.fade(0);
}).bind(this),
onComplete: function() {},
onError: (function(text, error) {
this.browserLoader.fade(0);
}).bind(this),
onFailure: (function(xmlHttpRequest) {
this.browserLoader.fade(0);
}).bind(this)
}, this).send();
}).bind(this)
});
},
browserSelection: function(direction) {
var csel,current;
this.diag.log('browserSelection : direction = ', direction);
if (this.browser.getElement('li') === null) return;
if (direction === 'go-bottom')
{
// select first item of next page
current = this.browser.getFirst('li').getElement('span.fi');
// blow away any lingering 'selected' after a page switch like that
csel = this.browser.getElement('span.fi.selected');
if (csel !== null)
csel.removeClass('selected');
}
else if (direction === 'go-top')
{
// select last item of previous page
current = this.browser.getLast('li').getElement('span.fi');
// blow away any lingering 'selected' after a page switch like that
csel = this.browser.getElement('span.fi.selected');
if (csel !== null)
csel.removeClass('selected');
}
else if (this.browser.getElement('span.fi.hover') === null && this.browser.getElement('span.fi.selected') === null)
{
// none is selected: select first item (folder/file)
current = this.browser.getFirst('li').getElement('span.fi');
}
else if(direction === 'current') {
current = this.Current;
}
else
{
// select the current file/folder or the one with hover
current = null;
if (this.browser.getElement('span.fi.hover') === null && this.browser.getElement('span.fi.selected') !== null) {
current = this.browser.getElement('span.fi.selected');
}
else if (this.browser.getElement('span.fi.hover') !== null) {
current = this.browser.getElement('span.fi.hover');
}
}
this.browser.getElements('span.fi.hover').each(function(span) {
span.removeClass('hover');
});
var stepsize = 1, next, currentFile;
switch (direction) {
// go down
case 'end':
stepsize = 1E5;
/* fallthrough */
case 'pagedown':
if (stepsize == 1) {
if (current.getPosition(this.browserScroll).y + current.getSize().y * 2 < this.browserScroll.getSize().y) {
stepsize = Math.floor((this.browserScroll.getSize().y - current.getPosition(this.browserScroll).y) / current.getSize().y) - 1;
if (stepsize < 1)
stepsize = 1;
}
else {
stepsize = Math.floor(this.browserScroll.getSize().y / current.getSize().y);
}
}
/* fallthrough */
case 'down':
current = current.getParent('li');
this.diag.log('key DOWN: stepsize = ', stepsize);
// when we're at the bottom of the view and there are more pages, go to the next page:
next = current.getNext('li');
if (next === null)
{
if (this.paging_goto_next(null, 'go-bottom'))
break;
}
else
{
for ( ; stepsize > 0; stepsize--) {
next = current.getNext('li');
if (next === null)
break;
current = next;
}
}
current = current.getElement('span.fi');
/* fallthrough */
case 'go-bottom': // 'faked' key sent when done shifting one pagination page down
current.addClass('hover');
this.Current = current;
direction = 'down';
break;
// go up
case 'home':
stepsize = 1E5;
/* fallthrough */
case 'pageup':
if (stepsize == 1) {
// when at the top of the viewport, a full page scroll already happens /visually/ when you go up 1: that one will end up at the /bottom/, after all.
stepsize = Math.floor(current.getPosition(this.browserScroll).y / current.getSize().y);
if (stepsize < 1)
stepsize = 1;
}
/* fallthrough */
case 'up':
current = current.getParent('li');
this.diag.log('key UP: stepsize = ', stepsize);
// when we're at the top of the view and there are pages before us, go to the previous page:
var previous = current.getPrevious('li');
if (previous === null)
{
if (this.paging_goto_prev(null, 'go-top'))
break;
}
else
{
for ( ; stepsize > 0; stepsize--) {
previous = current.getPrevious('li');
if (previous === null)
break;
current = previous;
}
}
current = current.getElement('span.fi');
/* fallthrough */
case 'go-top': // 'faked' key sent when done shifting one pagination page up
current.addClass('hover');
this.Current = current;
direction = 'up';
break;
case 'none': // 'faked' key sent when picking a row 'remotely', i.e. when we don't know where we are currently, but when we want to scroll to 'current' anyhow
current.addClass('hover');
this.Current = current;
break;
// select
case 'enter':
this.storeHistory = true;
this.Current = current;
csel = this.browser.getElement('span.fi.selected');
if (csel !== null) // remove old selected one
csel.removeClass('selected');
current.addClass('selected');
currentFile = current.retrieve('file');
this.diag.log('on key ENTER file = ', currentFile);
if (currentFile.mime === 'text/directory') {
this.load(currentFile.path /*.replace(this.root,'')*/);
}
else {
this.fillInfo(currentFile);
}
break;
// delete file/directory:
case 'delete':
this.storeHistory = true;
this.Current = current;
this.browser.getElements('span.fi.selected').each(function(span) {
span.removeClass('selected');
});
// and before we go and delete the entry, see if we pick the next one down or up as our next cursor position:
var parent = current.getParent('li');
next = parent.getNext('li');
if (next === null) {
next = parent.getPrevious('li');
}
if (next !== null) {
next = next.getElement('span.fi');
next.addClass('hover');
}
currentFile = current.retrieve('file');
this.diag.log('on key DELETE file = ', currentFile);
this.destroy(currentFile);
current = next;
this.Current = current;
break;
}
// make sure to scroll the view so the selected/'hovered' item is within visible range:
this.diag.log('key handler: current X/Y = ', current.getPosition(this.browserScroll), ', H/W/SCROLL = ', this.browserScroll.getSize(), ', 1U/SIZE = ', current.getSize());
var dy, browserScrollFx;
if (direction !== 'up' && current.getPosition(this.browserScroll).y + current.getSize().y * 2 >= this.browserScroll.getSize().y)
{
// make scroll duration slightly dependent on the distance to travel:
dy = (current.getPosition(this.browserScroll).y + current.getSize().y * 2 - this.browserScroll.getSize().y);
dy = 50 * dy / this.browserScroll.getSize().y;
this.diag.log('key @ direction = UP: DUR: ', dy);
browserScrollFx = new Fx.Scroll(this.browserScroll, { duration: (dy < 150 ? 150 : dy > 1000 ? 1000 : dy.toInt()) });
browserScrollFx.toElement(current);
}
else if (direction !== 'down' && current.getPosition(this.browserScroll).y <= current.getSize().y)
{
var sy = this.browserScroll.getScroll().y + current.getPosition(this.browserScroll).y - this.browserScroll.getSize().y + current.getSize().y * 2;
// make scroll duration slightly dependent on the distance to travel:
dy = this.browserScroll.getScroll().y - sy;
dy = 50 * dy / this.browserScroll.getSize().y;
this.diag.log('key @ direction = DOWN: SY = ', sy, ', DUR: ', dy);
browserScrollFx = new Fx.Scroll(this.browserScroll, { duration: (dy < 150 ? 150 : dy > 1000 ? 1000 : dy.toInt()) });
browserScrollFx.start(current.getPosition(this.browserScroll).x, (sy >= 0 ? sy : 0));
}
},
// -> cancel dragging
revert_drag_n_drop: function(el) {
el.fade(1).removeClass('drag').removeClass('move').setStyles({
// 'z-index': 'auto',
position: 'relative',
width: 'auto',
left: 0,
top: 0
}).inject(el.retrieve('parent'));
try{ el.setStyle('z-index', 'auto'); } catch(e) { el.setStyle('z-index', ''); } // IE<8 Complains about 'auto' for z-index
// also dial down the opacity of the icons within this row (download, rename, delete):
var icons = el.getElements('img.browser-icon');
if (icons) {
icons.each(function(icon) {
icon.fade(0);
});
}
this.diag.log('DISABLE keyboard up/down on revert');
this.drag_is_active = false;
this.imageadd.fade(0);
},
// clicked 'first' button in the paged list/thumb view:
paging_goto_prev: function(e, kbd_dir)
{
if (e) e.stop();
var startindex = this.get_view_fill_startindex();
if (!startindex)
return false;
return this.paging_goto_helper(startindex - this.listPaginationLastSize, this.listPaginationLastSize, kbd_dir);
},
paging_goto_next: function(e, kbd_dir)
{
if (e) e.stop();
var startindex = this.get_view_fill_startindex();
if (this.view_fill_json && startindex > this.view_fill_json.dirs.length + this.view_fill_json.files.length - this.listPaginationLastSize)
return false;
return this.paging_goto_helper(startindex + this.listPaginationLastSize, this.listPaginationLastSize, kbd_dir);
},
paging_goto_first: function(e, kbd_dir)
{
if (e) e.stop();
var startindex = this.get_view_fill_startindex();
if (!startindex)
return false;
return this.paging_goto_helper(0, null, kbd_dir);
},
paging_goto_last: function(e, kbd_dir)
{
if (e) e.stop();
var startindex = this.get_view_fill_startindex();
if (this.view_fill_json && startindex > this.view_fill_json.dirs.length + this.view_fill_json.files.length - this.options.listPaginationSize)
return false;
return this.paging_goto_helper(2E9 /* ~ maxint */, null, kbd_dir);
},
paging_goto_helper: function(startindex, pagesize, kbd_dir)
{
// similar activity as load(), but without the server communication...
this.deselect(null);
this.show_our_info_sections(false);
// abort any still running ('antiquated') fill chunks and reset the store before we set up a new one:
this.RequestQueue.cancel_bulk('fill');
clearTimeout(this.view_fill_timer);
this.view_fill_timer = null;
return this.fill(null, startindex, pagesize, kbd_dir);
},
fill: function(j, startindex, pagesize, kbd_dir, preselect)
{
var j_item_count;
if (typeof preselect === 'undefined') preselect = null;
if (!pagesize)
{
pagesize = this.options.listPaginationSize;
this.listPaginationLastSize = pagesize;
}
// else: pagesize specified means stick with that one. (useful to keep pagesize intact when going prev/next)
if (!j)
{
j = this.view_fill_json;
}
j_item_count = j.dirs.length + j.files.length;
startindex = parseInt(startindex, 10); // make sure it's an int number
if (isNaN(startindex))
{
startindex = 0;
}
if (!pagesize)
{
// no paging: always go to position 0 then!
startindex = 0;
}
else if (startindex > j_item_count)
{
startindex = j_item_count;
}
else if (startindex < 0)
{
startindex = 0;
}
// always make sure startindex is exactly on a page edge: this is important to keep the page numbers
// in the tooltips correct!
startindex = Math.floor(startindex / pagesize);
startindex *= pagesize;
// keyboard navigation sets the 'hover' class on the 'current' item: remove any of those:
this.browser.getElements('span.fi.hover').each(function(span) {
span.removeClass('hover');
});
this.diag.log('# fill: JSON = ', j, ', mgr: ', this);
this.root = j.root;
this.CurrentDir = j.this_dir;
this.browser.empty();
// Adding the thumbnail list in the preview panel: blow away any pre-existing list now, as we'll generate a new one now:
this.dir_filelist_thumbUl.empty();
// set history
if (typeof jsGET !== 'undefined' && this.storeHistory)
{
jsGET.set({'fmPath': this.CurrentDir.path});
}
var current_path = this.normalize(this.root + this.CurrentDir.path);
var text = [], pre = [];
// on error reported by backend, there WON'T be a JSON 'root' element at all times:
//
// TODO: how to handle that error condition correctly?
if (!j.root)
{
this.showError('' + j.error);
return false;
}
var rootPath = '/' + j.root;
var rootParent = this.dirname(rootPath);
var rplen = rootParent.length;
current_path.split('/').each((function(folderName) {
if (!folderName) return;
pre.push(folderName);
var path = ('/'+pre.join('/')+'/');
this.diag.log('on fill: display directory path chunks: JSON root = ', j.root, ', path: ' , path, ', folder: ', folderName, ', root: ', rootPath, ', parent: ', rootParent);
if (path.length <= rplen) {
// add non-clickable path
text.push(new Element('span', {'class': 'icon', text: folderName}));
} else {
// add clickable path
text.push(new Element('a', {
'class': 'icon',
href: '#',
text: folderName
}).addEvent('click', (function(e) {
e.stop();
path = path.replace(j.root,'');
this.diag.log('## path section - click event: ', e, ', path: ', path);
this.load(path);
}).bind(this))
);
}
text.push(new Element('span', {text: ' / '}));
}).bind(this));
text.pop();
text[text.length-1].addClass('selected').removeEvents('click').addEvent('click', (function(e) {
e.stop();
// show the 'directory' info in the detail pane again (this is a way to get back from previewing single files to previewing the directory as a gallery)
this.diag.log('click: fillInfo: current directory!');
this.fillInfo();
}).bind(this));
this.selectablePath.set('value', '/' + current_path);
this.clickablePath.empty().adopt(new Element('span', {text: '/ '}), text);
if (!j.dirs || !j.files) {
return false;
}
// ->> generate browser list
var els = [[], []];
/*
* For very large directories, where the number of directories in there and/or the number of files is HUGE (> 200),
* we DISABLE drag&drop functionality.
*
* Yes, we could have opted for the alternative, which is splitting up the .makeDraggable() activity in multiple
* setTimeout(callback, 0) initiated chunks in order to spare the user the hassle of a 'slow script' dialog,
* but in reality drag&drop is ludicrous in such an environment; currently we do not (yet) support autoscrolling
* the list to enable drag&dropping it to elements further away that the current viewport can hold at the same time,
* but drag&drop in a 500+ image carrying directory is resulting in a significant load of the browser anyway;
* alternative means to move/copy files should be provided in such cases instead.
*
* Hence we run through the list here and abort / limit the drag&drop assignment process when the hardcoded number of
* directories or files have been reached (support_DnD_for_this_dir).
*
* TODO: make these numbers 'auto adaptive' based on timing measurements: how long does it take to initialize
* a view on YOUR machine? --> adjust limits accordingly.
*/
var support_DnD_for_this_dir = this.allow_DnD(j, pagesize);
var starttime = new Date().getTime();
this.diag.log('fill list size = ', j_item_count);
var endindex = j_item_count;
var paging_now = 0;
if (pagesize)
{
// endindex MAY point beyond j_item_count; that's okay; we check the boundary every time in the other fill chunks.
endindex = startindex + pagesize;
// however for reasons of statistics gathering, we keep it bound to j_item_count at the moment:
if (endindex > j_item_count) endindex = j_item_count;
if (pagesize < j_item_count)
{
var pagecnt = Math.ceil(j_item_count / pagesize);
var curpagno = Math.floor(startindex / pagesize) + 1;
this.browser_paging_info.set('text', '' + curpagno + '/' + pagecnt);
if (curpagno > 1)
{
this.browser_paging_first.set('title', this.language.goto_page + ' 1');
this.browser_paging_first.fade(1);
this.browser_paging_prev.set('title', this.language.goto_page + ' ' + (curpagno - 1));
this.browser_paging_prev.fade(1);
}
else
{
this.browser_paging_first.set('title', '---');
this.browser_paging_first.fade(0.25);
this.browser_paging_prev.set('title', '---');
this.browser_paging_prev.fade(0.25);
}
if (curpagno < pagecnt)
{
this.browser_paging_last.set('title', this.language.goto_page + ' ' + pagecnt);
this.browser_paging_last.fade(1);
this.browser_paging_next.set('title', this.language.goto_page + ' ' + (curpagno + 1));
this.browser_paging_next.fade(1);
}
else
{
this.browser_paging_last.set('title', '---');
this.browser_paging_last.fade(0.25);
this.browser_paging_next.set('title', '---');
this.browser_paging_next.fade(0.25);
}
paging_now = 1;
}
}
this.browser_paging.fade(paging_now);
// fix for MSIE8: also fade out the pagination icons themselves
if (!paging_now)
{
this.browser_paging_first.fade(0);
this.browser_paging_prev.fade(0);
this.browser_paging_last.fade(0);
this.browser_paging_next.fade(0);
}
// remember pagination position history
this.store_view_fill_startindex(startindex);
// reset the fillInfo fire marker:
this.fillInfoOnFillFired = false;
this.view_fill_timer = this.fill_chunkwise_1.delay(1, this, [startindex, endindex, endindex - startindex, pagesize, support_DnD_for_this_dir, starttime, els, kbd_dir, preselect]);
return true;
},
list_row_maker: function(thumbnail_url, file)
{
return file.element = new Element('span', {'class': 'fi ' + this.listType, href: '#'}).adopt(
new Element('span', {
'class': this.listType,
'styles': {
'background-image': 'url(' + (thumbnail_url ? thumbnail_url : this.assetBasePath + 'Images/loader.gif') + ')'
}
}).addClass('fm-thumb-bg'),
new Element('span', {'class': 'filemanager-filename', text: file.name, title: file.name})
).store('file', file);
},
dir_gallery_item_maker: function(thumbnail_url, file)
{
var el = new Element('div', {
'class': 'fi',
'title': file.name
}).adopt(
new Element('div', {
'class': 'dir-gal-thumb-bg',
'styles': {
'width': this.options.thumbSize4DirGallery + 'px',
'height': this.options.thumbSize4DirGallery + 'px',
'background-image': 'url(' + (thumbnail_url ? thumbnail_url : this.assetBasePath + 'Images/loader.gif') + ')'
}
}),
new Element('div', {
'class': 'name',
'styles': {
'width': this.options.thumbSize4DirGallery + 'px'
},
'text': file.name
})
);
this.tips.attach(el);
return el;
},
dir_gallery_set_actual_img: function(file, dg_el)
{
// calculate which thumb to use and how to center it:
var img_url, iw, ih, ds, mt, mb, ml, mr, ratio;
ds = this.options.thumbSize4DirGallery;
if (ds > 48)
{
img_url = file.thumb250;
iw = file.thumb250_width;
ih = file.thumb250_height;
}
else
{
img_url = file.thumb48;
iw = file.thumb48_width;
ih = file.thumb48_height;
}
// 'zoom' image to fit area:
if (iw > ds)
{
var redux = ds / iw;
iw *= redux;
ih *= redux;
}
if (ih > ds)
{
var redux = ds / ih;
iw *= redux;
ih *= redux;
}
iw = Math.round(iw);
ih = Math.round(ih);
ml = Math.round((ds - iw) / 2);
mr = ds - ml - iw;
mt = Math.round((ds - ih) / 2);
mb = ds - mt - ih;
var self = this;
Asset.image(img_url, {
styles: {
width: iw,
height: ih,
'margin-left': ml,
'margin-top': mt,
'margin-right': mr,
'margin-bottom': mb
},
onLoad: function() {
var img_el = this;
var img_div = dg_el.getElement('div.dir-gal-thumb-bg').setStyle('background-image', '');
img_div.adopt(img_el);
},
onError: function() {
self.diag.log('dirgallery image asset: error!');
var iconpath = self.assetBasePath + 'Images/Icons/Large/default-error.png';
dg_el.getElement('div.dir-gal-thumb-bg').setStyle('background-image', 'url(' + iconpath + ')');
},
onAbort: function() {
self.diag.log('dirgallery image asset: ABORT!');
var iconpath = self.assetBasePath + 'Images/Icons/Large/default-error.png';
dg_el.getElement('div.dir-gal-thumb-bg').setStyle('background-image', 'url(' + iconpath + ')');
}
});
},
/*
* The old one-function-does-all fill() would take an awful long time when processing large directories. This function
* contains the most costly code chunk of the old fill() and has adjusted the looping through the j.dirs[] and j.files[] lists
* in such a way that we can 'chunk it up': we can measure the time consumed so far and when we have spent more than
* X milliseconds in the loop, we stop and allow the loop to commence after a minimal delay.
*
* The delay is the way to relinquish control to the browser and as a thank-you NOT get the dreaded
* 'slow script, continue or abort?' dialog in your face. Ahh, the joy of cooperative multitasking is back again! :-)
*/
fill_chunkwise_1: function(startindex, endindex, render_count, pagesize, support_DnD_for_this_dir, starttime, els, kbd_dir, preselect) {
var idx, file, loop_duration;
var self = this;
var j = this.view_fill_json;
var loop_starttime = new Date().getTime();
var fmFile = (typeof jsGET !== 'undefined' ? jsGET.get('fmFile') : null);
var duration = new Date().getTime() - starttime;
//this.diag.log(' + time duration @ fill_chunkwise_1(', startindex, '): ', duration);
/*
* Note that the '< j.dirs.length' / '< j.files.length' checks MUST be kept around: one of the fastest ways to abort/cancel
* the render is emptying the dirs[] + files[] array, as that would abort the loop on the '< j.dirs.length' / '< j.files.length'
* condition.
*
* This, together with killing our delay-timer, is done when anyone calls reset_view_fill_store() to
* abort this render pronto.
*/
// first loop: only render directories, when the indexes fit the range: 0 .. j.dirs.length-1
// Assume several directory aspects, such as no thumbnail hassle (it's one of two icons anyway, really!)
var el, editButtons;
for (idx = startindex; idx < endindex && idx < j.dirs.length; idx++)
{
file = j.dirs[idx];
if (idx % 10 === 0) {
// try not to spend more than 100 msecs per (UI blocking!) loop run!
loop_duration = new Date().getTime() - loop_starttime;
duration = new Date().getTime() - starttime;
//this.diag.log(' + time taken so far = ', duration, ' / ', loop_duration, ' @ elcnt = ', idx);
/*
* Are we running in adaptive pagination mode? yes: calculate estimated new pagesize and adjust average (EMA) when needed.
*
* Do this here instead of at the very end so that pagesize will adapt, particularly when user does not want to wait for
* this render to finish.
*/
this.adaptive_update_pagination_size(idx, endindex, render_count, pagesize, duration, 1.0 / 7.0, 1.1, 0.1 / 1000);
if (loop_duration >= 100)
{
this.view_fill_timer = this.fill_chunkwise_1.delay(1, this, [idx, endindex, render_count, pagesize, support_DnD_for_this_dir, starttime, els, kbd_dir, preselect]);
return; // end call == break out of loop
}
}
file.dir = j.path;
//this.diag.log('fill_chunkwise_1: dir = file: ', file, ' at index: ', idx);
// This is just a raw image
el = this.list_row_maker((this.listType === 'thumb' ? file.icon48 : file.icon), file);
//this.diag.log('add DIRECTORY click event to ', file);
el.addEvent('click', (function(e) {
self.diag.log('is_dir:CLICK: ', e);
var node = this;
self.relayClick.apply(self, [e, node]);
}).bind(el));
editButtons = [];
// rename, delete icon
if (file.name !== '..')
{
if (this.options.rename) editButtons.push('rename');
if (this.options.destroy) editButtons.push('destroy');
}
editButtons.each(function(v) {
//icons.push(
Asset.image(this.assetBasePath + 'Images/' + v + '.png', {title: this.language[v]}).addClass('browser-icon').setStyle('opacity', 0).addEvent('mouseup', (function(e, target) {
// this = el, self = FM instance
e.preventDefault();
this.store('edit', true);
// can't use 'file' in here directly anymore either:
var file = this.retrieve('file');
self.tips.hide();
self[v](file);
}).bind(el)).inject(el,'top');
//);
}, this);
els[1].push(el);
//if (file.name === '..') el.fade(0.7);
el.inject(new Element('li',{'class':this.listType}).inject(this.browser)).store('parent', el.getParent());
//icons = $$(icons.map((function(icon) {
// this.showFunctions(icon,icon,0.5,1);
// this.showFunctions(icon,el.getParent('li'),1);
//}).bind(this)));
// you CANNOT 'preselect' a directory as if it were a file, so we don't need to check against the 'preselect' or 'fmFile' values here!
}
// and another, ALMOST identical, loop to render the files. Note that these buggers have their own peculiarities... and make sure the index is adjusted to point into files[]
var dir_count = j.dirs.length;
// skip files[] rendering, when the startindex still points inside dirs[] ~ too many directories to fit any files on this page!
if (idx >= dir_count)
{
var dg_el;
for ( ; idx < endindex && idx - dir_count < j.files.length; idx++)
{
file = j.files[idx - dir_count];
if (idx % 10 === 0) {
// try not to spend more than 100 msecs per (UI blocking!) loop run!
loop_duration = new Date().getTime() - loop_starttime;
duration = new Date().getTime() - starttime;
//this.diag.log('time taken so far = ', duration, ' / ', loop_duration, ' @ elcnt = ', idx);
/*
* Are we running in adaptive pagination mode? yes: calculate estimated new pagesize and adjust average (EMA) when needed.
*
* Do this here instead of at the very end so that pagesize will adapt, particularly when user does not want to wait for
* this render to finish.
*/
this.adaptive_update_pagination_size(idx, endindex, render_count, pagesize, duration, 1.0 / 7.0, 1.1, 0.1 / 1000);
if (loop_duration >= 100)
{
this.view_fill_timer = this.fill_chunkwise_1.delay(1, this, [idx, endindex, render_count, pagesize, support_DnD_for_this_dir, starttime, els, kbd_dir, preselect]);
return; // end call == break out of loop
}
}
file.dir = j.path;
//this.diag.log('fill_chunkwise_1: files: file: ', file, ' at index: ', idx - dir_count);
// As we now have two views into the directory, we have to fetch the thumbnails, even when we're in 'list' view: the direcory gallery will need them!
// Besides, fetching the thumbs and all right after we render the directory also makes these thumbs + metadata available for drag&drop gallery and
// 'select mode', so they don't always have to ask for the meta data when it is required there and then.
if (file.thumb48 || this.listType !== 'thumb' || !file.thumbs_deferred)
{
// This is just a raw image
el = this.list_row_maker((this.listType === 'thumb' ? (file.thumb48 ? file.thumb48 : file.icon48) : file.icon), file);
dg_el = this.dir_gallery_item_maker(file.icon48, file);
if (file.thumb48)
{
this.dir_gallery_set_actual_img(file, dg_el);
}
}
else // thumbs_deferred...
{
// We must AJAX POST our propagateData, so we need to do the post and take the url to the
// thumbnail from the post results.
//
// The alternative here, taking only 1 round trip instead of 2, would have been to FORM POST
// to a tiny iframe, which is suitably sized to contain the generated thumbnail and the POST
// actually returning the binary image data, thus the iframe contents becoming the thumbnail image.
// update this one alongside the 'el':
dg_el = this.dir_gallery_item_maker(file.icon48, file);
el = (function(file, dg_el) { // Closure
var iconpath = this.assetBasePath + 'Images/Icons/' + (this.listType === 'thumb' ? 'Large/' : '') + 'default-error.png';
var list_row = this.list_row_maker((this.listType === 'thumb' ? file.icon48 : file.icon), file);
var tx_cfg = this.options.mkServerRequestURL(this, 'detail', {
directory: this.dirname(file.path),
file: file.name,
filter: this.options.filter,
mode: 'direct' + this.options.detailInfoMode
});
var req = new FileManager.Request({
url: tx_cfg.url,
data: tx_cfg.data,
fmDisplayErrors: false, // Should we display the error here? No, we just display the general error icon instead
onRequest: function() {},
onSuccess: (function(j) {
if (!j || !j.status || !j.thumb48)
{
list_row.getElement('span.fm-thumb-bg').setStyle('background-image', 'url(' + (this.listType === 'thumb' ? (j.icon48 ? j.icon48 : iconpath) : (j.icon ? j.icon : iconpath)) + ')');
dg_el.getElement('div.dir-gal-thumb-bg').setStyle('background-image', 'url(' + (j.icon48 ? j.icon48 : iconpath) + ')');
}
else
{
list_row.getElement('span.fm-thumb-bg').setStyle('background-image', 'url(' + (this.listType === 'thumb' ? j.thumb48 : j.icon) + ')');
if (j.thumb48)
{
this.dir_gallery_set_actual_img(j, dg_el);
}
}
// update the stored json for this file as well:
file = Object.merge(file, j);
delete file.error;
delete file.status;
delete file.content;
if (file.element)
{
file.element.store('file', file);
}
}).bind(this),
onError: (function(text, error) {
list_row.getElement('span.fm-thumb-bg').setStyle('background-image', 'url(' + iconpath + ')');
dg_el.getElement('div.dir-gal-thumb-bg').setStyle('background-image', 'url(' + iconpath + ')');
}).bind(this),
onFailure: (function(xmlHttpRequest) {
list_row.getElement('span.fm-thumb-bg').setStyle('background-image', 'url(' + iconpath + ')');
dg_el.getElement('div.dir-gal-thumb-bg').setStyle('background-image', 'url(' + iconpath + ')');
}).bind(this)
}, this);
this.RequestQueue.addRequest('fill:' + String.uniqueID(), req);
req.send();
return list_row;
}).bind(this)(file, dg_el);
}
/*
* WARNING: for some (to me) incomprehensible reason the old code which bound the event handlers to 'this==self' and which used the 'el' variable
* available here, does NOT WORK ANY MORE - tested in FF3.6. Turns out 'el' is pointing anywhere but where you want it by the time
* the event handler is executed.
*
* The 'solution' which I found was to rely on the 'self' reference instead and bind to 'el'. If the one wouldn't work, the other shouldn't,
* but there you have it: this way around it works. FF3.6.14 :-(
*
* EDIT 2011/03/16: the problem started as soon as the old Array.each(function(...) {...}) by the chunked code which uses a for loop:
*
* http://jibbering.com/faq/notes/closures/
*
* as it says there:
*
* A closure is formed when one of those inner functions is made accessible outside of the function in which it was
* contained, so that it may be executed after the outer function has returned. At which point it still has access to
* the local variables, parameters and inner function declarations of its outer function. Those local variables,
* parameter and function declarations (initially) >>>> have the values that they had when the outer function returned <<<<
* and may be interacted with by the inner function.
*
* The >>>> <<<< emphasis is mine: in the .each() code, each el was a separate individual, while due to the for loop,
* the last 'el' to exist at all is the one created during the last round of the loop in that chunk. Which explains the
* observed behaviour before the fix: the file names associated with the 'el' element object were always pointing
* at some item further down the list, not necessarily the very last one, but always these references were 'grouped':
* multiple rows would produce the same filename.
*
* EXTRA: 2011/04/09: why you don't want to add this event for any draggable item!
*
* It turns out that IE9 (IE6-8 untested as I write this) and Opera do NOT fire the 'click' event after the drag operation is
* 'cancel'led, while other browsers fire both (Chrome/Safari/FF3).
* For the latter ones, the event handler sequence after a simple click on a draggable item is:
* - Drag::onBeforeStart
* - Drag::onCancel
* - 'click'
* while a tiny amount of dragging produces this sequence instead:
* - Drag::onBeforeStart
* - Drag::onStart
* - Drag::onDrop
* - 'click'
*
* Meanwhile, Opera and IE9 do this:
* - Drag::onBeforeStart
* - Drag::onCancel
* - **NO** click event!
* while a tiny amount of dragging produces this sequence instead:
* - Drag::onBeforeStart
* - Drag::onStart
* - Drag::onDrop
* - **NO** click event!
*
* which explains why the old implementation did not simply register this 'click' event handler and had 'revert' fake the 'click'
* event instead.
* HOWEVER, the old way, using revert() (now called revert_drag_n_drop()) was WAY too happy to hit the 'click' event handler. In
* fact, the only spot where such 'manually firing' was desirable is when the drag operation is CANCELLED. And only there!
*/
// 2011/04/09: only register the 'click' event when the element is NOT a draggable:
if (!support_DnD_for_this_dir)
{
self.diag.log('add FILE click event to ', file);
el.addEvent('click', (function(e) {
self.diag.log('is_file:CLICK: ', e);
var node = this;
self.relayClick.apply(self, [e, node]);
}).bind(el));
}
// add double click functionality to the list elements
if(this.options.selectable === true) {
el.addEvent('dblclick', (function(e) {
self.diag.log('is_file:DBLCLICK: ', e);
var node = this;
self.relayDblClick.apply(self, [e, node]);
}).bind(el));
}
editButtons = [];
// download, rename, delete icon
if (this.options.download) editButtons.push('download');
if (this.options.rename) editButtons.push('rename');
if (this.options.destroy) editButtons.push('destroy');
editButtons.each(function(v) {
Asset.image(this.assetBasePath + 'Images/' + v + '.png', {title: this.language[v]}).addClass('browser-icon').setStyle('opacity', 0).addEvent('mouseup', (function(e, target) {
// this = el, self = FM instance
e.preventDefault();
this.store('edit', true);
// can't use 'file' in here directly anymore either:
var file = this.retrieve('file');
self.tips.hide();
self[v](file);
}).bind(el)).inject(el,'top');
}, this);
els[0].push(el);
el.inject(new Element('li',{'class':this.listType}).inject(this.browser)).store('parent', el.getParent());
// ->> LOAD the FILE/IMAGE from history when PAGE gets REFRESHED (only directly after refresh)
//this.diag.log('fill on PRESELECT (test): onShow = ', this.fillInfoOnFillFired, ', file = ', file, ', fmFile = ', fmFile, ', preselect = ', preselect);
if (!this.fillInfoOnFillFired)
{
if (preselect)
{
if (preselect === file.name)
{
this.deselect(null);
this.Current = file.element;
new Fx.Scroll(this.browserScroll,{duration: 'short', offset: {x: 0, y: -(this.browserScroll.getSize().y/4)}}).toElement(file.element);
file.element.addClass('selected');
this.diag.log('fill on PRESELECT: fillInfo: file = ', file);
this.fillInfo(file);
this.fillInfoOnFillFired = true;
}
}
else if (fmFile)
{
if (fmFile === file.name)
{
this.deselect(null);
this.Current = file.element;
new Fx.Scroll(this.browserScroll,{duration: 'short', offset: {x: 0, y: -(this.browserScroll.getSize().y/4)}}).toElement(file.element);
file.element.addClass('selected');
this.diag.log('fill: fillInfo: file = ', file);
this.fillInfo(file);
this.fillInfoOnFillFired = true;
}
}
}
// Partikule
// Thumbs list
// edit: Fabian Vogelsteller: made thumblist DRAGGABLE
// use a closure to keep a reference to the current dg_el, otherwise dg_el, file, etc. will carry the values they got at the end of the loop!
(function(dg_el, el, file)
{
var thumbLi = new Element('li');
dg_el.store('el_ref', el).store('file',file).store('parent',thumbLi);
thumbLi.inject(this.dir_filelist_thumbUl);
dg_el.inject(thumbLi);
}).bind(this)(dg_el, el, file);
// / Partikule
}
}
// when we get here, we have rendered all files in the current page and we know whether we have fired off a fillInfo on a preselect/history-recalled file now, or not:
if (!this.fillInfoOnFillFired)
{
this.diag.log('fill internal: fillInfo: file = ', j, j.this_dir);
this.fillInfo(j.this_dir);
this.fillInfoOnFillFired = true;
}
// check how much we've consumed so far:
duration = new Date().getTime() - starttime;
//this.diag.log(' + time taken in array traversal = ', duration);
// go to the next stage, right after these messages... ;-)
this.view_fill_timer = this.fill_chunkwise_2.delay(1, this, [render_count, pagesize, support_DnD_for_this_dir, starttime, els, kbd_dir]);
},
/*
* See comment for fill_chunkwise_1(): the makeDraggable() is a loop in itself and taking some considerable time
* as well, so make it happen in a 'fresh' run here...
*/
fill_chunkwise_2: function(render_count, pagesize, support_DnD_for_this_dir, starttime, els, kbd_dir) {
var duration = new Date().getTime() - starttime;
//this.diag.log(' + time duration @ fill_chunkwise_2() begin: ', duration);
// check how much we've consumed so far:
duration = new Date().getTime() - starttime;
//this.diag.log(' + time taken in array traversal + revert = ', duration);
// -> made preview thumblist DRAGGABLE
// add the thumbnail pane in preview window to the dropable elements
this.dir_filelist.getChildren('ul li div.fi').each((function(thumb){
els[0].push(thumb);
}).bind(this));
// make draggable
if (support_DnD_for_this_dir) {
// -> make draggable
$$(els[0]).makeDraggable({
droppables: $$(this.droppables.combine(els[1])),
//stopPropagation: true,
// We position the element relative to its original position; this ensures the drag always works with arbitrary container.
onDrag: (function(el, e)
{
var dpos = el.retrieve('delta_pos');
el.setStyles({
display: 'block',
left: e.page.x + dpos.x,
top: e.page.y + dpos.y
});
this.imageadd.setStyles({
'left': e.page.x + dpos.x - 12,
'top': e.page.y + dpos.y + 2
});
}).bind(this),
onBeforeStart: (function(el) {
// you CANNOT use .container to get good x/y coords as in standalone mode, this <div> has a bogus position;
//var cpos = this.container.getPosition();
// start the scroller
this.scroller.start();
}).bind(this),
// FIX: do not deselect item when aborting dragging _another_ item!
onCancel: (function(el) {
this.diag.log('draggable:onCancel', el);
this.scroller.stop();
this.revert_drag_n_drop(el);
/*
* Fixing the 'click' on FF+Opera (other browsers do get that event for any item which is made draggable):
* a basic mouse'click' appears as the event sequence onBeforeStart + onCancel.
*
* NOTE that onStart is NOT invoked! When it is, it's a drag operation, no matter if it's successful as a drag&drop or not.
*
* So we then manually fire the 'click' event. See also the comment near the 'click' event handler registration in fill_chunkwise_1()
* about the different behaviour in different browsers.
*/
this.relayClick(null, el);
}).bind(this),
onStart: (function(el, e) {
this.diag.log('draggable:onStart', el);
this.tips.hide();
var position = el.getPosition();
var dpos = {
x: position.x - e.page.x,
y: position.y - e.page.y
};
/*
* Use the element size (Y) for IE-fixing heuristics:
* in IE the mouse is already quite some distance away before the onStart fires,
* we need to restrict the vertical position of the dragged element in such a way
* that it will reside 'under the mouse cursor'.
*/
var elsize = el.getSize();
if (dpos.y > 0)
dpos.y = -Math.round(elsize.y / 2);
else if (dpos.y < -elsize.y)
dpos.y = -Math.round(elsize.y / 2);
this.diag.log('~~~ positions at start: ', position, dpos, e);
el.store('delta_pos', dpos);
el.addClass('drag').setStyles({
'z-index': this.options.zIndex + 1500,
'position': 'absolute',
'width': el.getWidth() - el.getStyle('paddingLeft').toInt() - el.getStyle('paddingRight').toInt(),
'display': 'none',
'left': e.page.x + dpos.x,
'top': e.page.y + dpos.y
}).inject(this.container);
el.fade(0.7).addClass('move');
this.diag.log('ENABLE keyboard up/down on drag start');
// FIX wrong visual when CONTROL key is kept depressed between drag&drops: the old code discarded the relevant keyboard handler; we simply switch visuals but keep the keyboard handler active.
// This state change will be reverted in revert_drag_n_drop().
this.drag_is_active = true;
this.imageadd.fade(0 + this.ctrl_key_pressed);
}).bind(this),
onEnter: function(el, droppable) {
droppable.addClass('droppable');
},
onLeave: function(el, droppable) {
droppable.removeClass('droppable');
},
onDrop: (function(el, droppable, e) {
this.diag.log('draggable:onDrop', el, droppable, e);
this.scroller.stop();
var is_a_move = !(e.control || e.meta);
this.drop_pending = 1 + is_a_move;
if (!is_a_move || !droppable) {
el.setStyles({left: 0, top: 0});
}
var dir = null;
if (droppable) {
droppable.addClass('selected').removeClass('droppable');
(function() {
droppable.removeClass('selected');
}).delay(300);
if (this.onDragComplete(el, droppable)) {
this.drop_pending = 0;
this.revert_drag_n_drop(el); // go and request the details anew, then refresh them in the view
return;
}
dir = droppable.retrieve('file');
this.diag.log('on drop dir = ', dir);
}
if ((!this.options.move_or_copy) || (is_a_move && !droppable)) {
this.drop_pending = 0;
this.revert_drag_n_drop(el); // go and request the details anew, then refresh them in the view
return;
}
this.revert_drag_n_drop(el); // do not send the 'detail' request in here: this.drop_pending takes care of that!
var file = el.retrieve('file');
this.diag.log('on drop file = ', file, ', current dir:', this.CurrentDir, ', droppable: ', droppable);
if (this.Request) this.Request.cancel();
var tx_cfg = this.options.mkServerRequestURL(this, 'move', {
file: file.name,
filter: this.options.filter,
directory: this.CurrentDir.path,
newDirectory: (dir ? dir.path : this.CurrentDir.path),
copy: is_a_move ? 0 : 1
});
this.Request = new FileManager.Request({
url: tx_cfg.url,
data: tx_cfg.data,
onSuccess: (function(j) {
if (!j || !j.status) {
this.drop_pending = 0;
this.browserLoader.fade(0);
return;
}
this.fireEvent('modify', [Object.clone(file), j, (is_a_move ? 'move' : 'copy'), this]);
var rerendering_list = false;
// remove entry from cached JSON directory list and remove the item from the view when this was a move!
// This is particularly important when working on a paginated directory and afterwards the pages are jumped back & forth:
// the next time around, this item should NOT appear in the list anymore!
if (is_a_move)
{
this.deselect(file.element);
if (this.view_fill_json)
{
this.delete_from_dircache(file); /* do NOT use j.name, as that one can be 'cleaned up' as part of the 'move' operation! */
// minor caveat: when we paginate the directory list, then browsing to the next page will skip one item (which would
// have been the first on the next page). The brute-force fix for this is to force a re-render of the page when in
// pagination view mode:
if (this.view_fill_json.dirs.length + this.view_fill_json.files.length > this.listPaginationLastSize)
{
// similar activity as load(), but without the server communication...
// abort any still running ('antiquated') fill chunks and reset the store before we set up a new one:
this.RequestQueue.cancel_bulk('fill');
clearTimeout(this.view_fill_timer);
this.view_fill_timer = null;
// was here
}
}
// -> moves this here so it always reloads the thumbnail pane in the preview window
rerendering_list = true;
this.fill(null, this.get_view_fill_startindex(), this.listPaginationLastSize);
// make sure fade does not clash with parallel directory (re)load:
if (!rerendering_list)
{
var p = file.element.getParent();
if (p) {
p.fade(0).get('tween').chain(function() {
this.element.destroy();
});
}
}
}
else
{
if (!dir)
{
// copied to the very same directory:
rerendering_list = true;
this.load(this.CurrentDir.path);
}
}
this.drop_pending = 0;
this.browserLoader.fade(0);
}).bind(this),
onError: (function(text, error) {
this.drop_pending = 0;
this.browserLoader.fade(0);
}).bind(this),
onFailure: (function(xmlHttpRequest) {
this.drop_pending = 0;
this.browserLoader.fade(0);
}).bind(this)
}, this).send();
}).bind(this)
});
this.browser_dragndrop_info.setStyle('background-position', '0px 0px');
this.browser_dragndrop_info.set('title', this.language.drag_n_drop);
}
// add tooltip for drag n drop icon
this.tips.attach(this.browser_dragndrop_info);
// check how much we've consumed so far:
duration = new Date().getTime() - starttime;
//this.diag.log(' + time taken in make draggable = ', duration);
$$(els[0].combine(els[1])).setStyles({'left': 0, 'top': 0});
// check how much we've consumed so far:
duration = new Date().getTime() - starttime;
//this.diag.log(' + time taken in setStyles = ', duration);
this.adaptive_update_pagination_size(render_count, render_count, render_count, pagesize, duration, 1.0 / 7.0, 1.02, 0.1 / 1000);
// go to the next stage, right after these messages... ;-)
this.view_fill_timer = this.fill_chunkwise_3.delay(1, this, [render_count, pagesize, support_DnD_for_this_dir, starttime, kbd_dir]);
},
/*
* See comment for fill_chunkwise_1(): the tooltips need to be assigned with each icon (2..3 per list item)
* and apparently that takes some considerable time as well for large directories and slightly slower machines.
*/
fill_chunkwise_3: function(render_count, pagesize, support_DnD_for_this_dir, starttime, kbd_dir) {
var duration = new Date().getTime() - starttime;
//this.diag.log(' + time duration @ fill_chunkwise_3() begin:', duration);
this.tips.attach(this.browser.getElements('img.browser-icon'));
this.browser_dragndrop_info.fade(1);
// check how much we've consumed so far:
duration = new Date().getTime() - starttime;
//this.diag.log(' + time taken in tips.attach = ', duration);
// when a render is completed, we have maximum knowledge, i.e. maximum prognosis power: shorter tail on the EMA is our translation of that.
this.adaptive_update_pagination_size(render_count, render_count, render_count, pagesize, duration, 1.0 / 5.0, 1.0, 0);
// we're done: erase the timer so it can be garbage collected
//this.RequestQueue.cancel_bulk('fill'); -- do NOT do this!
clearTimeout(this.view_fill_timer);
this.view_fill_timer = null;
// make sure the selection, when keyboard driven, is marked correctly
if (kbd_dir)
{
this.browserSelection(kbd_dir);
}
this.browserLoader.fade(0);
this.fireHooks('fill');
},
adaptive_update_pagination_size: function(currentindex, endindex, render_count, pagesize, duration, EMA_factor, future_fudge_factor, compensation)
{
var avgwait = this.options.listPaginationAvgWaitTime;
if (avgwait)
{
// we can now estimate how much time we'll need to process the entire list:
var orig_startindex = endindex - render_count;
var done_so_far = currentindex - orig_startindex;
// the 1.3 is a heuristic covering for chunk_2+3 activity
done_so_far /= parseFloat(render_count);
// at least 5% of the job should be done before we start using our info for estimation/extrapolation
if (done_so_far > 0.05)
{
/*
* and it turns out our fudge factors are not telling the whole story: the total number of elements
* to render are still a factor then.
*/
future_fudge_factor *= (1 + compensation * render_count);
var t_est = duration * future_fudge_factor / done_so_far;
// now take the configured _desired_ maximum average wait time and see how we should fare:
var p_est = render_count * avgwait / t_est;
// EMA + sensitivity: the closer to our current target, the better our info:
var tail = EMA_factor * (0.9 + 0.1 * done_so_far);
var newpsize = tail * p_est + (1 - tail) * pagesize;
// apply limitations: never reduce more than 50%, never increase more than 20%:
var delta = newpsize / pagesize;
if (delta < 0.5)
newpsize = 0.5 * pagesize;
else if (delta > 1.2)
newpsize = 1.2 * pagesize;
newpsize = newpsize.toInt();
// and never let it drop below rediculous values:
if (newpsize < 20)
newpsize = 20;
//this.diag.log('::auto-tune pagination: new page = ', newpsize, ' @ tail:', tail, ', p_est: ', p_est, ', psize:', pagesize, ', render:', render_count, ', done%:', done_so_far, ', delta index:', currentindex - orig_startindex, ', t_est:', t_est, ', dur:', duration, ', pdelta: ', delta);
this.options.listPaginationSize = newpsize;
}
}
},
fillInfo: function(file) {
if (!file) file = this.CurrentDir;
if (!file) return;
// set file history
this.diag.log('fillInfo: ', this.storeHistory, ', file: ', Object.clone(file));
if (typeof jsGET !== 'undefined' && this.storeHistory) {
if (file.mime !== 'text/directory')
jsGET.set({'fmFile': file.name});
else
jsGET.remove(['fmFile']);
}
var icon = file.icon;
this.switchButton4Current();
this.fireHooks('cleanupPreview');
// We need to remove our custom attributes form when the preview is hidden
this.fireEvent('hidePreview', [this]);
this.preview.empty();
//if (file.mime === 'text/directory') return;
if (this.drop_pending == 0)
{
if (this.Request) this.Request.cancel();
var dir = this.CurrentDir.path;
this.diag.log('fillInfo: request detail for file: ', Object.clone(file), ', dir: ', dir);
if(file.mime === 'text/directory')
{
if(file.readme && file.readme.length) { this.preview.set('html', file.readme); this.preview_area.setStyle('display', 'block').fade('in'); return; }
if(this.options.showDirGallery === false) return;
}
var tx_cfg = this.options.mkServerRequestURL(this, 'detail', {
directory: this.dirname(file.path),
// fixup for root directory detail requests:
file: (file.mime === 'text/directory' && file.path === '/') ? '/' : file.name,
filter: this.options.filter,
// provide either direct links to the thumbnails (when available in cache) or PHP event trigger URLs for delayed thumbnail image creation (performance optimization: faster page render):
mode: 'direct' + this.options.detailInfoMode
});
this.Request = new FileManager.Request({
url: tx_cfg.url,
data: tx_cfg.data,
onRequest: (function() {
this.previewLoader.inject(this.preview);
this.previewLoader.fade(1);
this.show_our_info_sections(false);
}).bind(this),
onSuccess: (function(j) {
if (!j || !j.status) {
this.previewLoader.dispose();
return;
}
// speed up DOM tree manipulation: detach .info from document temporarily:
this.info.dispose();
this.info_head.getElement('img').set({
src: icon,
alt: file.mime
});
this.info_head.getElement('h1').set('text', file.name);
this.info_head.getElement('h1').set('title', file.name);
// don't wait for the fade to finish to set up the new content
var prev = this.preview.removeClass('filemanager-loading').set('html', '');
if(typeof this.options.previewHandlers[j.mime] === 'function')
{
this.options.previewHandlers[j.mime](prev, j);
}
else if(typeof this.options.previewHandlers[j.mime.split('/')[0]] === 'function')
{
this.options.previewHandlers[j.mime.split('/')[0]](prev, j);
}
else
{
prev.set('html', (j.content ? j.content.substitute(this.language, /\\?\$\{([^{}]+)\}/g) : '')).getElement('img.preview');
}
if (file.mime === 'text/directory')
{
// only show the image set when this directory is also the current one (other directory detail views can result from a directory rename operation!
this.diag.log('? fillInfo for DIR: ', file, ', currentDir: ', this.CurrentDir);
if (file.path === this.CurrentDir.path)
{
this.preview.adopt(this.dir_filelist);
}
}
// and plug in the manipulated DOM subtree again:
this.info.inject(this.filemanager);
this.show_our_info_sections(true);
this.previewLoader.fade(0).get('tween').chain((function() {
this.previewLoader.dispose();
}).bind(this));
var els = this.preview.getElements('button');
if (els) {
els.addEvent('click', function(e) {
e.stop();
window.open(this.get('value'));
});
}
if (prev && !j.thumb250 && j.thumbs_deferred)
{
var iconpath = this.assetBasePath + 'Images/Icons/Large/default-error.png';
if (0)
{
prev.set('src', iconpath);
prev.setStyles({
'width': '',
'height': '',
'background': 'none'
});
}
var tx_cfg = this.options.mkServerRequestURL(this, 'detail', {
directory: this.dirname(file.path),
file: file.name,
filter: this.options.filter,
mode: 'direct' + this.options.detailInfoMode
});
var req = new FileManager.Request({
url: tx_cfg.url,
data: tx_cfg.data,
fmDisplayErrors: false, // Should we display the error here? No, we just display the general error icon instead
onRequest: function() {},
onSuccess: (function(j) {
var img_url = (j.icon48 ? j.icon48 : iconpath);
if (j && j.status && j.thumb250)
{
img_url = j.thumb250;
}
prev.set('src', img_url);
prev.addEvent('load', function() {
// when the thumb250 image has loaded, remove the loader animation in the background ...
//this.setStyle('background', 'none');
// ... AND blow away the encoded 'width' and 'height' styles: after all, the thumb250 generation MAY have failed.
// In that case, an icon is produced by the backend, but it will have different dimensions, and we don't want to
// distort THAT one, either.
this.setStyles({
'width': '',
'height': '',
'background': 'none'
});
});
// Xinha: We need to add in a form for setting the attributes of images etc,
// so we add this event and pass it the information we have about the item
// as returned by Backend/FileManager.php
this.fireEvent('details', [j, this]);
// update the stored json for this file as well:
// now mix with the previously existing 'file' info (as produced by a 'view' run):
file = Object.merge(file, j);
// remove unwanted JSON elements:
delete file.status;
delete file.error;
delete file.content;
if (file.element)
{
file.element.store('file', file);
}
if (typeof milkbox !== 'undefined')
{
milkbox.reloadPageGalleries();
}
}).bind(this),
onError: (function(text, error) {
prev.set('src', iconpath);
prev.setStyles({
'width': '',
'height': '',
'background': 'none'
});
}).bind(this),
onFailure: (function(xmlHttpRequest) {
prev.set('src', iconpath);
prev.setStyles({
'width': '',
'height': '',
'background': 'none'
});
}).bind(this)
}, this);
this.RequestQueue.addRequest('info:' + String.uniqueID(), req);
req.send();
}
else
{
// Xinha: We need to add in a form for setting the attributes of images etc,
// so we add this event and pass it the information we have about the item
// as returned by Backend/FileManager.php
this.fireEvent('details', [j, this]);
// We also want to hold onto the data so we can access it later on,
// e.g. when selecting the image.
// now mix with the previously existing 'file' info (as produced by a 'view' run):
file = Object.merge(file, j);
// remove unwanted JSON elements:
delete file.status;
delete file.error;
delete file.content;
if (file.element)
{
file.element.store('file', file);
}
if (typeof milkbox !== 'undefined')
{
milkbox.reloadPageGalleries();
}
}
}).bind(this),
onError: (function(text, error) {
this.previewLoader.dispose();
}).bind(this),
onFailure: (function(xmlHttpRequest) {
this.previewLoader.dispose();
}).bind(this)
}, this).send();
}
},
audioPreview: function(previewArea, fileDetails)
{
var dl = new Element('dl')
.adopt(new Element('dt').set('text', this.language['title']))
.adopt(new Element('dd').set('text', fileDetails.title))
.adopt(new Element('dt').set('text', this.language['artist']))
.adopt(new Element('dd').set('text', fileDetails.artist))
.adopt(new Element('dt').set('text', this.language['album']))
.adopt(new Element('dd').set('text', fileDetails.album))
.adopt(new Element('dt').set('text', this.language['length']))
.adopt(new Element('dd').set('text', fileDetails.length))
.adopt(new Element('dt').set('text', this.language['bitrate']))
.adopt(new Element('dd').set('text', fileDetails.bitrate))
.inject(previewArea);
previewArea = new Element('div', {'class': 'filemanager-preview-content'}).inject(previewArea);
var dewplayer = this.assetBasePath + '/dewplayer.swf';
new Element('object', {
type: 'application/x-shockwave-flash',
data: dewplayer,
width: 200,
height: 20,
style: 'margin-left:auto;margin-right:auto;display:block;'
})
.adopt(new Element('param', {name:'wmode', value:'transparent'}))
.adopt(new Element('param', {name:'movie', value:dewplayer}))
.adopt(new Element('param', {name:'flashvars', value:'mp3='+fileDetails.url+'&volume=50&showtime=1'}))
.inject(previewArea);
return previewArea.parentNode;
},
showFunctions: function(icon,appearOn,opacityBefore,opacityAfter) {
var opacity = [opacityBefore || 1, opacityAfter || 0];
icon.set({
opacity: opacity[1]
});
document.id(appearOn).addEvents({
mouseenter: (function() {
this.setStyle('opacity', opacity[0]);
}).bind(icon),
mouseleave: (function() {
this.setStyle('opacity', opacity[1]);
}).bind(icon)
});
return icon;
},
normalize: function(str) {
return str.replace(/\/+/g, '/');
},
dirname: function(path) {
var sects = path.split('/');
var topdir = sects.pop();
if (topdir === '')
{
// path has trailing '/'; keep it that way!
sects.pop();
sects.push('');
}
return sects.join('/');
},
switchButton4Current: function() {
var chk = !!this.Current;
var els = [];
els.push(this.menu.getElement('button.filemanager-open'));
els.push(this.menu.getElement('button.filemanager-download'));
els.each(function(el) {
if (el)
{
el.set('disabled', !chk)[(chk ? 'remove' : 'add') + 'Class']('disabled');
}
});
},
// adds buttons to the file main menu, which onClick start a method with the same name
addMenuButton: function(name) {
var el = new Element('div', {
'class': 'filemanager-button filemanager-' + name,
text: this.language[name]
}).inject(this.menu, 'top');
if (this[name+'_on_click'])
{
el.addEvent('click', this[name+'_on_click'].bind(this));
}
return el;
},
// clear the view chunk timer, erase the JSON store but do NOT reset the pagination to page 0:
// we may be reloading and we don't want to destroy the page indicator then!
reset_view_fill_store: function(j)
{
this.view_fill_startindex = 0; // offset into the view JSON array: which part of the entire view are we currently watching?
if (this.view_fill_json)
{
// make sure the old 'fill' run is aborted ASAP: clear the old files[] array to break
// the heaviest loop in fill:
this.view_fill_json.files = [];
this.view_fill_json.dirs = [];
}
this.reset_fill();
this.view_fill_json = ((j && j.status) ? j : null); // clear out the old JSON data and set up possibly new data.
// ^^^ the latest JSON array describing the entire list; used with pagination to hop through huge dirs without repeatedly
// consulting the server. The server doesn't need to know we're so slow we need pagination now! ;-)
},
// clear the view chunk timer only. We are probably redrawing the list view!
reset_fill: function()
{
this.browser_dragndrop_info.fade(0.5);
this.browser_dragndrop_info.setStyle('background-position', '0px -16px');
this.browser_dragndrop_info.set('title', this.language.drag_n_drop_disabled);
// as this is a long-running process, make sure the hourglass-equivalent is visible for the duration:
this.browserLoader.fade(1);
this.browser_paging.fade(0);
// abort any still running ('antiquated') fill chunks:
this.RequestQueue.cancel_bulk('fill');
clearTimeout(this.view_fill_timer);
this.view_fill_timer = null; // timer reference when fill() is working chunk-by-chunk.
},
store_view_fill_startindex: function(idx)
{
this.view_fill_startindex = idx;
if (typeof jsGET !== 'undefined' /* && this.storeHistory */) {
jsGET.set({'fmPageIdx': idx});
}
},
get_view_fill_startindex: function(idx)
{
// we don't care about null, undefined or 0 here: as we keep close track of the startindex, any nonzero valued setting wins out.
if (!idx)
{
idx = this.view_fill_startindex;
}
if (typeof jsGET !== 'undefined' && !idx)
{
idx = jsGET.get('fmPageIdx');
}
return parseInt(idx ? idx : 0, 10);
},
fireHooks: function(hook) {
var args = Array.slice(arguments, 1);
for(var key in this.hooks[hook]) {
this.hooks[hook][key].apply(this, args);
}
return this;
},
cvtXHRerror2msg: function(xmlHttpRequest) {
var status = xmlHttpRequest.status;
var orsc = xmlHttpRequest.onreadystatechange;
var response = (xmlHttpRequest.responseText || this.language['backend.unidentified_error']);
var text = response.substitute(this.language, /\\?\$\{([^{}]+)\}/g);
return text;
},
showError: function(text) {
var errorText = '' + text;
if (!errorText) {
errorText = this.language['backend.unidentified_error'];
}
errorText = errorText.substitute(this.language, /\\?\$\{([^{}]+)\}/g);
if (this.pending_error_dialog)
{
this.pending_error_dialog.appendMessage(errorText);
}
else
{
this.pending_error_dialog = new FileManager.Dialog(this.language.error, {
buttons: ['confirm'],
language: {
confirm: this.language.ok
},
content: [
errorText
],
zIndex: this.options.zIndex + 1000,
onOpen: this.onDialogOpen.bind(this),
onClose: function()
{
this.pending_error_dialog = null;
this.onDialogClose();
}.bind(this)
});
}
},
showMessage: function(textOrElement, title) {
if (!title) title = '';
new FileManager.Dialog(title, {
buttons: ['confirm'],
language: {
confirm: this.language.ok
},
content: [
textOrElement
],
zIndex: this.options.zIndex + 950,
onOpen: this.onDialogOpen.bind(this),
onClose: this.onDialogClose.bind(this)
});
},
onRequest: function() {
this.loader.fade(1);
},
onComplete: function() {
//this.loader.fade(0);
},
onSuccess: function() {
this.loader.fade(0);
},
onError: function() {
this.loader.fade(0);
},
onFailure: function() {
this.loader.fade(0);
},
onDialogOpen: function() {
this.dialogOpen = true;
this.onDialogOpenWhenUpload.apply(this);
},
onDialogClose: function() {
this.dialogOpen = false;
this.onDialogCloseWhenUpload.apply(this);
},
onDialogOpenWhenUpload: function() {},
onDialogCloseWhenUpload: function() {},
onDragComplete: function() {
return false; // return TRUE when the drop action is unwanted
},
// dev/diag shortcuts:
// always return a string; dump object/array/... in 'arr' to human readable string:
diag:
{
verbose: false,
dump: function(arr, level, max_depth, max_lines, no_show)
{
return '';
},
log: function(/* ... */)
{
if (!this.verbose) return;
if (typeof console !== 'undefined')
{
// WARNING: MS IE9 (+ v8?) says: this object doesn't support the 'apply' method. :-(((
// Also, MSIE 8/9 doesn't show object dumps like you'd expect; Firebug Lite allegedly fixes that,
// but this is code which intends to 'hide' all that shite, so we can simply write diag.log() and
// not bother where it will end up.
if (console.info && console.info.apply)
{
console.info.apply(console, arguments);
}
else if (console.log && console.log.apply)
{
console.log.apply(console, arguments);
}
else if (console.info || console.log)
{
// the MSIE downgrade
var l = (console.info || console.log);
var a;
var lt = '';
var m, e, v;
var multiobj = 0; // count items dumped without inter-WS
for (a in arguments)
{
multiobj++;
a = arguments[a];
switch (typeof a)
{
case 'undefined':
lt += '(undefined)';
break;
case 'null':
lt += '(null)';
break;
case 'object':
lt += '{';
m = '';
for (e in a)
{
lt += m;
v = a[e];
//if (typeof e !== 'string') continue;
switch (typeof v)
{
case 'function':
continue; // skip these
case 'undefined':
lt += e + ': (undefined)';
break;
case 'null':
lt += e + ': (null)';
break;
case 'object':
// nuts of course: IE9 has objects which turn out as === null and clunk on .toString() as a consequence >:-S
if (v === null)
{
lt += e + ': (null)';
}
else
{
lt += e + ': ' + v.toString();
}
break;
case 'string':
lt += e + ': "' + v + '"';
break;
default:
lt += e + ': ' + v.toString();
break;
}
m = ', ';
}
lt += '}';
break;
case 'string':
// reset inter-WS formatting assist:
multiobj = 0;
lt += a;
break;
default:
try
{
m = a.toString();
}
catch (e)
{
m = '(*clunk*)';
}
lt += v;
break;
}
if (multiobj >= 1)
{
lt += ' ';
}
}
}
}
}
}
});
FileManager.Request = new Class({
Extends: Request.JSON,
options:
{
secure: true, // Isn't this true by default anyway in REQUEST.JSON?
fmDisplayErrors: true // Automatically display errors - ** your onSuccess still gets called, just ignore if it's an error **
},
initialize: function(options, filebrowser) {
this.parent(options);
this.options.data = Object.merge({}, filebrowser.options.propagateData, this.options.data);
if (this.options.fmDisplayErrors)
{
this.addEvents({
success: function(j)
{
if (!j)
{
filebrowser.showError();
}
else if (!j.status)
{
filebrowser.showError(('' + j.error).substitute(filebrowser.language, /\\?\$\{([^{}]+)\}/g));
}
}.bind(this),
error: function(text, error)
{
filebrowser.showError(text);
},
failure: function(xmlHttpRequest)
{
var text = filebrowser.cvtXHRerror2msg(xmlHttpRequest);
filebrowser.showError(text);
}
});
}
this.addEvents({
request: filebrowser.onRequest.bind(filebrowser),
complete: filebrowser.onComplete.bind(filebrowser),
success: filebrowser.onSuccess.bind(filebrowser),
error: filebrowser.onError.bind(filebrowser),
failure: filebrowser.onFailure.bind(filebrowser)
});
}
});
FileManager.Language = {};
(function() {
// ->> load DEPENDENCIES
if (typeof __MFM_ASSETS_DIR__ === 'undefined')
{
var __DIR__ = (function() {
var scripts = document.getElementsByTagName('script');
var script = scripts[scripts.length - 1].src;
var host = window.location.href.replace(window.location.pathname+window.location.hash,'');
return script.substring(0, script.lastIndexOf('/')).replace(host,'') + '/';
})();
__MFM_ASSETS_DIR__ = __DIR__ + "../Assets";
}
Asset.javascript(__MFM_ASSETS_DIR__+'/js/milkbox/milkbox.js');
Asset.css(__MFM_ASSETS_DIR__+'/js/milkbox/css/milkbox.css');
Asset.css(__MFM_ASSETS_DIR__+'/Css/FileManager.css');
Asset.css(__MFM_ASSETS_DIR__+'/Css/Additions.css');
if(Browser.ie && Browser.version <= 7)
{
Asset.css(__MFM_ASSETS_DIR__+'/Css/FileManager_ie7.css');
}
if( typeof __MFM_USE_BACK_BUTTON_NAVIGATION__ != 'undefined' && __MFM_USE_BACK_BUTTON_NAVIGATION__ )
{
Asset.javascript(__MFM_ASSETS_DIR__+'/js/jsGET.js', {
events: {
load: (function() {
window.fireEvent('jsGETloaded');
}).bind(this)
}
});
}
Element.implement({
center: function(offsets) {
var scroll = document.getScroll();
var offset = document.getSize();
var size = this.getSize();
var values = {x: 'left', y: 'top'};
if (!offsets) {
offsets = {};
}
for (var z in values) {
var style = scroll[z] + (offset[z] - size[z]) / 2 + (offsets[z] || 0);
this.setStyle(values[z], (z === 'y' && style < 30) ? 30 : style);
}
return this;
}
});
FileManager.Dialog = new Class({
Implements: [Options, Events],
options: {
/*
* onShow: function() {},
* onOpen: function() {},
* onConfirm: function() {},
* onDecline: function() {},
* onClose: function() {},
*/
request: null,
buttons: ['confirm', 'decline'],
language: {},
zIndex: 2000,
autofocus_on: null // (string) suitable as a .getElement() argument or NULL for default. Example: 'button.filemanager-dialog-confirm'
},
initialize: function(text, options) {
this.setOptions(options);
this.dialogOpen = false;
this.content_el = new Element('div', {
'class': 'filemanager-dialog-content'
}).adopt([
typeOf(text) === 'string' ? this.str2el(text) : text
]);
this.el = new Element('div', {
'class': 'filemanager-dialog' + (Browser.ie ? ' filemanager-dialog-engine-trident' : '') + (Browser.ie ? ' filemanager-dialog-engine-trident' : '') + (Browser.ie8 ? '4' : '') + (Browser.ie9 ? '5' : ''),
opacity: 0,
tween: {duration: 'short'},
styles:
{
'z-index': this.options.zIndex
}
}).adopt(this.content_el);
if (typeof this.options.content !== 'undefined') {
this.options.content.each((function(content) {
if (content)
{
if (typeOf(content) !== 'string')
{
this.content_el.adopt(content);
}
else
{
this.content_el.adopt(this.str2el(content));
}
}
}).bind(this));
}
Array.each(this.options.buttons, function(v) {
new Element('button', {'class': 'filemanager-dialog-' + v, text: this.options.language[v]}).addEvent('click', (function(e) {
if (e) e.stop();
this.fireEvent(v).fireEvent('close');
//if (!this.options.hideOverlay)
this.overlay.hide();
this.destroy();
}).bind(this)).inject(this.el);
}, this);
this.overlay = new Overlay({
'class': 'filemanager-overlay filemanager-overlay-dialog',
events: {
click: this.fireEvent.pass('close', this)
},
//tween: {duration: 'short'},
styles:
{
'z-index': this.options.zIndex - 1
}
});
this.bound = {
scroll: (function() {
if (!this.el)
this.destroy();
else
this.el.center();
}).bind(this),
keyesc: (function(e) {
window.FileManager.prototype.diag.log('keyEsc: key press: ', e);
if (e.key === 'esc') {
e.stopPropagation();
this.destroy();
}
}).bind(this)
};
this.show();
},
show: function() {
this.overlay.show();
var self = this;
this.fireEvent('open');
this.el.setStyle('display', 'block').inject(document.body);
this.restrictSize();
var autofocus_el = (this.options.autofocus_on ? this.el.getElement(this.options.autofocus_on) : (this.el.getElement('button.filemanager-dialog-confirm') || this.el.getElement('button')));
if (autofocus_el)
{
// HTML5 support: see http://diveintohtml5.org/detect.html
autofocus_el.setProperty('autofocus', 'autofocus');
try{autofocus_el.focus();}catch(e) { /* IE<8 Failes */ }
}
this.el.center().fade(1).get('tween').chain((function() {
// Safari / Chrome have trouble focussing on things not yet fully rendered!
// see http://stackoverflow.com/questions/2074347/focus-not-working-in-safari-or-chrome
// and http://www.mkyong.com/javascript/focus-is-not-working-in-ie-solution/
if (autofocus_el)
{
if (0) // the delay suggested as a fix there is part of the fade()...
{
(function(el) {
el.focus();
}).delay(1, this, [autofocus_el]);
}
else
{
//autofocus_el.set('tabIndex', 0); // http://code.google.com/p/chromium/issues/detail?id=27868#c15
// ^-- not needed. When you debug JS in a Webkit browser, you're toast when it comes to getting input field focus, period. :-(
autofocus_el.focus();
}
}
}).bind(this));
self.fireEvent('show');
window.FileManager.prototype.diag.log('add key up(ESC)/resize/scroll on show 1500');
document.addEvents({
'scroll': this.bound.scroll,
'resize': this.bound.scroll,
'keyup': this.bound.keyesc
});
},
appendMessage: function(text) {
this.content_el.adopt([
typeOf(text) === 'string' ? this.str2el(text) : text
]);
this.restrictSize();
this.el.center();
},
restrictSize: function()
{
// make sure the dialog never is larger than the viewport!
var ddim = this.el.getSize();
var vdim = window.getSize();
var maxx = (vdim.x - 20) * 0.85; // heuristic: make dialog a little smaller than the screen itself and keep a place for possible outer scrollbars
if (ddim.x >= maxx)
{
this.el.setStyle('width', maxx.toInt());
}
ddim = this.el.getSize();
var cdim = this.content_el.getSize();
var maxy = (vdim.y - 20) * 0.85; // heuristic: make dialog a little less high than the screen itself and keep a place for possible outer scrollbars
if (ddim.y >= maxy)
{
// first attempt to correct this by making the dialog wider:
var x = ddim.x * 2.0;
while (x < maxx && ddim.y >= maxy)
{
this.el.setStyle('width', x.toInt());
ddim = this.el.getSize();
x = ddim.x * 1.3;
}
cdim = this.content_el.getSize();
if (ddim.y >= maxy)
{
var y = maxy - ddim.y + cdim.y;
this.content_el.setStyles({
height: y.toInt(),
overflow: 'auto'
});
}
}
},
str2el: function(text)
{
var el = new Element('div');
if (text.indexOf('<') != -1 && text.indexOf('>') != -1)
{
try
{
el.set('html', text);
}
catch(e)
{
el.set('text', text);
}
}
else
{
el.set('text', text);
}
return el;
},
destroy: function() {
if (this.el) {
this.el.fade(0).get('tween').chain((function() {
if (!this.options.hideOverlay) {
this.overlay.destroy();
}
this.el.destroy();
}).bind(this));
}
window.FileManager.prototype.diag.log('remove key up(ESC) on destroy');
document.removeEvent('scroll', this.bound.scroll).removeEvent('resize', this.bound.scroll).removeEvent('keyup', this.bound.keyesc);
this.fireEvent('close');
}
});
this.Overlay = new Class({
initialize: function(options) {
this.el = new Element('div', Object.append({
'class': 'filemanager-overlay'
}, options)).inject(document.body);
},
show: function() {
this.objects = $$('object, select, embed').filter(function(el) {
if (el.id === 'SwiffFileManagerUpload' || el.style.visibility === 'hidden') {
return false;
}
else {
el.style.visibility = 'hidden';
return true;
}
});
this.resize();
this.el.setStyle('display', 'block');
this.el.fade('hide').fade(0.5);
window.addEvent('resize', this.resize.bind(this));
return this;
},
hide: function() {
if (!Browser.ie) {
this.el.fade(0).get('tween').chain((function() {
this.revertObjects();
this.el.setStyle('display', 'none');
}).bind(this));
} else {
this.revertObjects();
this.el.setStyle('display', 'none');
}
window.removeEvent('resize', this.resize);
return this;
},
resize: function() {
if (!this.el) {
this.destroy();
}
else {
document.id(this.el).setStyles({ // IE7 thinks this.el is no longer a mootools object, ^shrug^
width: document.getScrollWidth(),
height: document.getScrollHeight()
});
}
},
destroy: function() {
this.revertObjects().el.destroy();
},
revertObjects: function() {
if (this.objects && this.objects.length) {
this.objects.each(function(el) {
el.setStyle('visibility', 'visible');
});
}
return this;
}
});
})();