diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-06-16 18:25:58 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-06-16 18:25:58 +0000 |
commit | a5f4bba440d7f9ea47046a0a561d49adf0a1e6d4 (patch) | |
tree | fb69158581673816a8cd895f9d352dcb3c678b1e /app/assets/javascripts/nav | |
parent | d16b2e8639e99961de6ddc93909f3bb5c1445ba1 (diff) | |
download | gitlab-ce-a5f4bba440d7f9ea47046a0a561d49adf0a1e6d4.tar.gz |
Add latest changes from gitlab-org/gitlab@14-0-stable-eev14.0.0-rc42
Diffstat (limited to 'app/assets/javascripts/nav')
15 files changed, 443 insertions, 114 deletions
diff --git a/app/assets/javascripts/nav/components/responsive_app.vue b/app/assets/javascripts/nav/components/responsive_app.vue new file mode 100644 index 00000000000..d601586a3f8 --- /dev/null +++ b/app/assets/javascripts/nav/components/responsive_app.vue @@ -0,0 +1,107 @@ +<script> +import { FREQUENT_ITEMS_PROJECTS, FREQUENT_ITEMS_GROUPS } from '~/frequent_items/constants'; +import { BV_DROPDOWN_SHOW, BV_DROPDOWN_HIDE } from '~/lib/utils/constants'; +import KeepAliveSlots from '~/vue_shared/components/keep_alive_slots.vue'; +import eventHub, { EVENT_RESPONSIVE_TOGGLE } from '../event_hub'; +import { resetMenuItemsActive, hasMenuExpanded } from '../utils'; +import ResponsiveHeader from './responsive_header.vue'; +import ResponsiveHome from './responsive_home.vue'; +import TopNavContainerView from './top_nav_container_view.vue'; + +export default { + components: { + KeepAliveSlots, + ResponsiveHeader, + ResponsiveHome, + TopNavContainerView, + }, + props: { + navData: { + type: Object, + required: true, + }, + }, + data() { + return { + activeView: 'home', + hasMobileOverlay: false, + }; + }, + computed: { + nav() { + return resetMenuItemsActive(this.navData); + }, + }, + created() { + eventHub.$on(EVENT_RESPONSIVE_TOGGLE, this.updateResponsiveOpen); + this.$root.$on(BV_DROPDOWN_SHOW, this.showMobileOverlay); + this.$root.$on(BV_DROPDOWN_HIDE, this.hideMobileOverlay); + + this.updateResponsiveOpen(); + }, + beforeDestroy() { + eventHub.$off(EVENT_RESPONSIVE_TOGGLE, this.onToggle); + this.$root.$off(BV_DROPDOWN_SHOW, this.showMobileOverlay); + this.$root.$off(BV_DROPDOWN_HIDE, this.hideMobileOverlay); + }, + methods: { + updateResponsiveOpen() { + if (hasMenuExpanded()) { + document.body.classList.add('top-nav-responsive-open'); + } else { + document.body.classList.remove('top-nav-responsive-open'); + } + }, + onMenuItemClick({ view }) { + if (view) { + this.activeView = view; + } + }, + showMobileOverlay() { + this.hasMobileOverlay = true; + }, + hideMobileOverlay() { + this.hasMobileOverlay = false; + }, + }, + FREQUENT_ITEMS_PROJECTS, + FREQUENT_ITEMS_GROUPS, +}; +</script> + +<template> + <div> + <div + class="mobile-overlay" + :class="{ 'mobile-nav-open': hasMobileOverlay }" + data-testid="mobile-overlay" + ></div> + <keep-alive-slots :slot-key="activeView"> + <template #home> + <responsive-home :nav-data="nav" @menu-item-click="onMenuItemClick" /> + </template> + <template #projects> + <responsive-header @menu-item-click="onMenuItemClick"> + {{ __('Projects') }} + </responsive-header> + <top-nav-container-view + :frequent-items-dropdown-type="$options.FREQUENT_ITEMS_PROJECTS.namespace" + :frequent-items-vuex-module="$options.FREQUENT_ITEMS_PROJECTS.vuexModule" + container-class="gl-px-3" + v-bind="nav.views.projects" + /> + </template> + <template #groups> + <responsive-header @menu-item-click="onMenuItemClick"> + {{ __('Groups') }} + </responsive-header> + <top-nav-container-view + :frequent-items-dropdown-type="$options.FREQUENT_ITEMS_GROUPS.namespace" + :frequent-items-vuex-module="$options.FREQUENT_ITEMS_GROUPS.vuexModule" + container-class="gl-px-3" + v-bind="nav.views.groups" + /> + </template> + </keep-alive-slots> + </div> +</template> diff --git a/app/assets/javascripts/nav/components/responsive_header.vue b/app/assets/javascripts/nav/components/responsive_header.vue new file mode 100644 index 00000000000..8a1d21993b7 --- /dev/null +++ b/app/assets/javascripts/nav/components/responsive_header.vue @@ -0,0 +1,37 @@ +<script> +import { GlTooltipDirective } from '@gitlab/ui'; +import TopNavMenuItem from './top_nav_menu_item.vue'; + +export default { + components: { + TopNavMenuItem, + }, + directives: { + GlTooltip: GlTooltipDirective, + }, + computed: { + menuItem() { + return { + id: 'home', + view: 'home', + icon: 'angle-left', + }; + }, + }, +}; +</script> + +<template> + <header class="gl-py-4 gl-display-flex gl-align-items-center"> + <top-nav-menu-item + v-gl-tooltip="{ title: s__('TopNav|Go back') }" + class="gl-p-3!" + :menu-item="menuItem" + icon-only + @click="$emit('menu-item-click', menuItem)" + /> + <span class="gl-font-size-h2 gl-font-weight-bold gl-ml-2"> + <slot></slot> + </span> + </header> +</template> diff --git a/app/assets/javascripts/nav/components/responsive_home.vue b/app/assets/javascripts/nav/components/responsive_home.vue new file mode 100644 index 00000000000..c8f2f0bfb10 --- /dev/null +++ b/app/assets/javascripts/nav/components/responsive_home.vue @@ -0,0 +1,62 @@ +<script> +import { GlTooltipDirective } from '@gitlab/ui'; +import TopNavMenuItem from './top_nav_menu_item.vue'; +import TopNavMenuSections from './top_nav_menu_sections.vue'; +import TopNavNewDropdown from './top_nav_new_dropdown.vue'; + +const NEW_VIEW = 'new'; +const SEARCH_VIEW = 'search'; + +export default { + components: { + TopNavMenuItem, + TopNavMenuSections, + TopNavNewDropdown, + }, + directives: { + GlTooltip: GlTooltipDirective, + }, + props: { + navData: { + type: Object, + required: true, + }, + }, + computed: { + menuSections() { + return [ + { id: 'primary', menuItems: this.navData.primary }, + { id: 'secondary', menuItems: this.navData.secondary }, + ].filter((x) => x.menuItems?.length); + }, + newDropdownViewModel() { + return this.navData.views[NEW_VIEW]; + }, + searchMenuItem() { + return this.navData.views[SEARCH_VIEW]; + }, + }, +}; +</script> + +<template> + <div> + <header class="gl-display-flex gl-align-items-center gl-py-4 gl-pl-4"> + <h1 class="gl-m-0 gl-font-size-h2 gl-reset-color gl-mr-auto">{{ __('Menu') }}</h1> + <top-nav-menu-item + v-if="searchMenuItem" + v-gl-tooltip="{ title: searchMenuItem.title }" + class="gl-ml-3" + :menu-item="searchMenuItem" + icon-only + /> + <top-nav-new-dropdown + v-if="newDropdownViewModel" + v-gl-tooltip="{ title: newDropdownViewModel.title }" + :view-model="newDropdownViewModel" + class="gl-ml-3" + /> + </header> + <top-nav-menu-sections class="gl-h-full" :sections="menuSections" v-on="$listeners" /> + </div> +</template> diff --git a/app/assets/javascripts/nav/components/top_nav_app.vue b/app/assets/javascripts/nav/components/top_nav_app.vue index f8f3ba26536..08a2c6952c8 100644 --- a/app/assets/javascripts/nav/components/top_nav_app.vue +++ b/app/assets/javascripts/nav/components/top_nav_app.vue @@ -1,16 +1,12 @@ <script> -import { GlNav, GlNavItemDropdown, GlDropdownForm, GlTooltip } from '@gitlab/ui'; -import { s__ } from '~/locale'; +import { GlNav, GlNavItemDropdown, GlDropdownForm } from '@gitlab/ui'; import TopNavDropdownMenu from './top_nav_dropdown_menu.vue'; -const TOOLTIP = s__('TopNav|Switch to...'); - export default { components: { GlNav, GlNavItemDropdown, GlDropdownForm, - GlTooltip, TopNavDropdownMenu, }, props: { @@ -19,15 +15,6 @@ export default { required: true, }, }, - methods: { - findTooltipTarget() { - // ### Why use a target function instead of `v-gl-tooltip`? - // To get the tooltip to align correctly, we need it to target the actual - // toggle button which we don't directly render. - return this.$el.querySelector('.js-top-nav-dropdown-toggle'); - }, - }, - TOOLTIP, }; </script> @@ -35,10 +22,13 @@ export default { <gl-nav class="navbar-sub-nav"> <gl-nav-item-dropdown :text="navData.activeTitle" - icon="dot-grid" - menu-class="gl-mt-3! gl-max-w-none! gl-max-h-none! gl-sm-w-auto!" + data-qa-selector="navbar_dropdown" + :data-qa-title="navData.activeTitle" + icon="hamburger" + menu-class="gl-mt-3! gl-max-w-none! gl-max-h-none! gl-sm-w-auto! js-top-nav-dropdown-menu" toggle-class="top-nav-toggle js-top-nav-dropdown-toggle gl-px-3!" no-flip + no-caret > <gl-dropdown-form> <top-nav-dropdown-menu @@ -48,12 +38,5 @@ export default { /> </gl-dropdown-form> </gl-nav-item-dropdown> - <gl-tooltip - boundary="window" - :boundary-padding="0" - :target="findTooltipTarget" - placement="right" - :title="$options.TOOLTIP" - /> </gl-nav> </template> diff --git a/app/assets/javascripts/nav/components/top_nav_container_view.vue b/app/assets/javascripts/nav/components/top_nav_container_view.vue index 21ff3ebcd7d..6f98f85ff90 100644 --- a/app/assets/javascripts/nav/components/top_nav_container_view.vue +++ b/app/assets/javascripts/nav/components/top_nav_container_view.vue @@ -2,14 +2,15 @@ import FrequentItemsApp from '~/frequent_items/components/app.vue'; import eventHub from '~/frequent_items/event_hub'; import VuexModuleProvider from '~/vue_shared/components/vuex_module_provider.vue'; -import TopNavMenuItem from './top_nav_menu_item.vue'; +import TopNavMenuSections from './top_nav_menu_sections.vue'; export default { components: { FrequentItemsApp, - TopNavMenuItem, + TopNavMenuSections, VuexModuleProvider, }, + inheritAttrs: false, props: { frequentItemsVuexModule: { type: String, @@ -19,6 +20,11 @@ export default { type: String, required: true, }, + containerClass: { + type: String, + required: false, + default: '', + }, linksPrimary: { type: Array, required: false, @@ -31,11 +37,11 @@ export default { }, }, computed: { - linkGroups() { + menuSections() { return [ - { key: 'primary', links: this.linksPrimary }, - { key: 'secondary', links: this.linksSecondary }, - ].filter((x) => x.links?.length); + { id: 'primary', menuItems: this.linksPrimary }, + { id: 'secondary', menuItems: this.linksSecondary }, + ].filter((x) => x.menuItems?.length); }, }, mounted() { @@ -49,26 +55,17 @@ export default { <template> <div class="top-nav-container-view gl-display-flex gl-flex-direction-column"> - <div class="frequent-items-dropdown-container gl-w-auto"> + <div + class="frequent-items-dropdown-container gl-w-auto" + :class="containerClass" + data-testid="frequent-items-container" + > <div class="frequent-items-dropdown-content gl-w-full! gl-pt-0!"> <vuex-module-provider :vuex-module="frequentItemsVuexModule"> <frequent-items-app v-bind="$attrs" /> </vuex-module-provider> </div> </div> - <div - v-for="({ key, links }, groupIndex) in linkGroups" - :key="key" - :class="{ 'gl-mt-3': groupIndex !== 0 }" - class="gl-mt-auto gl-pt-3 gl-border-1 gl-border-t-solid gl-border-gray-100" - data-testid="menu-item-group" - > - <top-nav-menu-item - v-for="(link, linkIndex) in links" - :key="link.title" - :menu-item="link" - :class="{ 'gl-mt-1': linkIndex !== 0 }" - /> - </div> + <top-nav-menu-sections class="gl-mt-auto" :sections="menuSections" with-top-border /> </div> </template> diff --git a/app/assets/javascripts/nav/components/top_nav_dropdown_menu.vue b/app/assets/javascripts/nav/components/top_nav_dropdown_menu.vue index 1cbd64b501d..cac8fecb6b1 100644 --- a/app/assets/javascripts/nav/components/top_nav_dropdown_menu.vue +++ b/app/assets/javascripts/nav/components/top_nav_dropdown_menu.vue @@ -1,17 +1,15 @@ <script> +import { cloneDeep } from 'lodash'; import { FREQUENT_ITEMS_PROJECTS, FREQUENT_ITEMS_GROUPS } from '~/frequent_items/constants'; import KeepAliveSlots from '~/vue_shared/components/keep_alive_slots.vue'; import TopNavContainerView from './top_nav_container_view.vue'; -import TopNavMenuItem from './top_nav_menu_item.vue'; - -const ACTIVE_CLASS = 'gl-shadow-none! gl-font-weight-bold! active'; -const SECONDARY_GROUP_CLASS = 'gl-pt-3 gl-mt-3 gl-border-1 gl-border-t-solid gl-border-gray-100'; +import TopNavMenuSections from './top_nav_menu_sections.vue'; export default { components: { KeepAliveSlots, TopNavContainerView, - TopNavMenuItem, + TopNavMenuSections, }, props: { primary: { @@ -31,29 +29,25 @@ export default { }, }, data() { + // It's expected that primary & secondary never change, so these are treated as "init" props. + // We need to clone so that we can mutate the data without mutating the props + const menuSections = [ + { id: 'primary', menuItems: cloneDeep(this.primary) }, + { id: 'secondary', menuItems: cloneDeep(this.secondary) }, + ].filter((x) => x.menuItems?.length); + return { - activeId: '', + menuSections, }; }, computed: { - menuItemGroups() { - return [ - { key: 'primary', items: this.primary, classes: '' }, - { - key: 'secondary', - items: this.secondary, - classes: SECONDARY_GROUP_CLASS, - }, - ].filter((x) => x.items?.length); - }, allMenuItems() { - return this.menuItemGroups.flatMap((x) => x.items); - }, - activeMenuItem() { - return this.allMenuItems.find((x) => x.id === this.activeId); + return this.menuSections.flatMap((x) => x.menuItems); }, activeView() { - return this.activeMenuItem?.view; + const active = this.allMenuItems.find((x) => x.active); + + return active?.view; }, menuClass() { if (!this.activeView) { @@ -63,67 +57,33 @@ export default { return ''; }, }, - created() { - // Initialize activeId based on initialization prop - this.activeId = this.allMenuItems.find((x) => x.active)?.id; - }, methods: { - onClick({ id, href }) { - // If we're a link, let's just do the default behavior so the view won't change - if (href) { - return; - } - - this.activeId = id; - }, - menuItemClasses(menuItem) { - if (menuItem.id === this.activeId) { - return ACTIVE_CLASS; - } - - return ''; + onMenuItemClick({ id }) { + this.allMenuItems.forEach((menuItem) => { + this.$set(menuItem, 'active', id === menuItem.id); + }); }, }, FREQUENT_ITEMS_PROJECTS, FREQUENT_ITEMS_GROUPS, - // expose for unit tests - ACTIVE_CLASS, - SECONDARY_GROUP_CLASS, }; </script> <template> <div class="gl-display-flex gl-align-items-stretch"> <div - class="gl-w-grid-size-30 gl-flex-shrink-0 gl-bg-gray-10" + class="gl-w-grid-size-30 gl-flex-shrink-0 gl-bg-gray-10 gl-py-3 gl-px-5" :class="menuClass" data-testid="menu-sidebar" > - <div - class="gl-py-3 gl-px-5 gl-h-full gl-display-flex gl-align-items-stretch gl-flex-direction-column" - > - <div - v-for="group in menuItemGroups" - :key="group.key" - :class="group.classes" - data-testid="menu-item-group" - > - <top-nav-menu-item - v-for="(menu, index) in group.items" - :key="menu.id" - data-testid="menu-item" - :class="[{ 'gl-mt-1': index !== 0 }, menuItemClasses(menu)]" - :menu-item="menu" - @click="onClick(menu)" - /> - </div> - </div> + <top-nav-menu-sections :sections="menuSections" @menu-item-click="onMenuItemClick" /> </div> <keep-alive-slots v-show="activeView" :slot-key="activeView" class="gl-w-grid-size-40 gl-overflow-hidden gl-py-3 gl-px-5" data-testid="menu-subview" + data-qa-selector="menu_subview_container" > <template #projects> <top-nav-container-view diff --git a/app/assets/javascripts/nav/components/top_nav_menu_item.vue b/app/assets/javascripts/nav/components/top_nav_menu_item.vue index a0d92811a6f..08b2fbf2ed1 100644 --- a/app/assets/javascripts/nav/components/top_nav_menu_item.vue +++ b/app/assets/javascripts/nav/components/top_nav_menu_item.vue @@ -1,5 +1,10 @@ <script> import { GlButton, GlIcon } from '@gitlab/ui'; +import { kebabCase, mapKeys } from 'lodash'; + +const getDataKey = (key) => `data-${kebabCase(key)}`; + +const ACTIVE_CLASS = 'gl-shadow-none! gl-font-weight-bold! active'; export default { components: { @@ -11,7 +16,18 @@ export default { type: Object, required: true, }, + iconOnly: { + type: Boolean, + required: false, + default: false, + }, + }, + computed: { + dataAttrs() { + return mapKeys(this.menuItem.data || {}, (value, key) => getDataKey(key)); + }, }, + ACTIVE_CLASS, }; </script> @@ -20,12 +36,17 @@ export default { category="tertiary" :href="menuItem.href" class="top-nav-menu-item gl-display-block" + :class="[menuItem.css_class, { [$options.ACTIVE_CLASS]: menuItem.active }]" + :aria-label="menuItem.title" + v-bind="dataAttrs" v-on="$listeners" > <span class="gl-display-flex"> - <gl-icon v-if="menuItem.icon" :name="menuItem.icon" class="gl-mr-2!" /> - {{ menuItem.title }} - <gl-icon v-if="menuItem.view" name="chevron-right" class="gl-ml-auto" /> + <gl-icon v-if="menuItem.icon" :name="menuItem.icon" :class="{ 'gl-mr-2!': !iconOnly }" /> + <template v-if="!iconOnly"> + {{ menuItem.title }} + <gl-icon v-if="menuItem.view" name="chevron-right" class="gl-ml-auto" /> + </template> </span> </gl-button> </template> diff --git a/app/assets/javascripts/nav/components/top_nav_menu_sections.vue b/app/assets/javascripts/nav/components/top_nav_menu_sections.vue new file mode 100644 index 00000000000..442af512350 --- /dev/null +++ b/app/assets/javascripts/nav/components/top_nav_menu_sections.vue @@ -0,0 +1,63 @@ +<script> +import TopNavMenuItem from './top_nav_menu_item.vue'; + +const BORDER_CLASSES = 'gl-pt-3 gl-border-1 gl-border-t-solid gl-border-gray-100'; + +export default { + components: { + TopNavMenuItem, + }, + props: { + sections: { + type: Array, + required: true, + }, + withTopBorder: { + type: Boolean, + required: false, + default: false, + }, + }, + methods: { + onClick(menuItem) { + // If we're a link, let's just do the default behavior so the view won't change + if (menuItem.href) { + return; + } + + this.$emit('menu-item-click', menuItem); + }, + getMenuSectionClasses(index) { + // This is a method instead of a computed so we don't have to incur the cost of + // creating a whole new array/object. + return { + [BORDER_CLASSES]: this.withTopBorder || index > 0, + 'gl-mt-3': index > 0, + }; + }, + }, + // Expose for unit tests + BORDER_CLASSES, +}; +</script> + +<template> + <div class="gl-display-flex gl-align-items-stretch gl-flex-direction-column"> + <div + v-for="({ id, menuItems }, sectionIndex) in sections" + :key="id" + :class="getMenuSectionClasses(sectionIndex)" + data-testid="menu-section" + > + <top-nav-menu-item + v-for="(menuItem, menuItemIndex) in menuItems" + :key="menuItem.id" + :menu-item="menuItem" + data-testid="menu-item" + class="gl-w-full" + :class="{ 'gl-mt-1': menuItemIndex > 0 }" + @click="onClick(menuItem)" + /> + </div> + </div> +</template> diff --git a/app/assets/javascripts/nav/components/top_nav_new_dropdown.vue b/app/assets/javascripts/nav/components/top_nav_new_dropdown.vue new file mode 100644 index 00000000000..154bed81854 --- /dev/null +++ b/app/assets/javascripts/nav/components/top_nav_new_dropdown.vue @@ -0,0 +1,55 @@ +<script> +import { GlDropdown, GlDropdownDivider, GlDropdownItem, GlDropdownSectionHeader } from '@gitlab/ui'; + +export default { + components: { + GlDropdown, + GlDropdownDivider, + GlDropdownItem, + GlDropdownSectionHeader, + }, + props: { + viewModel: { + type: Object, + required: true, + }, + }, + computed: { + sections() { + return this.viewModel.menu_sections || []; + }, + showHeaders() { + return this.sections.length > 1; + }, + }, +}; +</script> + +<template> + <gl-dropdown + toggle-class="top-nav-menu-item" + icon="plus" + :text="viewModel.title" + category="tertiary" + text-sr-only + no-caret + right + > + <template v-for="({ title, menu_items }, index) in sections"> + <gl-dropdown-divider v-if="index > 0" :key="`${index}_divider`" data-testid="divider" /> + <gl-dropdown-section-header v-if="showHeaders" :key="`${index}_header`" data-testid="header"> + {{ title }} + </gl-dropdown-section-header> + <template v-for="menuItem in menu_items"> + <gl-dropdown-item + :key="`${index}_item_${menuItem.id}`" + link-class="top-nav-menu-item" + :href="menuItem.href" + data-testid="item" + > + {{ menuItem.title }} + </gl-dropdown-item> + </template> + </template> + </gl-dropdown> +</template> diff --git a/app/assets/javascripts/nav/event_hub.js b/app/assets/javascripts/nav/event_hub.js new file mode 100644 index 00000000000..2c8b1371fe3 --- /dev/null +++ b/app/assets/javascripts/nav/event_hub.js @@ -0,0 +1,5 @@ +import eventHubFactory from '~/helpers/event_hub_factory'; + +export const EVENT_RESPONSIVE_TOGGLE = 'top-nav-responsive-toggle'; + +export default eventHubFactory(); diff --git a/app/assets/javascripts/nav/index.js b/app/assets/javascripts/nav/index.js index 646ce3f0ecf..86d6b42e4ea 100644 --- a/app/assets/javascripts/nav/index.js +++ b/app/assets/javascripts/nav/index.js @@ -1,12 +1,28 @@ -export const initTopNav = async () => { +// With combined_menu feature flag, there's a benefit to splitting up the import +const importModule = () => import(/* webpackChunkName: 'top_nav' */ './mount'); + +const tryMountTopNav = async () => { const el = document.getElementById('js-top-nav'); if (!el) { return; } - // With combined_menu feature flag, there's a benefit to splitting up the import - const { mountTopNav } = await import(/* webpackChunkName: 'top_nav' */ './mount'); + const { mountTopNav } = await importModule(); mountTopNav(el); }; + +const tryMountTopNavResponsive = async () => { + const el = document.getElementById('js-top-nav-responsive'); + + if (!el) { + return; + } + + const { mountTopNavResponsive } = await importModule(); + + mountTopNavResponsive(el); +}; + +export const initTopNav = async () => Promise.all([tryMountTopNav(), tryMountTopNavResponsive()]); diff --git a/app/assets/javascripts/nav/mount.js b/app/assets/javascripts/nav/mount.js index 0d46ff56249..51b6a31b8cb 100644 --- a/app/assets/javascripts/nav/mount.js +++ b/app/assets/javascripts/nav/mount.js @@ -1,11 +1,12 @@ import Vue from 'vue'; import Vuex from 'vuex'; +import ResponsiveApp from './components/responsive_app.vue'; import App from './components/top_nav_app.vue'; import { createStore } from './stores'; Vue.use(Vuex); -export const mountTopNav = (el) => { +const mount = (el, Component) => { const viewModel = JSON.parse(el.dataset.viewModel); const store = createStore(); @@ -13,7 +14,7 @@ export const mountTopNav = (el) => { el, store, render(h) { - return h(App, { + return h(Component, { props: { navData: viewModel, }, @@ -21,3 +22,7 @@ export const mountTopNav = (el) => { }, }); }; + +export const mountTopNav = (el) => mount(el, App); + +export const mountTopNavResponsive = (el) => mount(el, ResponsiveApp); diff --git a/app/assets/javascripts/nav/utils/has_menu_expanded.js b/app/assets/javascripts/nav/utils/has_menu_expanded.js new file mode 100644 index 00000000000..5f126bbdf76 --- /dev/null +++ b/app/assets/javascripts/nav/utils/has_menu_expanded.js @@ -0,0 +1,2 @@ +export const hasMenuExpanded = () => + Boolean(document.querySelector('.header-content.menu-expanded')); diff --git a/app/assets/javascripts/nav/utils/index.js b/app/assets/javascripts/nav/utils/index.js new file mode 100644 index 00000000000..4fa3d0910da --- /dev/null +++ b/app/assets/javascripts/nav/utils/index.js @@ -0,0 +1,2 @@ +export * from './has_menu_expanded'; +export * from './reset_menu_items_active'; diff --git a/app/assets/javascripts/nav/utils/reset_menu_items_active.js b/app/assets/javascripts/nav/utils/reset_menu_items_active.js new file mode 100644 index 00000000000..9b5d8e97c9c --- /dev/null +++ b/app/assets/javascripts/nav/utils/reset_menu_items_active.js @@ -0,0 +1,14 @@ +const resetActiveInArray = (arr) => arr?.map((menuItem) => ({ ...menuItem, active: false })); + +/** + * This method sets `active: false` for the menu items within the given nav data. + * + * @returns navData with the menu items updated with `active: false` + */ +export const resetMenuItemsActive = ({ primary, secondary, ...navData }) => { + return { + ...navData, + primary: resetActiveInArray(primary), + secondary: resetActiveInArray(secondary), + }; +}; |