feat: nav parent - expand by default option

scarlett
NGPixel 4 weeks ago
parent 527b8e9f64
commit 2acd026c43
No known key found for this signature in database

@ -10,6 +10,11 @@ const navigationItem = {
icon: { type: 'string' },
target: { type: 'string' },
openInNewWindow: { type: 'boolean' },
expandByDefault: {
type: 'boolean',
description:
'Whether a link holding children is shown expanded on load. Meaningless on any other item.'
},
visibilityGroups: {
type: 'array',
items: { type: 'string' },

@ -2161,6 +2161,8 @@
"navEdit.clearItems": "Clear All Items",
"navEdit.editMenuItems": "Edit Menu Items",
"navEdit.emptyMenuText": "Click the Add button to add your first menu item.",
"navEdit.expandByDefault": "Expand by Default",
"navEdit.expandByDefaultHint": "Whether the submenu is already expanded when the page loads.",
"navEdit.groupsFailed": "Failed to load the list of groups. Per-group visibility cannot be changed.",
"navEdit.header": "Header",
"navEdit.icon": "Icon",

@ -18,6 +18,8 @@ export interface NavigationItem {
icon?: string
target?: string
openInNewWindow?: boolean
/** A link with children only: whether the sidebar shows its submenu already open. */
expandByDefault?: boolean
visibilityGroups?: string[]
children?: NavigationItem[]
}

@ -288,56 +288,81 @@
</w-item-section>
</w-item>
<w-separator class="my-2" inset />
<w-item>
<blueprint-icon icon="link" />
<w-item-section>
<w-item-label>{{ t(`navEdit.target`) }}</w-item-label>
<w-item-label caption>{{ t(`navEdit.targetHint`) }}</w-item-label>
</w-item-section>
<w-item-section>
<w-input
outlined
v-model="state.current.target"
dense
hide-bottom-space
:aria-label="t(`navEdit.target`)">
<template #append>
<!--
Beside the field rather than in place of it: a path someone knows is quicker
typed than browsed to, and an external URL has nothing to browse. Same shape as
the icon picker's button one row up, for the same reason -- both open a chooser
for the field they sit in.
-->
<w-btn
flat
dense
round
icon="la:folder-open"
color="primary"
:aria-label="t(`common.actions.browse`)"
@click="browseTarget">
<w-tooltip>{{ t('common.actions.browse') }}</w-tooltip>
</w-btn>
</template>
</w-input>
</w-item-section>
</w-item>
<w-separator class="my-2" inset />
<w-item tag="label">
<blueprint-icon icon="external-link" />
<w-item-section>
<w-item-label>{{ t(`navEdit.openInNewWindow`) }}</w-item-label>
<w-item-label caption>{{ t(`navEdit.openInNewWindowHint`) }}</w-item-label>
</w-item-section>
<w-item-section avatar>
<w-toggle
v-model="state.current.openInNewWindow"
color="primary"
checked-icon="la:check"
unchecked-icon="la:times"
:aria-label="t(`navEdit.openInNewWindow`)" />
</w-item-section>
</w-item>
<!--
A parent is a row that opens a submenu rather than a row that goes anywhere: the sidebar
renders it as an expansion item and never reads its target, so both fields below are
hidden rather than shown doing nothing. Hidden, not cleared -- unnesting the last child
turns the row back into an ordinary link, and it comes back with the address it had.
-->
<template v-if="currentIsParent">
<w-item tag="label">
<blueprint-icon icon="chevron-right" />
<w-item-section>
<w-item-label>{{ t(`navEdit.expandByDefault`) }}</w-item-label>
<w-item-label caption>{{ t(`navEdit.expandByDefaultHint`) }}</w-item-label>
</w-item-section>
<w-item-section avatar>
<w-toggle
v-model="state.current.expandByDefault"
color="primary"
checked-icon="la:check"
unchecked-icon="la:times"
:aria-label="t(`navEdit.expandByDefault`)" />
</w-item-section>
</w-item>
</template>
<template v-else>
<w-item>
<blueprint-icon icon="link" />
<w-item-section>
<w-item-label>{{ t(`navEdit.target`) }}</w-item-label>
<w-item-label caption>{{ t(`navEdit.targetHint`) }}</w-item-label>
</w-item-section>
<w-item-section>
<w-input
outlined
v-model="state.current.target"
dense
hide-bottom-space
:aria-label="t(`navEdit.target`)">
<template #append>
<!--
Beside the field rather than in place of it: a path someone knows is quicker
typed than browsed to, and an external URL has nothing to browse. Same shape as
the icon picker's button one row up, for the same reason -- both open a chooser
for the field they sit in.
-->
<w-btn
flat
dense
round
icon="la:folder-open"
color="primary"
:aria-label="t(`common.actions.browse`)"
@click="browseTarget">
<w-tooltip>{{ t('common.actions.browse') }}</w-tooltip>
</w-btn>
</template>
</w-input>
</w-item-section>
</w-item>
<w-separator class="my-2" inset />
<w-item tag="label">
<blueprint-icon icon="external-link" />
<w-item-section>
<w-item-label>{{ t(`navEdit.openInNewWindow`) }}</w-item-label>
<w-item-label caption>{{ t(`navEdit.openInNewWindowHint`) }}</w-item-label>
</w-item-section>
<w-item-section avatar>
<w-toggle
v-model="state.current.openInNewWindow"
color="primary"
checked-icon="la:check"
unchecked-icon="la:times"
:aria-label="t(`navEdit.openInNewWindow`)" />
</w-item-section>
</w-item>
</template>
<w-separator class="my-2" inset />
<w-item>
<blueprint-icon icon="user-groups" />
@ -497,6 +522,7 @@ const state = reactive({
icon: '',
target: '/',
openInNewWindow: false,
expandByDefault: false,
visibilityGroups: [],
visibilityLimited: false,
isNested: false
@ -534,6 +560,23 @@ const visibilityOptions = [
*/
const navId = computed(() => (pageStore.isHome ? pageStore.navigationId : pageStore.id))
/**
* Whether the link being edited is a parent one the sidebar draws as a submenu.
*
* Parenthood is not a property of the item: this list is flat, and `isNested` says an item belongs to
* whatever link comes before it, so what makes a link a parent is the item that FOLLOWS it. Which is why
* this is asked of the list rather than read off `state.current`, and why it answers again the moment a
* child is nested, unnested or dragged away.
*/
const currentIsParent = computed(() => {
const item = state.current
if (item?.type !== 'link' || item.isNested) {
return false
}
const idx = state.items.findIndex((it) => it.id === item.id)
return idx >= 0 && Boolean(state.items[idx + 1]?.isNested)
})
const thumbStyle = {
right: '2px',
borderRadius: '5px',
@ -595,6 +638,7 @@ function addItem(type) {
newItem.icon = DEFAULT_LINK_ICON
newItem.target = '/'
newItem.openInNewWindow = false
newItem.expandByDefault = false
newItem.isNested = false
break
}
@ -659,6 +703,7 @@ async function loadMenuItems() {
'icon',
'target',
'openInNewWindow',
'expandByDefault',
'visibilityGroups'
]),
visibilityLimited: item.visibilityGroups?.length > 0
@ -702,7 +747,9 @@ function cleanMenuItem(item, isNested = false) {
return {
...pick(item, ['id', 'type', 'label', 'icon', 'target', 'openInNewWindow']),
visibilityGroups: item.visibilityLimited ? item.visibilityGroups : [],
...(!isNested && { children: [] })
// -> Only a top-level link can hold children, so only one of those can be a parent a nested
// item carrying an expand flag would be a setting nothing ever reads
...(!isNested && { children: [], expandByDefault: Boolean(item.expandByDefault) })
}
}
case 'separator': {

@ -15,12 +15,13 @@
>{{ item.label }}</w-item-label
>
<!-- -> Open from the start when the page being read is one of its children, so a reader arriving
by URL sees where they are in the tree. Not `v-model`: after that first render the group
is the reader's to open and close, and a bound value would fight them -->
by URL sees where they are in the tree -- or when the menu says this group opens that way
whatever is being read. Not `v-model`: after that first render the group is the reader's
to open and close, and a bound value would fight them -->
<w-expansion-item
v-else-if="item.type === `link` && item.children?.length > 0"
dense
:default-opened="containsCurrent(item)">
:default-opened="item.expandByDefault || containsCurrent(item)">
<!-- The icon goes through a header slot rather than the `icon` prop, so that an Iconify -->
<!-- reference is drawn by w-icon like everywhere else -->
<template #header>

Loading…
Cancel
Save