/* davbrowser.js - allows one to browse a DAV-enabled server from a webpage Copyright (C) 2004 Guido Wesdorp email johnny@debris.demon.nl This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA $Id: minisax.js,v 1.5 2004/07/31 00:10:15 johnny Exp $ */ var global = this; this.davbrowser = new function davbrowser() { var davbrowser = this; //------------------------------------------------------------------------- // some helper functions //------------------------------------------------------------------------- if (!global._) { global._ = function _(s, d) { /* remove once i18n is in place */ for (var attr in d) { s = s.replace('${' + attr + '}', d[attr]); }; return s; }; }; /* this used to be in helpers.js */ this.AsyncArrayWalker = function AsyncArrayWalker( array, elhandler, elhandlercontext, endhandler, endhandlercontext, elhandlerargs, endhandlerargs) { /* a rather specific solution for a rather specific problem this allows calling function elhandler with each element of array in context elhandlercontext, and calls endhandlercontext when done elhandler will be called with args (array[currentid], continuehandler, elhandlerargs[0], elhandlerargs[1], ...) endhandler will be called with args (resultarray, endhandlerargs[0], endhandlerargs[1], ...) */ var currentid = 0; var resultarray = []; var self = this; var execute = this.execute = function() { var continuehandler = function(el) { resultarray.push(el); execute.call(self); }; if (currentid >= array.length) { var args = [resultarray]; args = args.concat(endhandlerargs); endhandler.apply(endhandlercontext, args); return; }; var el = array[currentid]; var args = [el, continuehandler]; args = args.concat(elhandlerargs); elhandler.apply(elhandlercontext, args); currentid++; }; }; function sort_items(a, b) { aisdir = a.id.charAt(a.id.length - 1) == '/'; bisdir = b.id.charAt(b.id.length - 1) == '/'; if (aisdir && !bisdir) { return -1; } else if (bisdir && !aisdir) { return 1; } else { return (a.id > b.id) ? 1 : -1; }; }; var BrowserRoot = this.BrowserRoot = function BrowserRoot( doc, rootElement, path, fs, icons) { if (doc) { this._initialize(doc, rootElement, path, fs, icons); }; }; BrowserRoot.prototype = new tree.Root; BrowserRoot.prototype._initialize = function( doc, rootElement, path, fs, icons) { tree.Root.prototype._initialize.call(this, doc, rootElement, icons['closed-collection'], icons['opened-collection'], icons['resource']); if (path.charAt(0) == '/') { path = path.substr(1); }; this.path = path; this.fs = fs; this.icons = icons; this.selected = null; // will be overwritten later this.extensions = []; // some special attributes to function as sort of a handler-queue // when getItems is called while another 'thread' is waiting // for its results, the handler for the new call will be stored // and called when the first call is done (to allow caching) // XXX note that this is not safe, JS lacks locking so it can // never be entirely water-tight :( this._working = {}; // key path, value boolean this._waiting_handlers = {}; // key path, value handler //this._preloadIcons(); }; BrowserRoot.prototype.getItems = function(item, handler, reload) { /* generate a list of subitems items to display, when done call handler this is probably the most godawful case of callback hell I've ever encountered, up to the point where a list of items is traversed with async callbacks calling each other when they're done (see the graceful AsyncArrayWalker solution below, for more info see helpers.js), the reason for this mess is that we want to have the items in an as complete state as possible (so including properties and filetype). */ if (item.root == item) { // here's where the root gets filled var davroot = new BrowserNode(item.path, item); davroot.init(); function handlePropsCall(error, content) { if (error) { alert(_('Error loading properties: ' + error)); }; davroot.properties = content; davroot.filetype = this.getFileType(item); davroot.isdavroot = true; this.davroot = davroot; // optional extension method 'rootItems' to allow adding items // from outside var items = [davroot]; if (this.extendRootItems) { var extensions = this.extendRootItems(); items = items.concat(extensions); this.extensions = extensions; }; this._continueOpen(items); }; this.fs.getProps(davroot.getPath(), handlePropsCall, this); return; }; // this bit is necessary to prevent simultaneous calls to fs.listDir() var path = item.getPath(); if (!reload && this._working[path]) { // store the handler until the working call is done, then we can // call it with cached items if (!this._waiting_handlers[path]) { this._waiting_handlers[path] = []; }; this._waiting_handlers[path].push(handler); return; }; this._working[path] = true; var browserroot = this; function HandlerWrapper(handler, orghandler, orgcontext) { this.execute = function(error, response) { handler.call(browserroot, error, response, orghandler, orgcontext); }; }; var wrapper = new HandlerWrapper(this._handleListDirResponse, handler, item); this.fs.listDir(path, wrapper.execute, wrapper, !reload); }; BrowserRoot.prototype._handleListDirResponse = function(error, response, handler, item) { if (error) { alert(_('Error loading directory listing: ${error}', {'error': error})); this.doc.getElementsByTagName('body')[0].style.cursor = 'default'; return; }; var items = []; var starttime = new Date().getTime(); for (var i=0; i < response.length; i++) { var itempath = response[i]; var chunks = itempath.split('/'); var itemid = string.urldecode(chunks[chunks.length - 1]); if (itemid == '') { itemid = chunks[chunks.length - 2] + '/'; }; var child = new BrowserNode(itemid, item); child.init(); items.push(child); }; items = items.sort(sort_items); // now we want to walk through all items, do a listProps and set // certain properties according to those, so we grab an // AsyncArrayWalker and hope for the best... var browserroot = this; function ListPropsHandlerWrapper(item, handler, handlercontext, items) { var self = this; this.execute = function(curritem, continuehandler) { // the handler here is DavFS.getProps function getPropsCallback(error, content) { if (error) { curritem.properties = null; curritem.filetype = 'application/unknown'; } else { curritem.properties = content; curritem.filetype = browserroot.getFileType(curritem); }; continuehandler(curritem); }; handler.call(handlercontext, curritem.getPath(), getPropsCallback, self, true); }; }; var listpropshandlerwrapper = new ListPropsHandlerWrapper( item, this.fs.getProps, this.fs, items); var aw = new davbrowser.AsyncArrayWalker( items, listpropshandlerwrapper.execute, listpropshandlerwrapper, this._handleArrayWalkerResponse, this, [], [handler, item]); aw.execute(); }; BrowserRoot.prototype._handleArrayWalkerResponse = function(items, handler, item) { handler.call(item, items); var path = item.getPath(); while (this._waiting_handlers[path] && this._waiting_handlers[path].length) { var waiting = this._waiting_handlers[path].pop(); waiting.call(item, items); }; this._working[path] = false; }; BrowserRoot.prototype.getPath = function() { return ''; }; BrowserRoot.prototype.getItemByPath = function(path) { /* return an item by its path */ if (path.charAt(0) == '/') { path = path.substr(1); }; if (path == '') { return this; }; var root = this.davroot; // path = path.substr(rootid.length); /* if (path == '' || path == '/') { return root; }; */ var chunks = path.split('/'); if (chunks[chunks.length - 1] == '') { chunks.pop(); }; if (chunks[0] == root.id) { if (chunks.length == 1) { return root; }; chunks.shift(); }; if (chunks.length == 1) { for (var i=0; i < this.extensions.length; i++) { if (this.extensions[i].id == path) { return this.extensions[i]; }; }; }; return root.getItemByPath(path); }; BrowserRoot.prototype.getIcon = function(item) { /* return an img object that represents the file type */ var img = this.doc.createElement('img'); var src = item.icon_src; if (!src) { if (item.root == item) { src = this.icons['browser']; } else { src = this.getIconSource(item); }; }; img.setAttribute('src', src); return img; }; BrowserRoot.prototype.getIconSource = function(item) { var src = null; if (this.icons[item.filetype]) { src = this.icons[item.filetype]; } else { var chunks = item.filetype.split('/'); src = this.icons[chunks[0] + '/*']; if (!src) { src = this.icons['*']; }; }; return src; }; BrowserRoot.prototype.getFileType = function(item) { var filename = item.id; var filetype = null; if (item == item.root) { filetype = 'directory'; } else if (filename.charAt(filename.length - 1) == '/') { filetype = 'directory'; } else { if (item.properties) { if (!item.properties['DAV:'] || !item.properties['DAV:']['getcontenttype']) { filetype = 'application/unknown'; } else { filetype = item.properties['DAV:']['getcontenttype']. documentElement.childNodes[0].toXML(); }; } else { filetype = 'application/unknown'; }; }; return filetype; }; BrowserRoot.prototype._preloadIcons = function() { var loaded = []; var body = this.doc.getElementsByTagName('body')[0]; for (var filetype in this.icons) { var path = this.icons[filetype]; var img = new Image(); img.src = path; }; }; function BrowserNode(id, parent) { if (id || parent) { this._initialize(id, parent); }; }; BrowserNode.prototype = new tree.Node; BrowserNode.prototype._initialize = function(id, parent) { tree.Node.prototype._initialize(id, parent); this.id = id; this.parent = parent; this.items = {}; this.isdavroot = false; }; BrowserNode.prototype.isCollection = function() { var iscol = (this.id.substr(this.id.length - 1) == '/'); return iscol; }; BrowserNode.prototype.reload = function() { this.itemids = null; this.properties = null; if (!this.isCollection() || this.opened) { this.close(); this.open(true); }; }; BrowserNode.prototype.reloadSingle = function() { /* reload the properties of a non-collection */ var item = this; var handler = function(error, content) { if (error) { alert(_('Error loading properties: ${error}', {'error': error})); return; }; item.properties = content; }; this.root.fs.getProps(this.getPath(), handler); }; }();