feat: nav parent - expand by default option

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

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

@ -2161,6 +2161,8 @@
"navEdit.clearItems": "Clear All Items", "navEdit.clearItems": "Clear All Items",
"navEdit.editMenuItems": "Edit Menu Items", "navEdit.editMenuItems": "Edit Menu Items",
"navEdit.emptyMenuText": "Click the Add button to add your first menu item.", "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.groupsFailed": "Failed to load the list of groups. Per-group visibility cannot be changed.",
"navEdit.header": "Header", "navEdit.header": "Header",
"navEdit.icon": "Icon", "navEdit.icon": "Icon",

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

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

@ -15,12 +15,13 @@
>{{ item.label }}</w-item-label >{{ item.label }}</w-item-label
> >
<!-- -> Open from the start when the page being read is one of its children, so a reader arriving <!-- -> 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 by URL sees where they are in the tree -- or when the menu says this group opens that way
is the reader's to open and close, and a bound value would fight them --> 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 <w-expansion-item
v-else-if="item.type === `link` && item.children?.length > 0" v-else-if="item.type === `link` && item.children?.length > 0"
dense 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 --> <!-- 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 --> <!-- reference is drawn by w-icon like everywhere else -->
<template #header> <template #header>

Loading…
Cancel
Save