@nextcloud/vue
Version:
Nextcloud vue components
462 lines (461 loc) • 14.5 kB
JavaScript
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