UNPKG

@nextcloud/vue

Version:
462 lines (461 loc) 14.5 kB
import '../assets/NcBreadcrumbs-DYfGaSjT.css'; import { unsubscribe, subscribe } from "@nextcloud/event-bus"; import debounce from "debounce"; import { createElementBlock, openBlock, mergeProps, createElementVNode, createCommentVNode, toDisplayString, Fragment, cloneVNode, h } from "vue"; import { _ as _export_sfc } from "./_plugin-vue_export-helper-1tPrXgE0.mjs"; import { N as NcActions, i as isSlotPopulated } from "./NcActions-DWmvh7-Y.mjs"; import { N as NcActionButton } from "./NcActionButton-pKOSrlGE.mjs"; import { N as NcActionLink } from "./NcActionLink-vEvKSV4N.mjs"; import { N as NcActionRouter } from "./NcActionRouter-oT-YU_jf.mjs"; import { N as NcBreadcrumb } from "./NcBreadcrumb-Bwkn3eve.mjs"; const _sfc_main$1 = { name: "FolderIcon", emits: ["click"], props: { title: { type: String }, fillColor: { type: String, default: "currentColor" }, size: { type: Number, default: 24 } } }; const _hoisted_1 = ["aria-hidden", "aria-label"]; const _hoisted_2 = ["fill", "width", "height"]; const _hoisted_3 = { d: "M10,4H4C2.89,4 2,4.89 2,6V18A2,2 0 0,0 4,20H20A2,2 0 0,0 22,18V8C22,6.89 21.1,6 20,6H12L10,4Z" }; const _hoisted_4 = { key: 0 }; function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) { return openBlock(), createElementBlock("span", mergeProps(_ctx.$attrs, { "aria-hidden": $props.title ? null : "true", "aria-label": $props.title, class: "material-design-icon folder-icon", role: "img", onClick: _cache[0] || (_cache[0] = ($event) => _ctx.$emit("click", $event)) }), [ (openBlock(), createElementBlock("svg", { fill: $props.fillColor, class: "material-design-icon__svg", width: $props.size, height: $props.size, viewBox: "0 0 24 24" }, [ createElementVNode("path", _hoisted_3, [ $props.title ? (openBlock(), createElementBlock("title", _hoisted_4, toDisplayString($props.title), 1)) : createCommentVNode("", true) ]) ], 8, _hoisted_2)) ], 16, _hoisted_1); } const IconFolder = /* @__PURE__ */ _export_sfc(_sfc_main$1, [["render", _sfc_render]]); const crumbClass = "vue-crumb"; const _sfc_main = { name: "NcBreadcrumbs", components: { NcActions, NcActionButton, NcActionRouter, NcActionLink, NcBreadcrumb, IconFolder }, props: { /** * Set a css icon-class for the icon of the root breadcrumb to be used. */ rootIcon: { type: String, default: "icon-home" }, /** * Set the aria-label of the nav element. */ ariaLabel: { type: String, default: null } }, emits: ["dropped"], data() { return { /** * Array to track the hidden breadcrumbs by their index. * Comparing two crumbs somehow does not work, so we use the indices. */ hiddenIndices: [], /** * This is the props of the middle Action menu * that show the ellipsised breadcrumbs */ menuBreadcrumbProps: { // Don't show a name for this breadcrumb, only the Actions menu name: "", forceMenu: true, // Don't allow dropping directly on the actions breadcrumb disableDrop: true, // Is the menu open or not open: false }, breadcrumbsRefs: [] }; }, created() { window.addEventListener("resize", debounce(() => { this.handleWindowResize(); }, 100)); subscribe("navigation-toggled", this.delayedResize); }, mounted() { this.handleWindowResize(); }, updated() { this.delayedResize(); this.$nextTick(() => { this.hideCrumbs(); }); }, beforeUnmount() { window.removeEventListener("resize", this.handleWindowResize); unsubscribe("navigation-toggled", this.delayedResize); }, methods: { /** * Close the actions menu * * @param {object} e The event */ closeActions(e) { if (this.$refs.actionsBreadcrumb.$el.contains(e.relatedTarget)) { return; } this.menuBreadcrumbProps.open = false; }, /** * Call the resize function after a delay */ async delayedResize() { await this.$nextTick(); this.handleWindowResize(); }, /** * Check the width of the breadcrumb and hide breadcrumbs * if we overflow otherwise. */ handleWindowResize() { if (!this.$refs.container) { return; } const nrCrumbs = this.breadcrumbsRefs.length; const hiddenIndices = []; const availableWidth = this.$refs.container.offsetWidth; let totalWidth = this.getTotalWidth(); if (this.$refs.breadcrumb__actions) { totalWidth += this.$refs.breadcrumb__actions.offsetWidth; } let overflow = totalWidth - availableWidth; overflow += overflow > 0 ? 64 : 0; let i = 0; const startIndex = Math.floor(nrCrumbs / 2); while (overflow > 0 && i < nrCrumbs - 2) { const currentIndex = startIndex + (i % 2 ? i + 1 : i) / 2 * Math.pow(-1, i + nrCrumbs % 2); overflow -= this.getWidth(this.breadcrumbsRefs[currentIndex]?.$el, currentIndex === this.breadcrumbsRefs.length - 1); hiddenIndices.push(currentIndex); i++; } if (!this.arraysEqual(this.hiddenIndices, hiddenIndices.sort((a, b) => a - b))) { this.hiddenIndices = hiddenIndices; } }, /** * Checks if two arrays are equal. * Only works for primitive arrays, but that's enough here. * * @param {Array} a The first array * @param {Array} b The second array * @return {boolean} Wether the arrays are equal */ arraysEqual(a, b) { if (a.length !== b.length) { return false; } else if (a === b) { return true; } else if (a === null || b === null) { return false; } for (let i = 0; i < a.length; ++i) { if (a[i] !== b[i]) { return false; } } return true; }, /** * Calculates the total width of all breadcrumbs * * @return {number} The total width */ getTotalWidth() { return this.breadcrumbsRefs.reduce((width, crumb, index) => width + this.getWidth(crumb.$el, index === this.breadcrumbsRefs.length - 1), 0); }, /** * Calculates the width of the provided element * * @param {object} el The element * @param {boolean} isLast Is this the last crumb * @return {number} The width */ getWidth(el, isLast) { if (!el?.classList) { return 0; } const hide = el.classList.contains(`${crumbClass}--hidden`); el.style.minWidth = "auto"; if (isLast) { el.style.maxWidth = "210px"; } el.classList.remove(`${crumbClass}--hidden`); const w = el.offsetWidth; if (hide) { el.classList.add(`${crumbClass}--hidden`); } el.style.minWidth = ""; el.style.maxWidth = ""; return w; }, /** * Prevents the default of a provided event * * @param {object} e The event * @return {boolean} */ preventDefault(e) { if (e.preventDefault) { e.preventDefault(); } return false; }, /** * Handles the drag start. * Prevents a breadcrumb from being draggable. * * @param {object} e The event * @return {boolean} */ dragStart(e) { return this.preventDefault(e); }, /** * Handles when something is dropped on the breadcrumb. * * @param {object} e The drop event * @param {string} path The path of the breadcrumb * @param {boolean} disabled Whether dropping is disabled for this breadcrumb * @return {boolean} */ dropped(e, path, disabled) { if (!disabled) { this.$emit("dropped", e, path); } this.menuBreadcrumbProps.open = false; const crumbs = document.querySelectorAll(`.${crumbClass}`); for (const crumb of crumbs) { crumb.classList.remove(`${crumbClass}--hovered`); } return this.preventDefault(e); }, /** * Handles the drag over event * * @param {object} e The drag over event * @return {boolean} */ dragOver(e) { return this.preventDefault(e); }, /** * Handles the drag enter event * * @param {object} e The drag over event * @param {boolean} disabled Whether dropping is disabled for this breadcrumb */ dragEnter(e, disabled) { if (disabled) { return; } if (e.target.closest) { const target = e.target.closest(`.${crumbClass}`); if (target.classList && target.classList.contains(crumbClass)) { const crumbs = document.querySelectorAll(`.${crumbClass}`); for (const crumb of crumbs) { crumb.classList.remove(`${crumbClass}--hovered`); } target.classList.add(`${crumbClass}--hovered`); } } }, /** * Handles the drag leave event * * @param {object} e The drag leave event * @param {boolean} disabled Whether dropping is disabled for this breadcrumb */ dragLeave(e, disabled) { if (disabled) { return; } if (e.target.contains(e.relatedTarget)) { return; } if (e.target.closest) { const target = e.target.closest(`.${crumbClass}`); if (target.contains(e.relatedTarget)) { return; } if (target.classList && target.classList.contains(crumbClass)) { target.classList.remove(`${crumbClass}--hovered`); } } }, /** * Check for each crumb if we have to hide it and * add it to the array of all crumbs. */ hideCrumbs() { this.breadcrumbsRefs.forEach((crumb, i) => { if (crumb?.$el?.classList) { if (this.hiddenIndices.includes(i)) { crumb.$el.classList.add(`${crumbClass}--hidden`); } else { crumb.$el.classList.remove(`${crumbClass}--hidden`); } } }); }, isBreadcrumb(vnode) { return vnode?.type?.name === "NcBreadcrumb"; } }, /** * The render function to display the component * * @return {object|undefined} The created VNode */ render() { let breadcrumbs = []; this.$slots.default?.().forEach((vnode) => { if (this.isBreadcrumb(vnode)) { breadcrumbs.push(vnode); return; } if (vnode?.type === Fragment) { vnode?.children?.forEach?.((child) => { if (this.isBreadcrumb(child)) { breadcrumbs.push(child); } }); } }); if (breadcrumbs.length === 0) { return; } breadcrumbs[0] = cloneVNode(breadcrumbs[0], { icon: this.rootIcon, ref: "breadcrumbs" }); const breadcrumbsRefs = []; breadcrumbs = breadcrumbs.map((crumb, index) => cloneVNode(crumb, { ref: (crumb2) => { breadcrumbsRefs[index] = crumb2; } })); const crumbs = [...breadcrumbs]; if (this.hiddenIndices.length) { crumbs.splice( Math.round(breadcrumbs.length / 2), 0, // The Actions menu // Use a breadcrumb component for the hidden breadcrumbs // eslint-disable-line @stylistic/function-call-argument-newline h(NcBreadcrumb, { class: "dropdown", ...this.menuBreadcrumbProps, // Hide the dropdown menu from screen-readers, // since the crumbs in the menu are still in the list. "aria-hidden": true, // Add a ref to the Actions menu ref: "actionsBreadcrumb", key: "actions-breadcrumb-1", // Add handlers so the Actions menu opens on hover onDragenter: () => { this.menuBreadcrumbProps.open = true; }, onDragleave: this.closeActions, // Make sure we keep the same open state // as the Actions component "onUpdate:open": (open) => { this.menuBreadcrumbProps.open = open; } // Add all hidden breadcrumbs as ActionRouter or ActionLink }, { default: () => this.hiddenIndices.filter((index) => index <= breadcrumbs.length - 1).map((index) => { const crumb = breadcrumbs[index]; const { // Get the parameters from the breadcrumb component props to, href, disableDrop, name, // Props to forward ...propsToForward } = crumb.props; delete propsToForward.ref; let element = NcActionButton; let path = ""; if (href) { element = NcActionLink; path = href; } if (to) { element = NcActionRouter; path = to; } const folderIcon = h(IconFolder, { size: 20 }); return h(element, { ...propsToForward, class: crumbClass, href: href || null, to: to || null, // Prevent the breadcrumbs from being draggable draggable: false, // Add the drag and drop handlers onDragstart: this.dragStart, onDrop: ($event) => this.dropped($event, path, disableDrop), onDragover: this.dragOver, onDragenter: ($event) => this.dragEnter($event, disableDrop), onDragleave: ($event) => this.dragLeave($event, disableDrop) }, { default: () => name, icon: () => folderIcon }); }) }) ); } const wrapper = [h("nav", { "aria-label": this.ariaLabel }, [h("ul", { class: "breadcrumb__crumbs" }, [crumbs])])]; if (isSlotPopulated(this.$slots.actions?.())) { wrapper.push(h("div", { class: "breadcrumb__actions", ref: "breadcrumb__actions" }, this.$slots.actions?.())); } this.breadcrumbsRefs = breadcrumbsRefs; return h("div", { class: ["breadcrumb", { "breadcrumb--collapsed": this.hiddenIndices.length === breadcrumbs.length - 2 }], ref: "container" }, wrapper); } }; const NcBreadcrumbs = /* @__PURE__ */ _export_sfc(_sfc_main, [["__scopeId", "data-v-af2b1226"]]); export { NcBreadcrumbs as N }; //# sourceMappingURL=NcBreadcrumbs-PN5_hHQn.mjs.map