A zero dependencies, customizable, pure Javascript widget for navigating, managing, uploading, and downloading files and folders or other hierarchical object structures on any modern web browser.
A zero dependencies, customizable, pure Javascript widget for navigating, managing (move, copy, delete), uploading, and downloading files and folders or other hierarchical object structures on any modern web browser. Choose from a MIT or LGPL license.
Live Demo | The making of this widget
Experience a clean, elegant presentation of folders and files in a mobile-friendly layout that looks and feels great on all devices. CubicleSoft File Explorer is easily connected to any web application that needs to manage hierarchical objects (folders and files, database records, or even JSON, XML, etc).
Check out a working product that uses this widget: PHP File Manager and Editor
If you use this project, don't forget to donate to support its development!
To use this widget, you should be quite comfortable with writing both client-side Javascript and secure server-side code. Client-side Javascript is never a proper defense mechanism for proper server-side security and assume someone will try to break into your server by sending bad folder and file paths to the server to read/write data they shouldn't have access to.
Also note that this widget only provides the client-side (web browser) portion of the equation. You will need to supply your own server-side handlers (e.g. PHP) but that's the comparatively easy part.
With those caveats out of the way, download/clone this repo, put the file-explorer
directory on a server, and add these lines to your page to load the widget core:
<link rel="stylesheet" type="text/css" href="file-explorer/file-explorer.css">
<script type="text/javascript" src="file-explorer/file-explorer.js"></script>
Next, create an instance of the FileExplorer class inside a closure for security reasons:
<div id="filemanager" style="height: 50vh; max-height: 400px; position: relative;"></div>
<script type="text/javascript">
(function() {
var elem = document.getElementById('filemanager');
var options = {
initpath: [
[ '', 'Projects (/)', { canmodify: false } ]
],
onrefresh: function(folder, required) {
// Optional: Ignore non-required refresh requests. By default, folders are refreshed every 5 minutes so the widget has up-to-date information.
// if (!required) return;
// Maybe notify a connected WebSocket here to watch the folder on the server for changes.
if (folder === this.GetCurrentFolder())
{
}
// Make a call to your server here to get some entries to diplay.
// this.PrepareXHR(options) could be useful for doing that. Example:
var $this = this;
var xhr = new this.PrepareXHR({
url: '/yourapp/',
params: {
action: 'file_explorer_refresh',
path: JSON.stringify(folder.GetPathIDs()),
xsrftoken: 'asdfasdf'
},
onsuccess: function(e) {
var data = JSON.parse(e.target.response);
console.log(data);
if (data.success) folder.SetEntries(data.entries);
else if (required) $this.SetNamedStatusBarText('folder', $this.EscapeHTML('Failed to load folder. ' + data.error));
},
onerror: function(e) {
// Maybe output a nice message if the request fails for some reason.
// if (required) $this.SetNamedStatusBarText('folder', 'Failed to load folder. Server error.');
console.log(e);
}
});
xhr.Send();
},
// This will be covered in a moment...
// onrename: function(renamed, folder, entry, newname) {
// },
};
var fe = new window.FileExplorer(elem, options);
})();
</script>
That code will produce a less-than-exciting 'Loading...' view and also doesn't show the toolbar since there are no event listeners for the tools (yet):
The next step is to connect the widget to the backend server. There are many ways to do that. The widget instance itself exports the PrepareXHR class to make AJAX calls. Or you could use a framework specific mechanism of your choice (jQuery, Vue, whatever) or talk to a WebSocket server or whatever makes sense for your application. The entries returned from the server should ideally be compatible with Folder.SetEntries.
If using PHP on a server and this widget will reflect a physical file system on the same server, there is the useful FileExplorerFSHelper PHP class that can simplify connecting the widget to the backend server:
<?php
require_once "server-side-helpers/file_explorer_fs_helper.php";
$options = array(
"base_url" => "https://yoursite.com/yourapp/files/",
"protect_depth" => 1, // Protects base_dir + additional directory depth.
"recycle_to" => "Recycle Bin",
"temp_dir" => "/tmp",
"dot_folders" => false, // Set to true to allow things like: .git, .svn, .DS_Store
"allowed_exts" => ".jpg, .jpeg, .png, .gif, .svg, .txt",
"allow_empty_ext" => true,
"thumbs_dir" => "/var/www/yourapp/thumbs",
"thumbs_url" => "https://yoursite.com/yourapp/thumbs/",
"thumb_create_url" => "https://yoursite.com/yourapp/?action=file_explorer_thumbnail&xsrftoken=qwerasdf",
"refresh" => true,
"rename" => true,
"file_info" => false,
"load_file" => false,
"save_file" => false,
"new_folder" => true,
"new_file" => ".txt",
"upload" => true,
"upload_limit" => 20000000, // -1 for unlimited or an integer
"download" => true,
"copy" => true,
"move" => true,
"delete" => true
);
FileExplorerFSHelper::HandleActions("action", "file_explorer_", "/var/www/yourapp/files", $options);
Once entries are populating in the widget, various bits of navigation functionality should start working. The widget is starting to come to life but is mostly read only. Let's add an onrename
handler so users can press F2 or click on the text of a selected item to rename the item:
// Note: 'entry' is a copy of the original, so it is okay to modify any aspect of it, including 'id'.
onrename: function(renamed, folder, entry, newname) {
var xhr = new this.PrepareXHR({
url: '/yourapp/',
params: {
action: 'file_explorer_rename',
path: JSON.stringify(folder.GetPathIDs()),
id: entry.id,
newname: newname,
xsrftoken: 'asdfasdf'
},
onsuccess: function(e) {
var data = JSON.parse(e.target.response);
console.log(data);
// Updating the existing entry or passing in a completely new entry to the renamed() callback are okay.
if (data.success) renamed(data.entry);
else renamed(data.error);
},
onerror: function(e) {
console.log(e);
renamed('Server/network error.');
}
});
xhr.Send();
},
A lot is going on here. When the user finishes renaming an item, onrename
is called, which hands the request off to an AJAX request to the server to handle. During this operation, the textarea is marked read only and the busy state on the folder is enabled. When the AJAX operation completes, it must call renamed()
to let the widget know that the operation has been completed and indicate success by passing in a compatible entry object - either a modified entry
or a new entry from the server. On failure, a boolean of false or a string that is passed to renamed() to be used as part of the error message displayed to the user.
The above examples and documentation should be enough to get the ball rolling. Most event handler callbacks utilize a similar approach: Receive an event callback, make a server call or two, and finally call the completion callback function with the result of the operation. Callbacks always have the 'this' context as the FileExplorer instance.
See a complete, functional implementation of all of the important callbacks in the FileExplorerFSHelper PHP class documentation. It's an excellent starting point when utilizing the FileExplorerFSHelper class.
The options
object passed to the FileExplorer class accepts the following options:
[id, value, attrs]
. This path is passed to FileExplorer.SetPath.Destroy()
the widget instance if it is used in a popup overlay. Note that onblur
will not fire if the window itself loses focus - only if the widget loses focus to another element on the page.
The Live Demo utilizes nearly all of the available callbacks. The Live Demo source code was designed so as keep this documentation to a minimum and to provide decent example usage without incurring AJAX calls.
While most of the widget is not really intended to be modified externally, the toolbar is designed to be extensible. Building a new tool involves:
The simplest approach to developing a new tool is to look at the existing tools. However, the Delete tool is fairly simple and short:
(function() {
// Tools receive the FileExplorer instance as the only option passed to the tool.
var FileExplorerTool_Delete = function(fe) {
if (!(this instanceof FileExplorerTool_Delete)) return new FileExplorerTool_Delete(fe);
// Do not create the tool if deleting is disabled.
if (!fe.hasEventListener('delete') && !fe.settings.tools.delete) return;
var enabled = false;
// Register a toolbar button with File Explorer.
var node = fe.AddToolbarButton('fe_fileexplorer_folder_tool_delete', fe.Translate('Delete (Del)'));
// Handle clicks.
var ClickHandler = function(e) {
if (e.isTrusted && enabled) fe.DeleteSelectedItems(!e.shiftKey);
};
node.addEventListener('click', ClickHandler);
// Efficiently handle toolbar updates - only adding/removing the disabled class if it is different from its previous state.
var UpdateToolHandler = function(currfolder, attrs) {
var prevenabled = enabled;
enabled = (!currfolder.waiting && (!('canmodify' in attrs) || attrs.canmodify) && fe.GetNumSelectedItems());
if (prevenabled !== enabled)
{
if (enabled) node.classList.remove('fe_fileexplorer_disabled');
else node.classList.add('fe_fileexplorer_disabled');
// Notify File Explorer that the state was updated.
// When the event callback finishes, it will know that there is some work to do.
fe.ToolStateUpdated();
}
};
fe.addEventListener('update_tool', UpdateToolHandler);
// Cleanly handle the destroy event.
var DestroyToolHandler = function() {
node.removeEventListener('click', ClickHandler);
};
fe.addEventListener('destroy', DestroyToolHandler);
};
// Register the tool in the second group of tools (0-based).
window.FileExplorer.RegisterTool(1, FileExplorerTool_Delete);
})();
Some additional comments were added to the code above to aid in understanding what is going on. There are more complex tools to look at in the FileExplorer source code (e.g. FileExplorerTool_Download). The Class Documentation section below will be quite useful when developing a custom tool.
For custom tools, you might want to prefix custom settings object keys with something like a company abbreviation so the likelihood of a naming conflict is reduced.
One good idea for a custom tool might be a HTML embed tool. If a user selects a single item and clicks the embed tool, the clipboard receives some HTML code that can be pasted into another website to embed the item into a post.
window.FileExplorer
.window.FileExplorer.ImageLoader
for reusability purposes.window.FileExplorer.PopupMenu
for reusability purposes.window.FileExplorer.TextareaOverlay
for reusability purposes.CubicleSoft File Explorer is a complex piece of software written in Javascript. As with every complex piece of software written in Javascript, there are going to be problems with certain combinations of OS + device + web browser for a garden variety of reasons. Web browsers have lots of bugs and the specifications that browsers follow don't cover all edge cases, which leaves things open to interpretation for the browser vendor. What follows are known limitations of the widget due to conditions beyond nearly everyone's direct control and most of the listed problems have reasonable workarounds. Please do not open issues on the issue tracker for these items unless you actually solve them.
Some devices and web browsers lack HTML 5 drag-and-drop support, notably any browser on iOS as the iOS webview implementation lacks support. It is NOT recommended to use a polyfill with this widget to support HTML 5 drag-and-drop. There are several situations where a drag-and-drop polyfill will probably break certain touch-based features of this widget and/or break native drag-and-drop elsewhere. Instead, use the cut/copy/paste feature on devices/browsers where HTML 5 drag-and-drop is not natively supported.
The right-click context menu for cut/copy/paste can cause the main UI area to get stuck on some platforms until a click or keyboard event is registered, notably Mac OSX. There is a near-invisible textarea overlay that captures right-click events for cut/copy/paste operations but it doesn't always go away until a second mousedown/keyup event is registered. This happens because there is no such thing as an 'exitcontextmenu' event to detect that the context menu has closed and all known methods for detection are hacks. The solution is to either put up with the extra click or just not use right-click and use keyboard shortcuts or the toolbar buttons instead.
There is extremely limited "file paste" support from OS file systems into the widget. Raw image data that is stored on the clipboard (e.g. right-click on an image in a website then click Copy) can be pasted into the widget and it will be uploaded but files from the OS itself do not drop. Here's a Chromium bug that's been open since 2013 about the issue and has received almost no attention other than duplicates being merged into it. Dragging and dropping files from the OS into the browser is the most feature-complete file transfer mechanism available to date.
The iframe-based downloading method requires initiating a second request to the server for the same content. This kind of hack has to be done because there is no web browser event available to know that page navigation was cancelled when the download starts. The XHR request terminates as soon as it receives its first 'progress' event, usually in the first few KB and it assumes the iframe form submission equivalent, which started before the XHR request, completed in a similar amount of time and has started downloading the file. It is recommended that download requests that take more than a few seconds to process on the server (e.g. generating a compressed file) be run separately and the generated file stored on the server somewhere prior to calling startdownload(fileinfo)
in the client so that both the form and XHR requests return quickly, the download begins, and the iframe is removed all within a few seconds.
The hardware back/forward mouse button capture logic modifies browser history. There is no way to capture the hardware mouse buttons except to modify browser history so that there are three distinct history push states (back, current, forward) while hovering over the widget and using mouseenter/mouseleave to enable/disable the back/forward button capture. There are a couple of extremely difficult to track down bugs as well. The first bug is that the window.history.scrollRestoration
of the real page sometimes gets stuck on manual
instead of being restored to auto
, which causes page reload jumping issues and also causes Edge to jump during scrolling. That bug only seems to show up if lots of reloads happen. The second bug is the page the widget is on occasionally completely messes up the parent pushState and the widget decides it needs to back up too far in the history stack and ends up leaving the page altogether. I have no idea what causes either bug to occur. Also, during development of this specific feature, Firefox literally crashed a half-dozen times. Since browser history is modified, which may conflict with other software, the hardware back/forward mouse button capture feature is disabled by default. It is a pretty cool feature when it works.