@deepdub/react-arborist
Version:
623 lines (622 loc) • 21 kB
JavaScript
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
import * as utils from "../utils";
import { DefaultCursor } from "../components/default-cursor";
import { DefaultRow } from "../components/default-row";
import { DefaultNode } from "../components/default-node";
import { edit } from "../state/edit-slice";
import { focus, treeBlur } from "../state/focus-slice";
import { createRoot, ROOT_ID } from "../data/create-root";
import { actions as visibility } from "../state/open-slice";
import { actions as selection } from "../state/selection-slice";
import { actions as dnd } from "../state/dnd-slice";
import { DefaultDragPreview } from "../components/default-drag-preview";
import { DefaultContainer } from "../components/default-container";
import { createList } from "../data/create-list";
import { createIndex } from "../data/create-index";
const { safeRun, identify, identifyNull } = utils;
export class TreeApi {
constructor(store, props, list, listEl) {
this.store = store;
this.props = props;
this.list = list;
this.listEl = listEl;
this.visibleStartIndex = 0;
this.visibleStopIndex = 0;
/* Changes here must also be made in update() */
this.root = createRoot(this);
this.visibleNodes = createList(this);
this.idToIndex = createIndex(this.visibleNodes);
}
/* Changes here must also be made in constructor() */
update(props) {
this.props = props;
this.root = createRoot(this);
this.visibleNodes = createList(this);
this.idToIndex = createIndex(this.visibleNodes);
}
/* Store helpers */
dispatch(action) {
return this.store.dispatch(action);
}
get state() {
return this.store.getState();
}
get openState() {
return this.state.nodes.open.unfiltered;
}
/* Tree Props */
get width() {
var _a;
return (_a = this.props.width) !== null && _a !== void 0 ? _a : 300;
}
get height() {
var _a;
return (_a = this.props.height) !== null && _a !== void 0 ? _a : 500;
}
get indent() {
var _a;
return (_a = this.props.indent) !== null && _a !== void 0 ? _a : 24;
}
get rowHeight() {
var _a;
return (_a = this.props.rowHeight) !== null && _a !== void 0 ? _a : 24;
}
get overscanCount() {
var _a;
return (_a = this.props.overscanCount) !== null && _a !== void 0 ? _a : 1;
}
get searchTerm() {
return (this.props.searchTerm || "").trim();
}
get matchFn() {
var _a;
const match = (_a = this.props.searchMatch) !== null && _a !== void 0 ? _a : ((node, term) => {
const string = JSON.stringify(Object.values(node.data));
return string.toLocaleLowerCase().includes(term.toLocaleLowerCase());
});
return (node) => match(node, this.searchTerm);
}
accessChildren(data) {
var _a;
const get = this.props.childrenAccessor || "children";
return (_a = utils.access(data, get)) !== null && _a !== void 0 ? _a : null;
}
accessId(data) {
const get = this.props.idAccessor || "id";
const id = utils.access(data, get);
if (!id)
throw new Error("Data must contain an 'id' property or props.idAccessor must return a string");
return id;
}
/* Node Access */
get firstNode() {
var _a;
return (_a = this.visibleNodes[0]) !== null && _a !== void 0 ? _a : null;
}
get lastNode() {
var _a;
return (_a = this.visibleNodes[this.visibleNodes.length - 1]) !== null && _a !== void 0 ? _a : null;
}
get focusedNode() {
var _a;
return (_a = this.get(this.state.nodes.focus.id)) !== null && _a !== void 0 ? _a : null;
}
get mostRecentNode() {
var _a;
return (_a = this.get(this.state.nodes.selection.mostRecent)) !== null && _a !== void 0 ? _a : null;
}
get nextNode() {
const index = this.indexOf(this.focusedNode);
if (index === null)
return null;
else
return this.at(index + 1);
}
get prevNode() {
const index = this.indexOf(this.focusedNode);
if (index === null)
return null;
else
return this.at(index - 1);
}
get(id) {
if (!id)
return null;
if (id in this.idToIndex)
return this.visibleNodes[this.idToIndex[id]] || null;
else
return null;
}
at(index) {
return this.visibleNodes[index] || null;
}
nodesBetween(startId, endId) {
var _a;
if (startId === null || endId === null)
return [];
const index1 = (_a = this.indexOf(startId)) !== null && _a !== void 0 ? _a : 0;
const index2 = this.indexOf(endId);
if (index2 === null)
return [];
const start = Math.min(index1, index2);
const end = Math.max(index1, index2);
return this.visibleNodes.slice(start, end + 1);
}
indexOf(id) {
const key = utils.identifyNull(id);
if (!key)
return null;
return this.idToIndex[key];
}
/* Data Operations */
get editingId() {
return this.state.nodes.edit.id;
}
createInternal() {
return this.create({ type: "internal" });
}
createLeaf() {
return this.create({ type: "leaf" });
}
create(opts = {}) {
var _a, _b;
return __awaiter(this, void 0, void 0, function* () {
const parentId = opts.parentId === undefined
? utils.getInsertParentId(this)
: opts.parentId;
const index = (_a = opts.index) !== null && _a !== void 0 ? _a : utils.getInsertIndex(this);
const type = (_b = opts.type) !== null && _b !== void 0 ? _b : "leaf";
const data = yield safeRun(this.props.onCreate, {
type,
parentId,
index,
parentNode: this.get(parentId),
});
if (data) {
this.focus(data);
setTimeout(() => {
this.edit(data).then(() => {
this.select(data);
this.activate(data);
});
});
}
});
}
delete(node) {
return __awaiter(this, void 0, void 0, function* () {
if (!node)
return;
const idents = Array.isArray(node) ? node : [node];
const ids = idents.map(identify);
const nodes = ids.map((id) => this.get(id)).filter((n) => !!n);
yield safeRun(this.props.onDelete, { nodes, ids });
});
}
edit(node) {
const id = identify(node);
this.resolveEdit({ cancelled: true });
this.scrollTo(id);
this.dispatch(edit(id));
return new Promise((resolve) => {
TreeApi.editPromise = resolve;
});
}
submit(identity, value) {
return __awaiter(this, void 0, void 0, function* () {
if (!identity)
return;
const id = identify(identity);
yield safeRun(this.props.onRename, {
id,
name: value,
node: this.get(id),
});
this.dispatch(edit(null));
this.resolveEdit({ cancelled: false, value });
setTimeout(() => this.onFocus()); // Return focus to element;
});
}
reset() {
this.dispatch(edit(null));
this.resolveEdit({ cancelled: true });
setTimeout(() => this.onFocus()); // Return focus to element;
}
activate(id) {
const node = this.get(identifyNull(id));
if (!node)
return;
safeRun(this.props.onActivate, node);
}
resolveEdit(value) {
const resolve = TreeApi.editPromise;
if (resolve)
resolve(value);
TreeApi.editPromise = null;
}
/* Focus and Selection */
get selectedIds() {
return this.state.nodes.selection.ids;
}
get selectedNodes() {
let nodes = [];
for (let id of Array.from(this.selectedIds)) {
const node = this.get(id);
if (node)
nodes.push(node);
}
return nodes;
}
focus(node, opts = {}) {
if (!node)
return;
/* Focus is responsible for scrolling, while selection is
* responsible for focus. If selectionFollowsFocus, then
* just select it. */
if (this.props.selectionFollowsFocus) {
this.select(node);
}
else {
this.dispatch(focus(identify(node)));
if (opts.scroll !== false)
this.scrollTo(node);
if (this.focusedNode)
safeRun(this.props.onFocus, this.focusedNode);
}
}
pageUp() {
var _a, _b;
const start = this.visibleStartIndex;
const stop = this.visibleStopIndex;
const page = stop - start;
let index = (_b = (_a = this.focusedNode) === null || _a === void 0 ? void 0 : _a.rowIndex) !== null && _b !== void 0 ? _b : 0;
if (index > start) {
index = start;
}
else {
index = Math.max(start - page, 0);
}
this.focus(this.at(index));
}
pageDown() {
var _a, _b;
const start = this.visibleStartIndex;
const stop = this.visibleStopIndex;
const page = stop - start;
let index = (_b = (_a = this.focusedNode) === null || _a === void 0 ? void 0 : _a.rowIndex) !== null && _b !== void 0 ? _b : 0;
if (index < stop) {
index = stop;
}
else {
index = Math.min(index + page, this.visibleNodes.length - 1);
}
this.focus(this.at(index));
}
select(node, opts = {}) {
if (!node)
return;
const changeFocus = opts.focus !== false;
const id = identify(node);
if (changeFocus)
this.dispatch(focus(id));
this.dispatch(selection.only(id));
this.dispatch(selection.anchor(id));
this.dispatch(selection.mostRecent(id));
this.scrollTo(id, opts.align);
if (this.focusedNode && changeFocus) {
safeRun(this.props.onFocus, this.focusedNode);
}
safeRun(this.props.onSelect, this.selectedNodes);
}
deselect(node) {
if (!node)
return;
const id = identify(node);
this.dispatch(selection.remove(id));
}
selectMulti(identity) {
const node = this.get(identifyNull(identity));
if (!node)
return;
this.dispatch(focus(node.id));
this.dispatch(selection.add(node.id));
this.dispatch(selection.anchor(node.id));
this.dispatch(selection.mostRecent(node.id));
this.scrollTo(node);
if (this.focusedNode)
safeRun(this.props.onFocus, this.focusedNode);
safeRun(this.props.onSelect, this.selectedNodes);
}
selectContiguous(identity) {
if (!identity)
return;
const id = identify(identity);
const { anchor, mostRecent } = this.state.nodes.selection;
this.dispatch(focus(id));
this.dispatch(selection.remove(this.nodesBetween(anchor, mostRecent)));
this.dispatch(selection.add(this.nodesBetween(anchor, identifyNull(id))));
this.dispatch(selection.mostRecent(id));
this.scrollTo(id);
if (this.focusedNode)
safeRun(this.props.onFocus, this.focusedNode);
safeRun(this.props.onSelect, this.selectedNodes);
}
deselectAll() {
this.setSelection({ ids: [], anchor: null, mostRecent: null });
safeRun(this.props.onSelect, this.selectedNodes);
}
selectAll() {
var _a;
this.setSelection({
ids: Object.keys(this.idToIndex),
anchor: this.firstNode,
mostRecent: this.lastNode,
});
this.dispatch(focus((_a = this.lastNode) === null || _a === void 0 ? void 0 : _a.id));
if (this.focusedNode)
safeRun(this.props.onFocus, this.focusedNode);
safeRun(this.props.onSelect, this.selectedNodes);
}
setSelection(args) {
var _a;
const ids = new Set((_a = args.ids) === null || _a === void 0 ? void 0 : _a.map(identify));
const anchor = identifyNull(args.anchor);
const mostRecent = identifyNull(args.mostRecent);
this.dispatch(selection.set({ ids, anchor, mostRecent }));
safeRun(this.props.onSelect, this.selectedNodes);
}
/* Drag and Drop */
get cursorParentId() {
const { cursor } = this.state.dnd;
switch (cursor.type) {
case "highlight":
return cursor.id;
default:
return null;
}
}
get cursorOverFolder() {
return this.state.dnd.cursor.type === "highlight";
}
get dragNodes() {
return this.state.dnd.dragIds
.map((id) => this.get(id))
.filter((n) => !!n);
}
get dragNode() {
return this.get(this.state.nodes.drag.id);
}
get dragDestinationParent() {
return this.get(this.state.nodes.drag.destinationParentId);
}
get dragDestinationIndex() {
return this.state.nodes.drag.destinationIndex;
}
canDrop() {
var _a;
if (this.isFiltered)
return false;
const parentNode = (_a = this.get(this.state.dnd.parentId)) !== null && _a !== void 0 ? _a : this.root;
const dragNodes = this.dragNodes;
const isDisabled = this.props.disableDrop;
for (const drag of dragNodes) {
if (!drag)
return false;
if (!parentNode)
return false;
if (drag.isInternal && utils.isDescendant(parentNode, drag))
return false;
}
// Allow the user to insert their own logic
if (typeof isDisabled == "function") {
return !isDisabled({
parentNode,
dragNodes: this.dragNodes,
index: this.state.dnd.index || 0,
});
}
else if (typeof isDisabled == "string") {
// @ts-ignore
return !parentNode.data[isDisabled];
}
else if (typeof isDisabled === "boolean") {
return !isDisabled;
}
else {
return true;
}
}
hideCursor() {
this.dispatch(dnd.cursor({ type: "none" }));
}
showCursor(cursor) {
this.dispatch(dnd.cursor(cursor));
}
/* Visibility */
open(identity) {
const id = identifyNull(identity);
if (!id)
return;
if (this.isOpen(id))
return;
this.dispatch(visibility.open(id, this.isFiltered));
safeRun(this.props.onToggle, id);
}
close(identity) {
const id = identifyNull(identity);
if (!id)
return;
if (!this.isOpen(id))
return;
this.dispatch(visibility.close(id, this.isFiltered));
safeRun(this.props.onToggle, id);
}
toggle(identity) {
const id = identifyNull(identity);
if (!id)
return;
return this.isOpen(id) ? this.close(id) : this.open(id);
}
openParents(identity) {
const id = identifyNull(identity);
if (!id)
return;
const node = utils.dfs(this.root, id);
let parent = node === null || node === void 0 ? void 0 : node.parent;
while (parent) {
this.open(parent.id);
parent = parent.parent;
}
}
openSiblings(node) {
const parent = node.parent;
if (!parent) {
this.toggle(node.id);
}
else if (parent.children) {
const isOpen = node.isOpen;
for (let sibling of parent.children) {
if (sibling.isInternal) {
isOpen ? this.close(sibling.id) : this.open(sibling.id);
}
}
this.scrollTo(this.focusedNode);
}
}
openAll() {
utils.walk(this.root, (node) => {
if (node.isInternal)
node.open();
});
}
closeAll() {
utils.walk(this.root, (node) => {
if (node.isInternal)
node.close();
});
}
/* Scrolling */
scrollTo(identity, align = "smart") {
if (!identity)
return;
const id = identify(identity);
this.openParents(id);
return utils
.waitFor(() => id in this.idToIndex)
.then(() => {
var _a;
const index = this.idToIndex[id];
if (index === undefined)
return;
(_a = this.list.current) === null || _a === void 0 ? void 0 : _a.scrollToItem(index, align);
})
.catch(() => {
// Id: ${id} never appeared in the list.
});
}
/* State Checks */
get isEditing() {
return this.state.nodes.edit.id !== null;
}
get isFiltered() {
var _a;
return !!((_a = this.props.searchTerm) === null || _a === void 0 ? void 0 : _a.trim());
}
get hasFocus() {
return this.state.nodes.focus.treeFocused;
}
get hasNoSelection() {
return this.state.nodes.selection.ids.size === 0;
}
get hasOneSelection() {
return this.state.nodes.selection.ids.size === 1;
}
get hasMultipleSelections() {
return this.state.nodes.selection.ids.size > 1;
}
isSelected(id) {
if (!id)
return false;
return this.state.nodes.selection.ids.has(id);
}
isOpen(id) {
var _a, _b, _c;
if (!id)
return false;
if (id === ROOT_ID)
return true;
const def = (_a = this.props.openByDefault) !== null && _a !== void 0 ? _a : true;
if (this.isFiltered) {
return (_b = this.state.nodes.open.filtered[id]) !== null && _b !== void 0 ? _b : true; // Filtered folders are always opened by default
}
else {
return (_c = this.state.nodes.open.unfiltered[id]) !== null && _c !== void 0 ? _c : def;
}
}
isEditable(data) {
var _a;
const check = this.props.disableEdit || (() => false);
return (_a = !utils.access(data, check)) !== null && _a !== void 0 ? _a : true;
}
isDraggable(data) {
var _a;
const check = this.props.disableDrag || (() => false);
return (_a = !utils.access(data, check)) !== null && _a !== void 0 ? _a : true;
}
isDragging(node) {
const id = identifyNull(node);
if (!id)
return false;
return this.state.nodes.drag.id === id;
}
isFocused(id) {
return this.hasFocus && this.state.nodes.focus.id === id;
}
isMatch(node) {
return this.matchFn(node);
}
willReceiveDrop(node) {
const id = identifyNull(node);
if (!id)
return false;
const { destinationParentId, destinationIndex } = this.state.nodes.drag;
return id === destinationParentId && destinationIndex === null;
}
/* Tree Event Handlers */
onFocus() {
const node = this.focusedNode || this.firstNode;
if (node)
this.dispatch(focus(node.id));
}
onBlur() {
this.dispatch(treeBlur());
}
onItemsRendered(args) {
this.visibleStartIndex = args.visibleStartIndex;
this.visibleStopIndex = args.visibleStopIndex;
}
/* Get Renderers */
get renderContainer() {
return this.props.renderContainer || DefaultContainer;
}
get renderRow() {
return this.props.renderRow || DefaultRow;
}
get renderNode() {
return this.props.children || DefaultNode;
}
get renderDragPreview() {
return this.props.renderDragPreview || DefaultDragPreview;
}
get renderCursor() {
return this.props.renderCursor || DefaultCursor;
}
}