UNPKG

vue3-tree-vue

Version:

A Simple vue3 project for rendering items in a tree.

527 lines (514 loc) 20 kB
import { defineComponent, ref, computed, inject, onMounted, nextTick, watch, openBlock, createElementBlock, withModifiers, createCommentVNode, renderSlot, normalizeProps, guardReactiveProps, createElementVNode, normalizeClass, toDisplayString, createTextVNode, withDirectives, withKeys, vModelText, provide, resolveComponent, Fragment, renderList, createVNode, withCtx, mergeProps, createSlots } from 'vue'; const _TREE_STATE_PROVIDER_INJECT_KEY = "VUE3_TREE_VUE_TREE_STATE"; const updateChildrenCheckState = (parent, state) => { if (!parent.children) return; parent.children.forEach(child => { child.checked = parent.checked; state.getNode(child.id).indeterminate = false; updateChildrenCheckState(child, state); }); }; const updateParentCheckState = (child, state) => { if (!child) return; const parent = state.getParent(child.id); if (!parent) return; const parentNode = state.getNode(parent.id); if (parent.children) { const isEveryChildChecked = parent.children.every(child => child.checked); const someUnChecked = parent.children.some(child => !child.checked); const someChecked = parent.children.some(child => child.checked || state.getNode(child.id).indeterminate); const intermediate = someChecked && someUnChecked; if (isEveryChildChecked) { parentNode.checked = true; parentNode.indeterminate = false; } else if (intermediate) { parentNode.checked = false; parentNode.indeterminate = true; } else { parentNode.checked = false; parentNode.indeterminate = false; } } updateParentCheckState(parent, state); }; var script$1 = defineComponent({ inheritAttrs: true, props: { item: { type: Object, required: true }, isCheckable: { type: Boolean }, canRename: { type: Boolean }, checkboxStyle: { type: String }, lazyLoad: { type: Boolean }, hideGuideLines: { type: Boolean } }, emits: ["on-rename", "onContextMenu"], setup(props, { emit, attrs }) { const checkbox = ref(); const parent = computed(() => attrs.parent); const treeState = inject(_TREE_STATE_PROVIDER_INJECT_KEY); const setCheckboxState = () => { if (checkbox.value && props.item.checked !== undefined) { checkbox.value.checked = props.item.checked; } }; onMounted(() => { var _parent$value; treeState.trackNode(props.item, parent.value); if (props.item.checked) { nextTick(() => { setCheckboxState(); updateParentCheckState(props.item, treeState); }); } else if (((_parent$value = parent.value) === null || _parent$value === void 0 ? void 0 : _parent$value.checked) !== undefined) { props.item.checked = parent.value.checked; } }); const styles = computed(() => { var style = { 'selected-tree-item': !props.isCheckable && props.item.selected }; if (props.item.styles) { props.item.styles.forEach(s => style[s] = true); } return style; }); const updateCheckState = () => { if (checkbox.value) { props.item.checked = checkbox.value.checked; props.item.indeterminate = false; updateParentCheckState(props.item, treeState); updateChildrenCheckState(props.item, treeState); treeState.emitItemCheckedChange(); } }; watch(() => props.item.indeterminate, () => { if (checkbox.value && props.item.indeterminate !== undefined) { checkbox.value.indeterminate = props.item.indeterminate; } }); watch(() => props.item.checked, () => nextTick(setCheckboxState)); watch(() => props.item.expanded, () => toggleExpand(false)); watch(() => { var _props$item$children; return (_props$item$children = props.item.children) === null || _props$item$children === void 0 ? void 0 : _props$item$children.length; }, () => { var _props$item$children2; return (_props$item$children2 = props.item.children) === null || _props$item$children2 === void 0 ? void 0 : _props$item$children2.forEach(child => treeState === null || treeState === void 0 ? void 0 : treeState.trackNode(child, props.item)); }); const isRenaming = ref(false); const renameBox = ref(); const beginRenaming = () => { if (!props.canRename) return; isRenaming.value = true; nextTick().then(() => { var _renameBox$value; return (_renameBox$value = renameBox.value) === null || _renameBox$value === void 0 ? void 0 : _renameBox$value.focus(); }); }; const finishRenaming = () => { // v-on:blur and key(enter) can cause this to fire twice. // this check protects against that. if (!isRenaming) return; emit("on-rename", props.item); }; const toggleExpand = (shouldSet = true) => { /// `expanded` can be set programmatically. When this is the case `shouldSet` is false /// see watch(props.item.expanded) /// the component should not bother flipping the expanded or firing the /// expanded/collapsed events. if (shouldSet) { props.item.expanded = !props.item.expanded; } if (props.item.expanded) { treeState === null || treeState === void 0 || treeState.emitItemExpanded(props.item); } else { treeState === null || treeState === void 0 || treeState.emitItemCollapsed(props.item); } }; return { styles, checkbox, parent, treeState, isRenaming, renameBox, updateCheckState, beginRenaming, finishRenaming, toggleExpand }; } }); const _hoisted_1$1 = { key: 0, class: "guide-line" }; const _hoisted_2$1 = { key: 0, class: "tree-item__checkbox-area" }; const _hoisted_3$1 = ["disabled"]; const _hoisted_4$1 = { class: "d-flex" }; const _hoisted_5 = { class: "tiny_horizontal_margin" }; const _hoisted_6 = { class: "tiny_horizontal_margin" }; const _hoisted_7 = { key: 0, for: "checkbox", class: "pointer" }; const _hoisted_8 = { class: "tiny_horizontal_margin" }; const _hoisted_9 = { class: "tiny_horizontal_margin" }; function render$1(_ctx, _cache, $props, $setup, $data, $options) { return openBlock(), createElementBlock("div", { class: "d-flex align-items-center", onContextmenu: _cache[8] || (_cache[8] = withModifiers($event => _ctx.$emit('onContextMenu', { item: _ctx.item, event: $event }), ["prevent"])) }, [_ctx.parent != null && !_ctx.hideGuideLines ? (openBlock(), createElementBlock("div", _hoisted_1$1)) : createCommentVNode("", true), _ctx.lazyLoad || _ctx.item.children && _ctx.item.children.length > 0 ? (openBlock(), createElementBlock("div", { key: 1, onClick: _cache[0] || (_cache[0] = $event => _ctx.toggleExpand()) }, [renderSlot(_ctx.$slots, "expander", normalizeProps(guardReactiveProps(_ctx.item)), () => [createElementVNode("span", { class: normalizeClass(["chevron-right", { 'rotate-90': _ctx.item.expanded }]) }, null, 2)])])) : createCommentVNode("", true), createElementVNode("div", { class: normalizeClass(["pointer tree-item", _ctx.styles]), style: { "width": "100%" } }, [!_ctx.isRenaming ? (openBlock(), createElementBlock("div", { key: 0, onDblclick: _cache[4] || (_cache[4] = (...args) => _ctx.beginRenaming && _ctx.beginRenaming(...args)) }, [_ctx.isCheckable ? (openBlock(), createElementBlock("div", _hoisted_2$1, [createElementVNode("input", { onContextmenu: _cache[1] || (_cache[1] = withModifiers(() => {}, ["prevent"])), onChange: _cache[2] || (_cache[2] = (...args) => _ctx.updateCheckState && _ctx.updateCheckState(...args)), type: "checkbox", ref: "checkbox", disabled: _ctx.item.disabled, class: normalizeClass(_ctx.checkboxStyle) }, null, 42, _hoisted_3$1), createElementVNode("div", _hoisted_4$1, [createElementVNode("div", _hoisted_5, [renderSlot(_ctx.$slots, "icon")]), createElementVNode("div", _hoisted_6, [renderSlot(_ctx.$slots, "prepend")])]), !_ctx.isRenaming ? (openBlock(), createElementBlock("label", _hoisted_7, toDisplayString(_ctx.item.name), 1)) : createCommentVNode("", true), _cache[9] || (_cache[9] = createTextVNode("   ")), renderSlot(_ctx.$slots, "append")])) : (openBlock(), createElementBlock("div", { key: 1, class: "d-flex", onClick: _cache[3] || (_cache[3] = $event => { var _ctx$treeState; return (_ctx$treeState = _ctx.treeState) === null || _ctx$treeState === void 0 ? void 0 : _ctx$treeState.emitItemSelected(_ctx.item); }) }, [createElementVNode("div", _hoisted_8, [renderSlot(_ctx.$slots, "icon")]), createElementVNode("div", _hoisted_9, [renderSlot(_ctx.$slots, "prepend")]), renderSlot(_ctx.$slots, "name", {}, () => [createElementVNode("span", null, toDisplayString(_ctx.item.name), 1)]), _cache[10] || (_cache[10] = createTextVNode("   ")), renderSlot(_ctx.$slots, "append")]))], 32)) : withDirectives((openBlock(), createElementBlock("input", { key: 1, ref: "rename-box", "onUpdate:modelValue": _cache[5] || (_cache[5] = $event => _ctx.item.name = $event), onKeyup: _cache[6] || (_cache[6] = withKeys((...args) => _ctx.finishRenaming && _ctx.finishRenaming(...args), ["enter"])), onBlur: _cache[7] || (_cache[7] = (...args) => _ctx.finishRenaming && _ctx.finishRenaming(...args)) }, null, 544)), [[vModelText, _ctx.item.name]])], 2)], 32); } script$1.render = render$1; function useTreeViewItemMouseActions() { const addHoverClass = event => { const target = event.currentTarget; if (target) { target.classList.add('tree-item__drag-over'); } }; const addRootHoverClass = (event, isRootNode) => { if (!isRootNode) return; const target = event.currentTarget; if (target) { target.classList.add('root__drag-over'); } }; const removeRootHoverClass = (event, isRootNode) => { if (!isRootNode) return; const target = event.currentTarget; if (target) { target.classList.remove('root__drag-over'); } }; const removeHoverClass = event => { const target = event.currentTarget; if (target) { target.classList.remove('tree-item__drag-over'); } }; const onDragNode = (item, event) => { if (item.disableDragAndDrop) return; if (event.dataTransfer) { event.dataTransfer.setData('text/plain', JSON.stringify(item)); } }; const onDropNode = (dropHost, event, isDropValid, state) => { if (event.dataTransfer) { removeHoverClass(event); const droppedNode = JSON.parse(event.dataTransfer.getData('text/plain')); if (!isDropValid) return; if (dropHost && droppedNode.id === dropHost.id || droppedNode.disableDragAndDrop) { return; } isDropValid(droppedNode, dropHost).then(canDrop => { if (canDrop) { state.detach(droppedNode.id); if (dropHost && !dropHost.children) dropHost.children = []; if (dropHost) dropHost.children.push(droppedNode);else state.attach(droppedNode); // Dropping into root } }); } }; return { addHoverClass, removeHoverClass, onDragNode, onDropNode, addRootHoverClass, removeRootHoverClass }; } function uuidv4() { return '10000000-1000-4000-8000-100000000000'.replace(/[018]/g, c => (Number.parseFloat(c) ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> Number.parseFloat(c) / 4).toString(16)); } // TODO: Watch children length: When node is deleted: Remove from graph. /** * Initialises the root state of a tree. * @param itemSelectedEventHandler A callback when an item is selected. * @param itemSelectedEventHandler A callback when an item is checked. * @returns */ function useGraph(items, itemSelectedEventHandler, itemCheckedEventHandler, itemExpandedEventHandler, itemCollapsedEventHandler) { const childParentLookUp = {}; const nodeLookUp = {}; const getParent = childId => childParentLookUp[childId]; const trackNode = (node, parentNode) => { if (!node.id) { node.id = uuidv4(); } nodeLookUp[node.id] = node; childParentLookUp[node.id] = parentNode; }; const detach = id => { const parent = childParentLookUp[id]; if (parent == null) { items.value = items.value.filter(item => item.id != id); } else { var _parent$children; parent.children = (_parent$children = parent.children) === null || _parent$children === void 0 ? void 0 : _parent$children.filter(child => child.id != id); } delete childParentLookUp[id]; }; const attach = item => { items.value.push(item); trackNode(item, undefined); }; const emitItemSelected = node => { if (node.disabled === true) { return; } itemSelectedEventHandler(node); Object.values(nodeLookUp).forEach(node => node.selected = false); node.selected = true; }; const emitItemCheckedChange = () => itemCheckedEventHandler(Object.values(nodeLookUp).filter(node => node.checked && !node.disabled)); const getNode = id => nodeLookUp[id]; return { getNode, getParent, trackNode, detach, emitItemCheckedChange, emitItemSelected, emitItemExpanded: itemExpandedEventHandler, emitItemCollapsed: itemCollapsedEventHandler, attach }; } var script = defineComponent({ name: 'tree-view', props: { items: { type: Array, required: true, default: () => { return []; } }, isCheckable: { type: Boolean }, hideGuideLines: { type: Boolean, default: false }, onDropValidator: { type: Function }, treeState: { type: Object }, checkboxStyle: { type: String }, lazyLoad: { type: Boolean } }, components: { 'treeview-item': script$1 }, emits: ['onContextMenu', 'onSelect', 'onCheck', 'onExpand', 'onCollapse'], setup(props, { emit, attrs }) { const reactiveItems = ref([]); watch(() => props.items, () => reactiveItems.value = props.items, { immediate: true }); const parent = computed(() => attrs.parent); const internalItems = computed(() => reactiveItems.value.map(item => item)); const treeState = ref(); // Create a tree state object for only root nodes. treeState.value = inject(_TREE_STATE_PROVIDER_INJECT_KEY, undefined); if (!treeState.value) { treeState.value = useGraph(reactiveItems, selectedItem => emit('onSelect', selectedItem), checkedItems => emit('onCheck', checkedItems), expandedItem => emit('onExpand', expandedItem), collapsedItem => emit('onCollapse', collapsedItem)); provide(_TREE_STATE_PROVIDER_INJECT_KEY, treeState.value); } return { ...useTreeViewItemMouseActions(), parent, internalItems, treeState }; } }); const _hoisted_1 = { class: "vue3-tree-vue" }; const _hoisted_2 = ["draggable"]; const _hoisted_3 = ["id"]; const _hoisted_4 = { style: { "list-style": "none" } }; function render(_ctx, _cache, $props, $setup, $data, $options) { const _component_treeview_item = resolveComponent("treeview-item"); const _component_tree_view = resolveComponent("tree-view"); return openBlock(), createElementBlock("div", _hoisted_1, [createElementVNode("ul", { id: "explorer", class: normalizeClass(["explorer tree-item-node-parent", { 'no-guide': _ctx.hideGuideLines }]), draggable: _ctx.onDropValidator != undefined, onDragover: [_cache[3] || (_cache[3] = withModifiers(() => {}, ["stop", "prevent"])), _cache[5] || (_cache[5] = withModifiers($event => _ctx.addRootHoverClass($event, _ctx.parent == null), ["stop"]))], onDragenter: _cache[4] || (_cache[4] = withModifiers(() => {}, ["stop", "prevent"])), onDragleave: _cache[6] || (_cache[6] = withModifiers($event => _ctx.removeRootHoverClass($event, _ctx.parent == null), ["stop"])), onDrop: _cache[7] || (_cache[7] = withModifiers($event => { _ctx.onDropNode(undefined, $event, _ctx.onDropValidator, _ctx.treeState); _ctx.removeRootHoverClass($event, _ctx.parent == null); }, ["prevent", "stop"])) }, [(openBlock(true), createElementBlock(Fragment, null, renderList(_ctx.internalItems, treeViewItem => { return openBlock(), createElementBlock("li", { key: treeViewItem.id, id: treeViewItem.id, class: "tree-item-node" }, [createVNode(_component_treeview_item, { class: "pointer tree-view-item", item: treeViewItem, parent: _ctx.parent, hideGuideLines: _ctx.hideGuideLines, onDragover: [_cache[0] || (_cache[0] = withModifiers(() => {}, ["stop", "prevent"])), withModifiers(_ctx.addHoverClass, ["stop"])], onDragenter: _cache[1] || (_cache[1] = withModifiers(() => {}, ["stop", "prevent"])), draggable: _ctx.onDropValidator != undefined, onDragstart: withModifiers($event => _ctx.onDragNode(treeViewItem, $event), ["stop"]), onDragleave: withModifiers(_ctx.removeHoverClass, ["stop"]), onDrop: withModifiers($event => _ctx.onDropNode(treeViewItem, $event, _ctx.onDropValidator, _ctx.treeState), ["prevent", "stop"]), checkboxStyle: _ctx.checkboxStyle, isCheckable: _ctx.isCheckable, treeState: _ctx.treeState, lazyLoad: _ctx.lazyLoad, onContextmenu: withModifiers($event => _ctx.$emit('onContextMenu', { item: treeViewItem, event: $event }), ["prevent"]) }, { icon: withCtx(() => [renderSlot(_ctx.$slots, "item-prepend-icon", mergeProps({ ref_for: true }, treeViewItem))]), prepend: withCtx(() => [renderSlot(_ctx.$slots, "item-prepend", mergeProps({ ref_for: true }, treeViewItem))]), expander: withCtx(() => [renderSlot(_ctx.$slots, "item-expander", mergeProps({ ref_for: true }, treeViewItem))]), name: withCtx(() => [renderSlot(_ctx.$slots, "item-name", mergeProps({ ref_for: true }, treeViewItem))]), append: withCtx(() => [renderSlot(_ctx.$slots, "item-append", mergeProps({ ref_for: true }, treeViewItem))]), _: 2 }, 1032, ["item", "parent", "hideGuideLines", "draggable", "onDragstart", "onDragleave", "onDrop", "onDragover", "checkboxStyle", "isCheckable", "treeState", "lazyLoad", "onContextmenu"]), treeViewItem.children && treeViewItem.children.length > 0 ? (openBlock(), createElementBlock("div", { key: 0, class: normalizeClass(["node-child", { 'nested': _ctx.parent != null, 'root': _ctx.parent == undefined, 'hide': !treeViewItem.expanded, 'hide-guidelines': _ctx.hideGuideLines }]) }, [createVNode(_component_tree_view, { items: treeViewItem.children, hideGuideLines: _ctx.hideGuideLines, isNested: true, onDropValidator: _ctx.onDropValidator, lazyLoad: _ctx.lazyLoad, checkboxStyle: _ctx.checkboxStyle, parent: treeViewItem, isCheckable: _ctx.isCheckable, onOnContextMenu: _cache[2] || (_cache[2] = $event => _ctx.$emit('onContextMenu', $event)) }, createSlots({ _: 2 }, [renderList(_ctx.$slots, (_, slot) => { return { name: slot, fn: withCtx(props => [renderSlot(_ctx.$slots, slot, mergeProps({ ref_for: true }, props))]) }; })]), 1032, ["items", "hideGuideLines", "onDropValidator", "lazyLoad", "checkboxStyle", "parent", "isCheckable"])], 2)) : createCommentVNode("", true)], 8, _hoisted_3); }), 128))], 42, _hoisted_2), createElementVNode("li", _hoisted_4, [_ctx.parent ? renderSlot(_ctx.$slots, "child-append", normalizeProps(mergeProps({ key: 0 }, _ctx.parent))) : createCommentVNode("", true)])]); } script.render = render; var entry_esm = (() => { const installable = script; installable.install = app => { app.component('vue3-tree-vue', installable); }; return installable; })(); export { entry_esm as default };