Pomax / custom-file-tree

An HTML <file-tree> custom element
10 stars 0 forks source link

<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="https://github.com/Pomax/custom-file-tree/raw/main/somewhere/file-tree.esm.js" type="module" async></script>
<link rel="stylesheet" href="https://github.com/Pomax/custom-file-tree/blob/main/somewhere/file-tree.css" async />

And then you can work with any <file-tree> like you would any other HTML element. For example, if you like working in HTML and you want to bootstrap your file-tree off of an API endpoint:

<file-tree src="https://github.com/Pomax/custom-file-tree/raw/main/api/v1/dir-listing"></file-tree>

or if you prefer to work on the JS side, things are pretty much as expected:

// query select, or really any normal way to get an element handle:
const fileTree = document.querySelector(`file-tree`);

// Bootstrap off of an endpoint:
fileTree.setAttribute(`src`, `./api/v1/dir-listing`);

// Or tell the file tree which files and directories exist directly:
fileTree.setContent([
  `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;
  // we'll have the API determine whether this operation is allowed or not:
  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 added by loading the drag-drop-touch polyfill found over on https://github.com/drag-drop-touch-js/dragdroptouch:

<script src="https://github.com/Pomax/custom-file-tree/raw/main/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.

The <file-tree> API

Functions

There are three functions supported by <file-tree>:

Attributes

The src attribute

Like <image> or <script>, the <file-tree> tag supports the src attribute for specifying a URL from which to load content. This content must be JSON data representing an array of strings, with each string representing a file or directory path.

<file-tree src="https://github.com/Pomax/custom-file-tree/raw/main/api/v1/get-dir"></file-tree>

The remove-empty attribute

Additionally, file trees may specify a remove-empty attribute, i.e.

<file-tree remove-empty="true"></file-tree>

Setting this attribute tells the file tree that it may delete directories that become empty due to file move/delete operations.

By default, file trees content "normally", even though under the hood all content is wrapped by a directory entry with path "." to act as a root.

The show-top-level attribute

Finally, file trees specify a show-top-level attribute to show this root directory, i.e.

<file-tree show-top-level="true"></file-tree>

File and directory elements have a persistent state

If you wish to associate data with <file-entry> and <dir-entry> elements, you can do so by adding data to their .state property either directly, or by using the .setState(update) function, which takes an update object and applies all key:value pairs in the update to the element's state.

const readme = fileTree.querySelector(`[path="README.md"]`);

// This works
readme.state.content = `...some file content...`;
readme.state.hash = `...`;
readme.state.timestamp = Date.now();

// As does this
readme.setState({
  content: `...`,
  hash: `...`,
  timestamp: Date.now(),
});

It should go without saying, but: this is an HTML element and state bindings are immediate.

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

Events are listed here as name → detail object content. Note that unlike regular file and directory events, these events do not come with a grant() function, and are informative, not permission-seeking (technically, they come with a no-op grant() function; running it will have no effect).

Events relating to files:

Events are listed here as name → detail object content, with the grant() function omitted from the detail object in the following documentation. All file events come with a grant function.

Error events

The following events will be emitted when certain errors occur. All errors have an event detail object that is the same as for the non-error event, with an additional error property that has a string value reflecting what went wrong.

Events relating to directories:

Events are listed here as name → detail object content, with the grant() function omitted from the detail object in the following documentation. All directory events come with a grant function.

Error events

The following events will be emitted when certain errors occur. All errors have an event detail object that is the same as for the non-error event, with an additional error property that has a string value reflecting what went wrong.

Customizing the styling

If you don't like the default styling, just override it! This custom element uses normal CSS, so you're under no obligation to load the file-tree.css file, either load it and then override the parts you want to customize, or don't even load file-tree.css at all and come up with your own styling.

That said, there are a number of CSS variables that you can override on the file-tree selector if you just want to tweak things a little, with their current definitions being:

file-tree {
  --fallback-icon: "🌲";
  --open-dir-icon: "📒";
  --closed-dir-icon: "📕";
  --file-icon: "📄";

  --dir-touch-padding: 0;
  --open-dir-icon-cursor: pointer;
  --closed-dir-icon-cursor: pointer;
  --dir-heading-cursor: pointer;
  --file-icon-cursor: pointer;
  --file-heading-cursor: pointer;

  --icon-size: 1.25em;
  --line-height: 1.5em;
  --indent: 1em;
  --entry-padding: 0.25em;

  --highlight-background: lightcyan;
  --highlight-border-color: blue;
  --drop-target-color: rgb(205, 255, 242);
}

For example, if you just want to customize the icons and colors, load the file-tree.css and then load your own overrides that set new values for those CSS variables. Nice and simple!

Contributing

— Pomax