@gitlab/ui
Version:
GitLab UI Components
250 lines (235 loc) • 8.5 kB
JavaScript
import debounce from 'lodash/debounce';
import { translate } from '../../../utils/i18n';
import GlAvatar from '../avatar/avatar';
import GlDisclosureDropdown from '../new_dropdowns/disclosure/disclosure_dropdown';
import { GlTooltipDirective } from '../../../directives/tooltip/tooltip';
import { breadCrumbSizeOptions } from '../../../utils/constants';
import GlBreadcrumbItem from './breadcrumb_item';
import __vue_normalize__ from 'vue-runtime-helpers/dist/normalize-component.js';
//
var script = {
name: 'GlBreadcrumb',
components: {
GlBreadcrumbItem,
GlAvatar,
GlDisclosureDropdown
},
directives: {
GlTooltip: GlTooltipDirective
},
inheritAttrs: false,
props: {
/**
* The breadcrumb items to be displayed as links.
*/
items: {
type: Array,
required: true,
default: () => [{
text: '',
href: ''
}],
validator: items => {
return items.every(item => {
const keys = Object.keys(item);
return keys.includes('text') && (keys.includes('href') || keys.includes('to'));
});
}
},
ariaLabel: {
type: String,
required: false,
default: 'Breadcrumb'
},
/**
* The label for the collapsed dropdown toggle. Screen-reader only.
*/
showMoreLabel: {
type: String,
required: false,
default: () => translate('GlBreadcrumb.showMoreLabel', 'Show more breadcrumbs')
},
/**
* Allows to disable auto-resize behavior. Items will then overflow their container instead of being collapsed into a dropdown.
*/
autoResize: {
type: Boolean,
required: false,
default: true
},
/**
* Size of the breadcrumb item. Use `sm` for page breadcrumbs.
*/
size: {
type: String,
required: false,
default: breadCrumbSizeOptions.sm,
validator: value => Object.keys(breadCrumbSizeOptions).includes(value)
}
},
data() {
return {
fittingItems: [...this.items],
// array of items that fit on the screen
overflowingItems: [],
// array of items that didn't fit and were put in a dropdown instead
totalBreadcrumbsWidth: 0,
// the total width of all breadcrumb items combined
widthPerItem: [],
// array with the individual widths of each breadcrumb item
resizeDone: false // to apply some CSS only during/after resizing
};
},
computed: {
hasCollapsible() {
return this.overflowingItems.length > 0;
},
breadcrumbStyle() {
return this.resizeDone ? {} : {
opacity: 0
};
},
itemStyle() {
/**
* If the last/only item, which is always visible, has a very long title,
* it could overflow the breadcrumb component. This CSS makes sure it
* shows an ellipsis instead.
* But this CSS cannot be active while we do the size calculation, as that
* would then not take the real unshrunk width of that item into account.
*/
if (this.resizeDone && this.fittingItems.length === 1) {
return {
'flex-shrink': 1,
'text-overflow': 'ellipsis',
'overflow-x': 'hidden',
'text-wrap': 'nowrap'
};
}
return {};
},
dropdownSize() {
return this.size === 'sm' ? 'small' : 'medium';
},
avatarSize() {
return this.size === 'sm' ? 16 : 24;
}
},
watch: {
items: {
handler: 'measureAndMakeBreadcrumbsFit',
deep: true
},
autoResize(newValue) {
if (newValue) this.enableAutoResize();else this.disableAutoResize();
}
},
created() {
this.debounceMakeBreadcrumbsFit = debounce(this.makeBreadcrumbsFit, 25);
},
mounted() {
if (this.autoResize) {
this.enableAutoResize();
} else {
this.resizeDone = true;
}
},
beforeDestroy() {
this.disableAutoResize();
},
methods: {
resetItems() {
this.fittingItems = [...this.items];
this.overflowingItems = [];
},
async measureAndMakeBreadcrumbsFit() {
this.resetItems();
if (!this.autoResize) return;
this.resizeDone = false;
// Wait for DOM update so all items get rendered and can be measured.
await this.$nextTick();
this.totalBreadcrumbsWidth = 0;
if (!this.$refs.breadcrumbs) return;
this.$refs.breadcrumbs.forEach((b, index) => {
const width = b.$el.clientWidth;
this.totalBreadcrumbsWidth += width;
this.widthPerItem[index] = width;
});
this.makeBreadcrumbsFit();
},
makeBreadcrumbsFit() {
this.resizeDone = false;
this.resetItems();
const containerWidth = this.$el.clientWidth;
const buttonWidth = 40; // px
if (this.totalBreadcrumbsWidth > containerWidth) {
// Not all breadcrumb items fit. Start moving items over to the dropdown.
const startSlicingAt = 0;
// The last item will never be moved into the dropdown.
const stopSlicingAt = this.items.length - 1;
let widthNeeded = this.totalBreadcrumbsWidth;
for (let index = startSlicingAt; index < stopSlicingAt; index += 1) {
// Move one breadcrumb item into the dropdown
this.overflowingItems.push(this.fittingItems[startSlicingAt]);
this.fittingItems.splice(startSlicingAt, 1);
widthNeeded -= this.widthPerItem[index];
// Does it fit now? Then stop.
if (widthNeeded + buttonWidth < containerWidth) break;
}
}
this.resizeDone = true;
},
isLastItem(index) {
return index === this.fittingItems.length - 1;
},
getAriaCurrentAttr(index) {
return this.isLastItem(index) ? 'page' : false;
},
enableAutoResize() {
this.resizeObserver || (this.resizeObserver = new ResizeObserver(this.debounceMakeBreadcrumbsFit));
this.resizeObserver.observe(this.$el);
this.measureAndMakeBreadcrumbsFit();
},
disableAutoResize() {
if (this.resizeObserver) {
this.resizeObserver.unobserve(this.$el);
this.resizeObserver = null;
}
this.resetItems();
},
hideItemClass(item) {
// TODO once https://gitlab.com/gitlab-org/gitlab/-/issues/520089 is addressed:
// - Remove this hiding of empty breadcrumbs.
// - Tighten `items` validator to require non-empty `text`.
return !item.text ? 'gl-hidden' : '';
}
}
};
/* script */
const __vue_script__ = script;
/* template */
var __vue_render__ = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('nav',{staticClass:"gl-breadcrumbs",style:(_vm.breadcrumbStyle),attrs:{"aria-label":_vm.ariaLabel}},[_c('ol',_vm._g(_vm._b({staticClass:"gl-breadcrumb-list breadcrumb"},'ol',_vm.$attrs,false),_vm.$listeners),[(_vm.hasCollapsible)?_c('li',{class:("gl-breadcrumb-item gl-breadcrumb-item-" + _vm.size)},[_c('gl-disclosure-dropdown',{attrs:{"items":_vm.overflowingItems,"toggle-text":_vm.showMoreLabel,"fluid-width":"","text-sr-only":"","no-caret":"","icon":"ellipsis_h","size":_vm.dropdownSize}})],1):_vm._e(),_vm._v(" "),_vm._l((_vm.fittingItems),function(item,index){return _c('gl-breadcrumb-item',{key:index,ref:"breadcrumbs",refInFor:true,class:_vm.hideItemClass(item),style:(_vm.itemStyle),attrs:{"text":item.text,"href":item.href,"to":item.to,"size":_vm.size,"aria-current":_vm.getAriaCurrentAttr(index)}},[(item.avatarPath)?_c('gl-avatar',{staticClass:"gl-breadcrumb-avatar-tile gl-border gl-mr-2 !gl-rounded-base",attrs:{"src":item.avatarPath,"size":_vm.avatarSize,"aria-hidden":"true","shape":"rect","data-testid":"avatar"}}):_vm._e(),_c('span',{staticClass:"gl-align-middle"},[_vm._v(_vm._s(item.text))])],1)})],2)])};
var __vue_staticRenderFns__ = [];
/* style */
const __vue_inject_styles__ = undefined;
/* scoped */
const __vue_scope_id__ = undefined;
/* module identifier */
const __vue_module_identifier__ = undefined;
/* functional template */
const __vue_is_functional_template__ = false;
/* style inject */
/* style inject SSR */
/* style inject shadow dom */
const __vue_component__ = /*#__PURE__*/__vue_normalize__(
{ render: __vue_render__, staticRenderFns: __vue_staticRenderFns__ },
__vue_inject_styles__,
__vue_script__,
__vue_scope_id__,
__vue_is_functional_template__,
__vue_module_identifier__,
false,
undefined,
undefined,
undefined
);
export { __vue_component__ as default };