@neovici/cosmoz-treenode-navigator
Version:
A Pion component that lets you navigate and search through hierarchically structured data-nodes and select one of them.
241 lines (240 loc) • 7.72 kB
JavaScript
/*eslint-disable max-statements */
import { component, lift, useEffect, useMemo, useProperty, useRef, useState, } from '@pionjs/pion';
import { html } from 'lit-html';
import { ref } from 'lit/directives/ref.js';
import style from './cosmoz-treenode-button-view.styles';
import './cosmoz-treenode-navigator';
import { useKeyDown } from './hooks/useKeyDown';
import { when } from 'lit-html/directives/when.js';
import { debounce$ } from '@neovici/cosmoz-utils/promise';
import { getTreePathParts } from './util/helpers';
const CosmozNodeButtonView = ({ tree, multiSelection = false, dialogText, buttonTextPlaceholder, searchPlaceholder, noReset = false, searchGlobalPlaceholder, searchMinLength = 3, searchDebounceTimeout = 2000, }) => {
const dialogRef = useRef(null);
const searchInputRef = useRef(null);
const [highlightedNode, setHighlightedNode] = useProperty('highlightedNode');
// legacy props
const [selectedNode, setSelectedNode] = useProperty('selectedNode');
const [nodePath, setNodePath] = useProperty('nodePath', '');
const [nodesOnNodePath, setNodesOnNodePath] = useProperty('nodesOnNodePath', []);
const [opened, setOpened] = useState(false);
const [selectedNodes, setSelectedNodes] = useState([]);
useEffect(() => {
if (dialogRef.current) {
searchInputRef.current = dialogRef.current
.querySelector('cosmoz-treenode-navigator')
?.shadowRoot?.querySelector('cosmoz-input');
}
}, [dialogRef.current]);
// keep legacy props in sync w/ "highlightedNode"
useEffect(() => {
if (highlightedNode !== selectedNode) {
setSelectedNode(highlightedNode);
}
if (highlightedNode?.pathLocator !== nodePath) {
setNodePath(highlightedNode?.pathLocator ?? '');
}
}, [highlightedNode]);
// external updates to "selectedNode"
useEffect(() => {
if (selectedNode && selectedNode !== highlightedNode) {
setHighlightedNode(selectedNode);
}
}, [selectedNode]);
// external updates to "nodePath"
useEffect(() => {
if (!(nodePath || tree)) {
return;
}
const parts = getTreePathParts(nodePath, tree);
setNodesOnNodePath(parts);
const last = parts.length > 0 ? parts[parts.length - 1] : null;
if (last && last !== highlightedNode) {
setHighlightedNode(last);
}
}, [nodePath, tree]);
const buttonLabel = useMemo(() => {
if (!Array.isArray(nodesOnNodePath) || nodesOnNodePath.length === 0) {
return buttonTextPlaceholder || '';
}
return nodesOnNodePath
.filter((n) => n)
.map((part) => part[tree.searchProperty])
.join(' / ');
}, [nodesOnNodePath, tree]);
const reset = () => {
setNodesOnNodePath([]);
setSelectedNodes([]);
setHighlightedNode(null);
setSelectedNode(null);
setNodePath('');
};
const clearItemSelection = ({ item, ev }) => {
setSelectedNodes(selectedNodes.filter((node) => node !== item));
ev.preventDefault();
ev.stopPropagation();
};
const getChipText = (node) => {
return node.name;
};
const showSelectedNodes = (multiSelection, selectedNodesLength) => {
return multiSelection && selectedNodesLength > 0;
};
const refit = debounce$(() => {
dialogRef.current?.fit?.();
}, 50);
const focusSearch = () => {
if (searchInputRef.current) {
searchInputRef.current.focus();
}
};
const onOpen = () => {
dialogRef.current?.showModal();
setOpened(true);
setTimeout(focusSearch, 0);
};
const onClose = () => {
setOpened(false);
dialogRef.current?.close();
};
useKeyDown('Escape', onClose);
// `cosmoz-treenode-navigator` handles updating nodesOnNodePath
const selectNode = () => {
if (!highlightedNode?.pathLocator) {
return;
}
if (multiSelection &&
!selectedNodes.some((n) => n.pathLocator === highlightedNode.pathLocator)) {
setSelectedNodes([...selectedNodes, highlightedNode]);
}
// update legacy props
setSelectedNode(highlightedNode);
setNodePath(highlightedNode.pathLocator);
setNodesOnNodePath(getTreePathParts(highlightedNode.pathLocator, tree));
onClose();
};
return html `
<div class="actions" part="actions">
<button
class="action-open"
type="button"
=${onOpen}
part="action-open"
>
<slot name="button-before"></slot>
<div class="path-text">
<span>${buttonLabel}</span>
</div>
<slot name="button-after"></slot>
</button>
${when(!noReset && !!highlightedNode, () => html ` <button
=${reset}
class="action-reset"
part="action-reset"
>
<svg
width="10"
height="9"
viewBox="0 0 10 9"
stroke="currentColor"
xmlns="http://www.w3.org/2000/svg"
>
<line
x1="8.53033"
y1="0.53033"
x2="1.53033"
y2="7.53033"
stroke-width="1.5"
></line>
<line
x1="8.46967"
y1="7.53033"
x2="1.46967"
y2="0.530331"
stroke-width="1.5"
></line>
</svg>
</button>`)}
</div>
${when(showSelectedNodes(multiSelection, selectedNodes.length), () => html `
<div id="chips" part="chips" class="row">
${selectedNodes.map((item) => html `
<div class="chip">
<span>${getChipText(item)}</span>
<button
class="chip__clear"
=${(ev) => clearItemSelection({ item, ev })}
type="button"
>
×
</button>
</div>
`)}
</div>
`)}
<dialog
class="dialog"
part="dialog"
${ref((el) => {
dialogRef.current = el;
})}
>
<header class="dialog-header" part="header">
<h1 class="dialog-heading" part="heading">${dialogText}</h1>
</header>
<main class="dialog-main" part="main">
<cosmoz-treenode-navigator
id="treeNavigator"
class="dialog-treenode-navigator no-padding"
.highlightedNode=${highlightedNode}
-node-changed=${lift(setHighlightedNode)}
.searchPlaceholder=${searchPlaceholder}
.searchGlobalPlaceholder=${searchGlobalPlaceholder}
.searchMinLength=${searchMinLength}
.searchDebounceTimeout=${searchDebounceTimeout}
.tree=${tree}
.opened=${opened}
.nodesOnNodePath=${nodesOnNodePath}
-on-node-path-changed=${lift(setNodesOnNodePath)}
-dblclicked=${selectNode}
-data-plane-changed=${refit}
>
<slot></slot>
</cosmoz-treenode-navigator>
</main>
<footer class="dialog-footer" part="footer">
<p class="dialog-footer-button-container">
<button
?disabled=${!highlightedNode?.pathLocator}
autofocus
class="dialog-footer-button"
part="select-button"
=${selectNode}
>
Select
</button>
<button
class="dialog-footer-button"
part="cancel-button"
=${onClose}
>
Cancel
</button>
</p>
</footer>
</dialog>
`;
};
CosmozNodeButtonView.observedAttributes = [
'button-text-placeholder',
'dialog-text',
'search-placeholder',
'search-global-placeholder',
'node-path',
'selected-node',
'no-reset',
'search-min-length',
];
customElements.define('cosmoz-treenode-button-view', component(CosmozNodeButtonView, {
styleSheets: [style],
}));
//# sourceMappingURL=cosmoz-treenode-button-view.js.map