Home

Awesome

Folder and File Explorer

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.

Screenshot of CubicleSoft File Explorer

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

Donate Discord

If you use this project, don't forget to donate to support its development!

Features

Getting Started

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):

A loading screen

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.

FileExplorer Options

The options object passed to the FileExplorer class accepts the following options:

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.

Making Custom Tools

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.

Class Documentation

Known Limitations

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.