UNPKG

@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
/*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" @click=${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 @click=${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" @click=${(ev) => clearItemSelection({ item, ev })} type="button" > &times; </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} @highlighted-node-changed=${lift(setHighlightedNode)} .searchPlaceholder=${searchPlaceholder} .searchGlobalPlaceholder=${searchGlobalPlaceholder} .searchMinLength=${searchMinLength} .searchDebounceTimeout=${searchDebounceTimeout} .tree=${tree} .opened=${opened} .nodesOnNodePath=${nodesOnNodePath} @nodes-on-node-path-changed=${lift(setNodesOnNodePath)} @node-dblclicked=${selectNode} @on-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" @click=${selectNode} > Select </button> <button class="dialog-footer-button" part="cancel-button" @click=${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