Skip to content

Commit

Permalink
fix: adjust breadcrumbs component
Browse files Browse the repository at this point in the history
Signed-off-by: Raimund Schlüßler <raimund.schluessler@mailbox.org>
  • Loading branch information
raimund-schluessler committed Aug 14, 2023
1 parent b1aa2a9 commit 5273f33
Show file tree
Hide file tree
Showing 3 changed files with 115 additions and 63 deletions.
12 changes: 12 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Expand Up @@ -86,6 +86,7 @@
"unist-util-visit": "^5.0.0",
"vue": "^2.7.14",
"vue-color": "^2.8.1",
"vue-frag": "^1.4.3",
"vue-material-design-icons": "^5.1.2",
"vue2-datepicker": "^3.11.0"
},
Expand Down
165 changes: 102 additions & 63 deletions src/components/NcBreadcrumbs/NcBreadcrumbs.vue
Expand Up @@ -149,6 +149,7 @@ import IconFolder from 'vue-material-design-icons/Folder.vue'
import debounce from 'debounce'
import Vue from 'vue'
import { Fragment } from 'vue-frag'
const crumbClass = 'vue-crumb'
Expand Down Expand Up @@ -196,6 +197,7 @@ export default {
// Is the menu open or not
open: false,
},
breadcrumbsRefs: {},
}
},
beforeMount() {
Expand Down Expand Up @@ -226,21 +228,21 @@ export default {
/**
* Check that crumbs to hide are hidden
*/
this.delayedHideCrumbs()
this.$nextTick(() => {
this.hideCrumbs()
})
},
beforeDestroy() {
beforeUnmount() {
window.removeEventListener('resize', this.handleWindowResize)
unsubscribe('navigation-toggled', this.delayedResize)
},
methods: {
/**
* Check that all crumbs to hide are really hidden
* Call the resize function after a delay
*/
delayedHideCrumbs() {
this.$nextTick(() => {
const crumbs = this.$slots.default || []
this.hideCrumbs(crumbs)
})
async delayedResize() {
await this.$nextTick()
this.handleWindowResize()
},
/**
* Close the actions menu
Expand All @@ -254,53 +256,64 @@ export default {
}
this.menuBreadcrumbProps.open = false
},
/**
* Call the resize function after a delay
*/
delayedResize() {
this.$nextTick(() => {
this.handleWindowResize()
})
},
/**
* Check the width of the breadcrumb and hide breadcrumbs
* if we overflow otherwise.
*/
handleWindowResize() {
// All breadcrumb components passed into the default slot
const breadcrumbs = this.$slots.default || []
// If there is no container yet, we cannot determine its size
if (this.$refs.container) {
const nrCrumbs = breadcrumbs.length
const hiddenIndices = []
const availableWidth = this.$refs.container.offsetWidth
let totalWidth = this.getTotalWidth(breadcrumbs)
// If we have breadcumbs actions, we have to take their width into account too.
if (this.$refs.breadcrumb__actions) {
totalWidth += this.$refs.breadcrumb__actions.offsetWidth
}
let overflow = totalWidth - availableWidth
// If we overflow, we have to take the action-item width into account as well.
overflow += (overflow > 0) ? 64 : 0
let i = 0
// We start hiding the breadcrumb in the center
const startIndex = Math.floor(nrCrumbs / 2)
// Don't hide the first and last breadcrumb
while (overflow > 0 && i < nrCrumbs - 2) {
// We hide elements alternating to the left and right
const currentIndex = startIndex + ((i % 2) ? i + 1 : i) / 2 * Math.pow(-1, i + (nrCrumbs % 2))
// Calculate the remaining overflow width after hiding this breadcrumb
overflow -= this.getWidth(breadcrumbs[currentIndex].elm)
hiddenIndices.push(currentIndex)
i++
if (!this.$refs.container) {
return
}
// All breadcrumb components passed into the default slot
const breadcrumbs = Object.values(this.breadcrumbsRefs)
const breadcrumbsVnodes = []
// We have to iterate over all slot elements
this.$slots.default.forEach(vnode => {
if (this.isBreadcrumb(vnode)) {
breadcrumbsVnodes.push(vnode)
return
}
// We only update the hidden crumbs if they have changed,
// otherwise we will run into an infinite update loop.
if (!this.arraysEqual(this.hiddenIndices, hiddenIndices.sort((a, b) => a - b))) {
// Get all breadcrumbs based on the hidden indices
this.hiddenCrumbs = hiddenIndices.map((index) => { return breadcrumbs[index] })
this.hiddenIndices = hiddenIndices
// If we encounter a Fragment, we have to check its children too
if (vnode?.type === Fragment) {
vnode?.children?.forEach?.(child => {
if (this.isBreadcrumb(child)) {
breadcrumbsVnodes.push(child)
}
})
}
})
const nrCrumbs = breadcrumbs.length
const hiddenIndices = []
const availableWidth = this.$refs.container.offsetWidth
let totalWidth = this.getTotalWidth(breadcrumbs)
// If we have breadcumbs actions, we have to take their width into account too.
if (this.$refs.breadcrumb__actions) {
totalWidth += this.$refs.breadcrumb__actions.offsetWidth
}
let overflow = totalWidth - availableWidth
// If we overflow, we have to take the action-item width into account as well.
overflow += (overflow > 0) ? 64 : 0
let i = 0
// We start hiding the breadcrumb in the center
const startIndex = Math.floor(nrCrumbs / 2)
// Don't hide the first and last breadcrumb
while (overflow > 0 && i < nrCrumbs - 2) {
// We hide elements alternating to the left and right
const currentIndex = startIndex + ((i % 2) ? i + 1 : i) / 2 * Math.pow(-1, i + (nrCrumbs % 2))
// Calculate the remaining overflow width after hiding this breadcrumb
overflow -= this.getWidth(breadcrumbs[currentIndex]?.elm)
hiddenIndices.push(currentIndex)
i++
}
// We only update the hidden crumbs if they have changed,
// otherwise we will run into an infinite update loop.
if (!this.arraysEqual(this.hiddenIndices, hiddenIndices.sort((a, b) => a - b))) {
// Get all breadcrumbs based on the hidden indices
this.hiddenCrumbs = hiddenIndices.map((index) => { return breadcrumbsVnodes[index] })
this.hiddenIndices = hiddenIndices
}
},
/**
Expand Down Expand Up @@ -330,7 +343,7 @@ export default {
* @return {number} The total width
*/
getTotalWidth(breadcrumbs) {
return breadcrumbs.reduce((width, crumb, index) => width + this.getWidth(crumb.elm), 0)
return breadcrumbs.reduce((width, crumb, index) => width + this.getWidth(crumb?.elm), 0)
},
/**
* Calculates the width of the provided element
Expand All @@ -339,7 +352,7 @@ export default {
* @return {number} The width
*/
getWidth(el) {
if (!el.classList) return 0
if (!el?.classList) return 0
const hide = el.classList.contains(`${crumbClass}--hidden`)
el.style.minWidth = 'auto'
el.classList.remove(`${crumbClass}--hidden`)
Expand Down Expand Up @@ -464,21 +477,23 @@ export default {
/**
* Check for each crumb if we have to hide it and
* add it to the array of all crumbs.
*
* @param {Array} crumbs The array of the crumbs to hide
* @param {number} offset The offset of the indices of the provided crumbs array
*/
hideCrumbs(crumbs, offset = 0) {
hideCrumbs() {
const crumbs = Object.values(this.breadcrumbsRefs)
crumbs.forEach((crumb, i) => {
if (crumb?.elm?.classList) {
if (this.hiddenIndices.includes(i + offset)) {
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
Expand All @@ -488,7 +503,22 @@ export default {
*/
render(h) {
// Get the breadcrumbs
const breadcrumbs = this.$slots.default || []
const breadcrumbs = []
// We have to iterate over all slot elements
this.$slots.default.forEach(vnode => {
if (this.isBreadcrumb(vnode)) {
breadcrumbs.push(vnode)
return
}
// If we encounter a Fragment, we have to check its children too
if (vnode?.type === Fragment) {
vnode?.children?.forEach?.(child => {
if (this.isBreadcrumb(child)) {
breadcrumbs.push(child)
}
})
}
})
// Check that we have at least one breadcrumb
if (breadcrumbs.length === 0) {
Expand All @@ -498,23 +528,32 @@ export default {
// Add the root icon to the first breadcrumb
// eslint-disable-next-line import/no-named-as-default-member
Vue.set(breadcrumbs[0].componentOptions.propsData, 'icon', this.rootIcon)
Vue.set(breadcrumbs[0].componentOptions.propsData, 'ref', 'breadcrumbs')

Check warning on line 531 in src/components/NcBreadcrumbs/NcBreadcrumbs.vue

View workflow job for this annotation

GitHub Actions / eslint

Caution: `Vue` also has a named export `set`. Check if you meant to write `import {set} from 'vue'` instead
/**
* Use a proxy object to store breadcrumbs refs
* and don't write to this.breadcrumbsRefs directly
* to not trigger a myriad of re-renders.
*/
const breadcrumbsRefs = {}
// Add the breadcrumbs to the array of the created VNodes, check if hiding them is necessary.
breadcrumbs.forEach((crumb, index) => {
Vue.set(crumb, 'ref', `crumb-${index}`)

Check warning on line 541 in src/components/NcBreadcrumbs/NcBreadcrumbs.vue

View workflow job for this annotation

GitHub Actions / eslint

Caution: `Vue` also has a named export `set`. Check if you meant to write `import {set} from 'vue'` instead
breadcrumbsRefs[index] = crumb
})
// The array of all created VNodes
let crumbs = []
if (!this.hiddenCrumbs.length) {
// We don't hide any breadcrumbs.
crumbs = breadcrumbs
// But we need to check if some are already hidden, and show them.
this.hideCrumbs(crumbs)
} else {
/**
* We show the first half of the breadcrumbs before the Actions dropdown menu
* which shows the hidden breadcrumbs.
*/
// Add the breadcrumbs to the array of the created VNodes, check if hiding them is necessary.
crumbs = breadcrumbs.slice(0, Math.round(breadcrumbs.length / 2))
this.hideCrumbs(crumbs)
// The Actions menu
// Use a breadcrumb component for the hidden breadcrumbs
Expand Down Expand Up @@ -595,16 +634,16 @@ export default {
// The second half of the breadcrumbs
const crumbs2 = breadcrumbs.slice(Math.round(breadcrumbs.length / 2))
crumbs = crumbs.concat(crumbs2)
// One crumb is the Actions dropdown, so subtract one.
this.hideCrumbs(crumbs2, crumbs.length - 1)
}
const wrapper = [h('nav', {}, [h('ul', { class: 'breadcrumb__crumbs' }, crumbs)])]
const wrapper = [h('nav', {}, [h('ul', { class: 'breadcrumb__crumbs' }, [crumbs])])]
// Append the actions slot if it is populated
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.hiddenCrumbs.length === breadcrumbs.length - 2) }], ref: 'container' }, wrapper)
},
}
Expand All @@ -616,7 +655,7 @@ export default {
flex-grow: 1;
display: inline-flex;
&--collapsed .vue-crumb:last-child {
&--collapsed :deep(.vue-crumb:last-child) {
min-width: 100px;
flex-shrink: 1;
}
Expand Down

0 comments on commit 5273f33

Please sign in to comment.