@nextcloud/vue
Version:
Nextcloud vue components
496 lines (495 loc) • 15.5 kB
JavaScript
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