<file-tree>, the file tree element
This is an HTML custom element for adding file tree visualisation and interaction to your page.
Simply add the element .js and .css files to your page using plain HTML:
<script src="somewhere/file-tree.esm.js" type="module" async></script>
<link rel="stylesheet" href="somewhere/file-tree.css" async />
And then you can work with any <file-tree>
like you would any other HTML element. For example:
const fileTree = document.querySelector(`file-tree`);
fileTree.setFiles([
`README.md`,
`dist/client.bundle.js`,
`src/server/index.js`,
`LICENSE.md`,
`src/client/index.js`,
`src/server/middleware.js`,
`package.json`,
`dist/client.bundle.min.js`,
]);
After which users can play with the file tree as much as they like: all operations generate "permission-seeking" events, which need to be explicitly granted before the filetree will let them happen, meaning that you have code like:
filetree.addEventListener(`file:rename`, async ({ detail }) => {
const { oldPath, newPath, grant } = detail;
const result = await api.renameFile(oldPath, newPath);
if (result.error) {
warnUser(`An error occurred trying to rename ${oldPath} to ${newPath}.`);
} else if (result.denied) {
warnUser(`You do not have permission to rename files.`);
} else {
grant();
}
});
Thus ensuring that the file tree stays in sync with your real filesystem (whether that's through an api as in the example, or a client-side )
Demo
There is a live demo that shows off the above, with event handling set up to blanket-allow every action a user can take.
Touch support
Part of the functionality for this element is based on the HTML5 drag-and-drop API (for parts of the file tree itself, as well as dragging files and folders into it from your device), which is notoriously based on "mouse events" rather than "pointer events", meaning there is no touch support out of the box.
However, touch support can be trivially achieved using the following shim, which has its own repository over on https://github.com/pomax/dragdroptouch (which is a fork of https://github.com/Bernardo-Castilho/dragdroptouch, rewritten as a modern ESM with support for autoloading)
<script
src="https://pomax.github.io/dragdroptouch/dist/drag-drop-touch.esm.min.js?autoload"
type="module"
></script>
Load this as first thing on your page, and done: drag-and-drop using touch will now work.
File tree events
As mentioned above, events are "permission seeking", meaning that they are dispatched before an action is allowed to take place. Your event listener code is responsible for deciding whether or not that action is allowed to take place given the full context of who's performing it on which file/directory.
If an event is not allowed to happen, your code can simply exit the event handler. The file-tree will remain as it was before the user tried to manipulate it.
If an event is allowed to happen, your code must call event.detail.grant()
, which lets the file tree perform the associated action.
Events relating to files:
Events are listed here as name → detail object content
, with the grant
function omitted from the detail object, as by definition all events come with a grant function.
file:click
→ {path}
,
Dispatched when a file entry is clicked, with path
representing the full path of the file in question.
Granting this action will assign the selected
class to the associated file entry.file:create
→ {path}
,
Dispatched when a new file is created by name, without content, with path
being the file's full path.
Granting this action will create a new file entry, nested according to the path
value.file:rename
→ {oldPath, newPath}
,
Dispatched when an existing file is renamed by the user, with oldPath
being the current file path, and newPath
the desired new path.
Granting this action will change the file entry's label and path values.
Note: file renames are (currently) restricted to file names only, as renames that include directory prefixes (including ../
) should be effected by just moving the file to the correct directory.file:upload
→ {path, content}
,
Dispatched when a new file is created by drag-and-drop, with path
being the file's full path, and content
being the file's content as ArrayBuffer.
Granting this action will create a new file entry, nested according to the path
value.
Note: there is no mechanism for retrieving the file's content later, file content is discarded upon event resolution.file:move
→ {oldPath, newPath}
,
Dispatched when a file gets moved to a different directory, with oldPath
being the current file path, and newPath
the desired new path.
Granting this action will move the file entry from its current location to the location indicated by newPath
.file:delete
→ {path}
,
Dispatched when a file gets deleted, with path
representing the full path of the file in question.
Granting this action will remove the file entry from the tree.
Note: if this is the only file in a directory, and the <file-tree>
specifies the remove-empty
attribute, the now empty directory will also be deleted, gated by a dir:delete
permission event, but not gated by a confirm()
dialog to the user.
Events relating to directories:
dir:click
→ {path, currentState}
,
Dispatched when a directory entry is clicked, with path
representing the full path of the directory in question, and currentState
reflecting whether this directory is currently visualized as "open"
or "closed"
, determined by whether or not its class list includes the closed
class.
Granting this action will toggle the closed
class on the associated directory entry.dir:create
→ {path}
,
Dispatched when a directory gets created, with path
being the directory's full path.
Granting this action will create a new directory entry, nested according to the path
value.dir:rename
→ {oldPath, newPath}
,
Dispatched when an existing directory is renamed by the user, with oldPath
being the current directory path, and newPath
the desired new path.
Granting this action will change the directory entry's label and path values.
Note: directory nesting cannot (currently) be effected by renaming, and should instead be effected by just moving the directory into or out of another directory.dir:move
→ {oldPath, newPath}
,
Dispatched when a directory gets moved to a different parent directory, with oldPath
being the current directory path, and newPath
the desired new path.
Granting this action will move the directory entry from its current location to the location indicated by newPath
.dir:delete
→ {path}
,
Dispatched when a directory gets deleted, with path
representing the full path of the directory in question.
Granting this action will remove the directory entry (including its associated content) from the tree.
Note: this action is gated behind a confirm()
dialog for the user.
Special attributes
File tree tags may also specify a "remove-empty" attribute, i.e.
<file-tree remove-empty></file-tree>
Setting this attribute tells the file tree that it may delete directories that become empty due to file move/delete operations.
Warning: file tree element are not persistent
File tree elements are not persistent, and may be removed and rebuilt from scratch depending on user actions. This means that if you set a class on a file or directory entry, that class may seem to "get unset" or "disappear" when in reality the entire file or directory entry got deleted and a new one was generated in its place.
Contributing
— Pomax