UNPKG

@nextcloud/vue

Version:
496 lines (495 loc) 15.5 kB
import '../assets/NcBreadcrumbs-DahxPGJY.css'; import { unsubscribe, subscribe } from "@nextcloud/event-bus"; import debounce from "debounce"; import Vue from "vue"; import { Fragment } from "vue-frag"; import { n as normalizeComponent } from "./_plugin-vue2_normalizer-DU4iP6Vu.mjs"; import { N as NcActionButton } from "./NcActionButton-CVW8aRkE.mjs"; import NcActionLink from "../Components/NcActionLink.mjs"; import NcActionRouter from "../Components/NcActionRouter.mjs"; import { N as NcActions } from "./NcActions-C832pWHO.mjs"; import { N as NcBreadcrumb } from "./NcBreadcrumb-D1106x4x.mjs"; const _sfc_main$1 = { name: "FolderIcon", emits: ["click"], props: { title: { type: String }, fillColor: { type: String, default: "currentColor" }, size: { type: Number, default: 24 } } }; var _sfc_render$1 = function render() { var _vm = this, _c = _vm._self._c; return _c("span", _vm._b({ staticClass: "material-design-icon folder-icon", attrs: { "aria-hidden": _vm.title ? null : "true", "aria-label": _vm.title, "role": "img" }, on: { "click": function($event) { return _vm.$emit("click", $event); } } }, "span", _vm.$attrs, false), [_c("svg", { staticClass: "material-design-icon__svg", attrs: { "fill": _vm.fillColor, "width": _vm.size, "height": _vm.size, "viewBox": "0 0 24 24" } }, [_c("path", { attrs: { "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" } }, [_vm.title ? _c("title", [_vm._v(_vm._s(_vm.title))]) : _vm._e()])])]); }; var _sfc_staticRenderFns$1 = []; var __component__$1 = /* @__PURE__ */ normalizeComponent( _sfc_main$1, _sfc_render$1, _sfc_staticRenderFns$1, false, null, null ); const IconFolder = __component__$1.exports; function ValidateSlot(slots, allowed, vm) { if (slots === void 0) { return; } for (let index = slots.length - 1; index >= 0; index--) { const node = slots[index]; const isHtmlElement = !node.componentOptions && node.tag && allowed.indexOf(node.tag) === -1; const isVueComponent = !!node.componentOptions && typeof node.componentOptions.tag === "string"; const isForbiddenComponent = isVueComponent && allowed.indexOf(node.componentOptions.tag) === -1; if (isHtmlElement || !isVueComponent || isForbiddenComponent) { if (isHtmlElement || isForbiddenComponent) { Vue.util.warn(`${isHtmlElement ? node.tag : node.componentOptions.tag} is not allowed inside the ${vm.$options.name} component`, vm); } slots.splice(index, 1); } } } 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: {} }; }, beforeMount() { ValidateSlot(this.$slots.default, ["NcBreadcrumb"], this); }, beforeUpdate() { ValidateSlot(this.$slots.default, ["NcBreadcrumb"], this); }, created() { window.addEventListener("resize", debounce(() => { this.handleWindowResize(); }, 100)); subscribe("navigation-toggled", this.delayedResize); }, mounted() { this.handleWindowResize(); }, updated() { this.delayedResize(); this.$nextTick(() => { this.hideCrumbs(); }); }, beforeDestroy() { 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 breadcrumbs = Object.values(this.breadcrumbsRefs); const nrCrumbs = breadcrumbs.length; const hiddenIndices = []; const availableWidth = this.$refs.container.offsetWidth; let totalWidth = this.getTotalWidth(breadcrumbs); 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(breadcrumbs[currentIndex]?.elm, currentIndex === breadcrumbs.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; } if (a === b) { return true; } 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 * * @param {Array} breadcrumbs All breadcrumbs * @return {number} The total width */ getTotalWidth(breadcrumbs) { return breadcrumbs.reduce((width, crumb, index) => width + this.getWidth(crumb?.elm, index === breadcrumbs.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}`); crumbs.forEach((f) => { f.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}`); crumbs.forEach((f) => { f.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() { const crumbs = Object.values(this.breadcrumbsRefs); crumbs.forEach((crumb, i) => { if (crumb?.elm?.classList) { if (this.hiddenIndices.includes(i)) { crumb.elm.classList.add(`${crumbClass}--hidden`); } else { crumb.elm.classList.remove(`${crumbClass}--hidden`); } } }); }, isBreadcrumb(vnode) { return (vnode?.componentOptions?.tag || vnode?.tag || "").includes("NcBreadcrumb"); } }, /** * The render function to display the component * * @param {Function} h The function to create VNodes * @return {object|undefined} The created VNode */ render(h) { const 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; } Vue.set(breadcrumbs[0].componentOptions.propsData, "icon", this.rootIcon); Vue.set(breadcrumbs[0].componentOptions.propsData, "ref", "breadcrumbs"); const breadcrumbsRefs = {}; breadcrumbs.forEach((crumb, index) => { Vue.set(crumb, "ref", `crumb-${index}`); breadcrumbsRefs[index] = crumb; }); let crumbs = []; if (!this.hiddenIndices.length) { crumbs = breadcrumbs; } else { crumbs = breadcrumbs.slice(0, Math.round(breadcrumbs.length / 2)); crumbs.push(h("NcBreadcrumb", { class: "dropdown", props: this.menuBreadcrumbProps, attrs: { // 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 nativeOn: { dragstart: this.dragStart, dragenter: () => { this.menuBreadcrumbProps.open = true; }, dragleave: this.closeActions }, on: { // Make sure we keep the same open state // as the Actions component "update:open": (open) => { this.menuBreadcrumbProps.open = open; } } // Add all hidden breadcrumbs as ActionRouter or ActionLink }, this.hiddenIndices.filter((index) => index <= breadcrumbs.length - 1).map((index) => { const crumb = breadcrumbs[index]; const to = crumb.componentOptions.propsData.to; const href = crumb.componentOptions.propsData.href; const disabled = crumb.componentOptions.propsData.disableDrop; const title = crumb.componentOptions.propsData.title; const name = crumb.componentOptions.propsData.name; let element = "NcActionButton"; let path = ""; if (href) { element = "NcActionLink"; path = href; } if (to) { element = "NcActionRouter"; path = to; } const folderIcon = h("IconFolder", { props: { size: 20 }, slot: "icon" }); return h(element, { class: crumbClass, props: { href: href || null, title, to: to || null }, // Prevent the breadcrumbs from being draggable attrs: { draggable: false }, on: { ...crumb.componentOptions.listeners }, // Add the drag and drop handlers nativeOn: { dragstart: this.dragStart, drop: ($event) => this.dropped($event, path, disabled), dragover: this.dragOver, dragenter: ($event) => this.dragEnter($event, disabled), dragleave: ($event) => this.dragLeave($event, disabled) } }, [folderIcon, name]); }))); const crumbs2 = breadcrumbs.slice(Math.round(breadcrumbs.length / 2)); crumbs = crumbs.concat(crumbs2); } const wrapper = [h("nav", { attrs: { "aria-label": this.ariaLabel } }, [h("ul", { class: "breadcrumb__crumbs" }, [crumbs])])]; if (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 _sfc_render = null; const _sfc_staticRenderFns = null; var __component__ = /* @__PURE__ */ normalizeComponent( _sfc_main, _sfc_render, _sfc_staticRenderFns, false, null, "daf14f2f" ); const NcBreadcrumbs = __component__.exports; export { NcBreadcrumbs as N }; //# sourceMappingURL=NcBreadcrumbs-C9Zo0nca.mjs.map