@itihon/file-tree-view
Version:
File tree view component based on Web Components API
319 lines (318 loc) • 10.7 kB
JavaScript
(function(){"use strict";try{if(typeof document<"u"){var e=document.createElement("style");e.appendChild(document.createTextNode('file-tree-view{display:block;width:220px;height:fit-content;max-height:100%;overflow:auto;background-color:var(--background-color);font-family:Helvetica;font-size:14px;--left-indent: 20px;--line-height: 18px;--default-file-icon: "📄";--default-folder-icon: "📁";--default-expanded-folder-icon: "📂";--selected-color: gainsboro;--focus-color: #cacaca;--background-color: darkgray;--hover-color: silver}ftv-file,ftv-folder{display:block;min-width:100%;width:max-content}ftv-file:focus-visible>.label,ftv-folder:focus-visible>.label,ftv-file:focus>.label,ftv-folder:focus>.label{background-color:var(--focus-color)}ftv-file:focus-visible,ftv-folder:focus-visible,ftv-file:focus,ftv-folder:focus{outline:none}ftv-file[selected]>.label,ftv-folder[selected]>.label{background-color:var(--selected-color)}.label:hover{background-color:var(--hover-color)}.label{display:block;min-width:100%;width:fit-content;height:var(--line-height);cursor:pointer}.label:before{display:inline-block;width:var(--line-height);height:var(--line-height)}.content{display:block;overflow:hidden;height:0;padding-left:var(--left-indent)}ftv-file>.label:before{content:var(--default-file-icon)}ftv-folder>.label:before{content:var(--default-folder-icon)}ftv-folder[expanded]>.label:before{content:var(--default-expanded-folder-icon)}ftv-folder[expanded]>.content{height:fit-content}.noselect{-webkit-touch-callout:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}')),document.head.appendChild(e)}}catch(o){console.error("vite-plugin-css-injected-by-js",o)}})();
class l extends HTMLElement {
getSelfRef() {
return this;
}
getParentRef() {
return this.parentNode;
}
treeViewContainer = null;
getNode;
getTreeViewContainer() {
return this.treeViewContainer;
}
connectedCallback() {
let e = this;
for (; e && e.tagName.toLowerCase() !== "file-tree-view"; )
e = e.parentElement;
e instanceof l || (this.treeViewContainer = e);
}
disconnectedCallback() {
this.treeViewContainer = null;
}
constructor(e) {
super(), e ? this.getNode = this.getSelfRef : this.getNode = this.getParentRef;
}
}
customElements.define("ftv-auxiliary", l);
class u extends l {
expandable = !1;
label;
path = "";
initPath() {
let e = this, t = "";
for (; e instanceof u; )
t = "/" + e.getName() + t, e = e.getContainingFolder();
return t;
}
constructor(e, t = !1) {
super(!0);
const s = e || this.getAttribute("name") || "";
this.label = new l(!1), this.label.classList.add("label", "noselect"), this.insertAdjacentElement("afterbegin", this.label), this.setName(s), this.setAttribute("name", s), this.tabIndex = -1, this.expandable = t;
}
connectedCallback() {
super.connectedCallback(), this.path = this.initPath();
}
getContainingFolder() {
const e = this.parentElement;
return e instanceof l ? e.getNode() : null;
}
getName() {
return this.label.textContent;
}
isSelected() {
return this.hasAttribute("selected");
}
toggleSelected() {
this.toggleAttribute("selected");
}
select() {
this.toggleAttribute("selected", !0);
}
deselect() {
this.toggleAttribute("selected", !1);
}
isFocused() {
return document.activeElement === this;
}
setName(e) {
if (e.trim().length === 0)
throw new Error("File or folder name cannot be empty");
this.label.textContent = e;
}
getRelativePath() {
return this.path;
}
isFolder() {
return this.expandable;
}
}
class g extends u {
constructor(e) {
super(e);
}
}
customElements.define("ftv-file", g);
class w extends Map {
createStates(e) {
return new Map(
e.map((t) => [t, /* @__PURE__ */ new Map()])
);
}
}
const h = new w(), f = { bubbles: !0 };
function N(i, e) {
const t = h.get(i);
if (t) {
const s = t.get("expanded");
if (s)
return s.get(e);
}
}
function v(i, e, t) {
const s = h.get(i);
if (s) {
const n = s.get("expanded");
if (n)
return n.set(e, t), !0;
}
return !1;
}
class c extends u {
content;
addContent(e = []) {
Array.isArray(e) ? this.content.append(...e) : this.content.appendChild(e);
}
getContent() {
return this.content;
}
clearContent() {
for (; this.content.firstChild; )
this.content.firstChild.remove();
}
get length() {
return this.content.children.length;
}
constructor(e, t = []) {
super(e, !0), this.content = new l(!1), this.content.classList.add("content"), this.content.append(
...t,
...Array.from(this.children).filter(
(s) => !(s instanceof l)
// filter out the label alreade created in the parend class constructor
)
), this.appendChild(this.content);
}
connectedCallback() {
if (super.connectedCallback(), this.isExpanded()) return;
const e = this.getTreeViewContainer(), t = this.getRelativePath();
e && (N(e, t) ? this.expand() : this.collapse());
}
disconnectedCallback() {
const e = this.getTreeViewContainer(), t = this.getRelativePath();
e && v(e, t, this.isExpanded()), super.disconnectedCallback();
}
isExpanded() {
return this.hasAttribute("expanded");
}
toggleExpanded() {
this.toggleAttribute("expanded") ? this.dispatchEvent(new CustomEvent("expand", f)) : this.dispatchEvent(new CustomEvent("collapse", f));
}
expand() {
this.isExpanded() || (this.toggleAttribute("expanded", !0), this.dispatchEvent(new CustomEvent("expand", f)));
}
collapse() {
this.isExpanded() && (this.toggleAttribute("expanded", !1), this.dispatchEvent(new CustomEvent("collapse", f)));
}
}
customElements.define("ftv-folder", c);
const r = { preventScroll: !0 }, F = { block: "nearest" }, m = (i, e = "desc") => e === "desc" ? (t, s) => t[i] < s[i] ? -1 : t[i] > t[i] ? 1 : 0 : (t, s) => t[i] > s[i] ? -1 : t[i] < t[i] ? 1 : 0, y = m("name"), A = m("type", "asc");
class p extends HTMLElement {
selectedItem = null;
getLastVisibleNode(e) {
if (e.isExpanded()) {
const t = e.getContent();
if (t.children.length) {
const s = t.lastElementChild;
return s.isFolder() ? this.getLastVisibleNode(s) : s;
} else
return e;
}
return e;
}
getNextVisibleNode(e) {
const t = e.nextElementSibling;
if (t)
return t;
const s = e.getContainingFolder();
if (s) {
const n = this.getNextVisibleNode(s);
return n === s ? e : n;
}
return e;
}
focusPrevious(e) {
const t = e.previousElementSibling;
if (t)
t.isFolder() ? this.getLastVisibleNode(t).focus(r) : t.focus(r);
else {
const s = e.getContainingFolder();
s && s.focus(r);
}
}
focusNext(e) {
if (e.isFolder())
if (e.isExpanded()) {
const t = e.getContent();
t.children.length ? t.firstElementChild.focus(r) : this.getNextVisibleNode(e).focus(r);
} else
this.getNextVisibleNode(e).focus(r);
else
this.getNextVisibleNode(e).focus(r);
}
load(e, t = this, s = !0, n = !1) {
const { children: o, name: C } = e;
let a = t;
if (s) {
const d = new c(C);
a.addContent(d), a = d;
} else
a = t;
o && (n ? o.sort(y).sort(A) : o).forEach(
(d) => {
const { type: E, name: b } = d;
if (E === "file") {
const x = new g(b);
a.addContent(x);
} else
this.load(d, a);
}
);
}
getSelectedItem() {
return this.selectedItem;
}
getNodeByPath(e) {
let t = null;
const s = e.split("/");
for (const n of s)
if (n)
if (t instanceof c)
t = t.getContent().querySelector(`:scope > [name="${n}"]`);
else {
const o = this.firstElementChild;
if (o)
if (o.getAttribute("name") === n)
t = o;
else break;
else break;
}
return t;
}
addContent(e) {
if (this.firstElementChild)
throw new Error("This tree-view already has a root element.");
this.appendChild(e);
}
addNode(e, t, s) {
const n = e === "/" || e === "" ? this : this.getNodeByPath(e);
if (!n)
throw new Error(`Path ${e} not found.`);
if (n === this && this.firstElementChild)
throw new Error("This tree-view already has a root element.");
if (!(n instanceof c) && n !== this)
throw new Error(`Path ${e} is not a folder.`);
if (s === "file") {
const o = new g(t);
n.addContent(o);
}
if (s === "folder") {
const o = new c(t);
n.addContent(o);
}
}
removeNode(e) {
const t = this.getNodeByPath(e);
if (!t)
throw new Error(`Path ${e} not found.`);
t.remove();
}
connectedCallback() {
h.has(this) || h.set(this, h.createStates(["expanded"]));
}
constructor() {
super(), this.addEventListener("click", (e) => {
const t = e.target;
if (t instanceof p || t.classList.contains("content"))
return;
const s = t.getNode();
s && s.isFolder() && s.toggleExpanded(), this.selectedItem && this.selectedItem.deselect(), this.selectedItem = s, this.selectedItem.select();
}), this.addEventListener("keydown", (e) => {
e.code !== "Tab" && e.preventDefault();
const t = e.target === this ? this.firstElementChild : e.target;
if (t) {
const s = t.getNode();
if (s) {
if (e.code === "ArrowUp" && this.focusPrevious(s), e.code === "ArrowDown" && this.focusNext(s), e.code === "ArrowLeft")
if (s.isFolder() && s.isExpanded())
s.collapse();
else {
const n = s.getContainingFolder();
n && n.focus(r);
}
if (e.code === "ArrowRight" && s.isFolder())
if (!s.isExpanded())
s.expand();
else {
const n = s.getContent().firstElementChild;
n && n.focus(r);
}
if (e.code === "Home") {
const n = this.firstElementChild;
n && n.focus();
}
if (e.code === "End") {
const n = this.lastElementChild;
n && (n.isFolder() ? this.getLastVisibleNode(n).focus() : n.focus());
}
}
}
}), this.addEventListener("focusin", (e) => {
const s = e.target.firstElementChild, n = this.scrollLeft;
s.scrollIntoView(F), this.scrollLeft = n;
}), this.tabIndex = 0;
}
}
customElements.define("file-tree-view", p);
export {
p as default
};