refactor(menu): get rid of dropdown in menu, instead using popover to show hidden menu

This commit is contained in:
07akioni 2020-01-23 17:24:48 +08:00
parent 1d35e6945f
commit 45ee04bbb2
6 changed files with 142 additions and 184 deletions

View File

@ -28,7 +28,7 @@ function createRenderer (wrapCodeWithCard = true) {
return `<n-p>${text}</n-p>` return `<n-p>${text}</n-p>`
}, },
link (href, title, text) { link (href, title, text) {
return `<n-a title="${title}" href="${href}">${text}</n-a>` return `<n-a to="${href}" >${text}</n-a>`
}, },
list (body, ordered, start) { list (body, ordered, start) {
const type = ordered ? 'n-ol' : 'n-ul' const type = ordered ? 'n-ol' : 'n-ul'

View File

@ -21,7 +21,8 @@ export default {
name: 'Menu', name: 'Menu',
provide () { provide () {
return { return {
NMenu: this NMenu: this,
NSubmenu: null
} }
}, },
mixins: [withapp, themeable], mixins: [withapp, themeable],
@ -65,6 +66,10 @@ export default {
openNames: { openNames: {
type: Array, type: Array,
default: undefined default: undefined
},
inPopover: {
type: Boolean,
default: false
} }
}, },
data () { data () {
@ -83,7 +88,7 @@ export default {
this.$emit('select', value) this.$emit('select', value)
this.$emit('input', value) this.$emit('input', value)
}, },
handleOpenNamesChange (name) { toggleOpenName (name) {
const currentOpenNames = Array.from(this.synthesizedOpenNames) const currentOpenNames = Array.from(this.synthesizedOpenNames)
const index = currentOpenNames.findIndex(openName => openName === name) const index = currentOpenNames.findIndex(openName => openName === name)
if (~index) { if (~index) {
@ -94,7 +99,13 @@ export default {
if (this.openNames === undefined) { if (this.openNames === undefined) {
this.internalOpenNames = currentOpenNames this.internalOpenNames = currentOpenNames
} }
this.$emit('openNamesChange', currentOpenNames) this.$emit('open-names-change', currentOpenNames)
},
handleOpenNamesChange (names) {
if (this.openName === undefined) {
this.internalOpenNames = names
}
this.$emit('open-names-change', names)
} }
} }
} }

View File

@ -1,43 +1,5 @@
<template> <template>
<li <li
v-if="!shouldBeRenderedAsDropdownItem && isFirstLevel"
:key="name"
class="n-menu-item-wrapper"
>
<n-tooltip trigger="hover" :disabled="!NMenu.collapsed" placement="right" :delay="300">
<template v-slot:activator>
<div
class="n-menu-item"
:style="{ paddingLeft: delayedPaddingLeft + 'px' }"
:class="{
'n-menu-item--selected': selected,
'n-menu-item--disabled': synthesizedDisabled
}"
@click="handleClick"
>
<div
v-if="$slots.icon"
class="n-menu-item__icon"
:style="{
width: maxIconSize && (maxIconSize + 'px'),
height: maxIconSize && (maxIconSize + 'px'),
fontSize: activeIconSize && (activeIconSize + 'px'),
}"
>
<slot name="icon" />
</div>
<div class="n-menu-item__header">
<slot>
<render :render="title" />
</slot>
</div>
</div>
</template>
<render :render="title" />
</n-tooltip>
</li>
<li
v-else-if="!shouldBeRenderedAsDropdownItem"
:key="name" :key="name"
class="n-menu-item" class="n-menu-item"
:style="{ paddingLeft: delayedPaddingLeft + 'px' }" :style="{ paddingLeft: delayedPaddingLeft + 'px' }"
@ -47,26 +9,23 @@
}" }"
@click="handleClick" @click="handleClick"
> >
<!-- identical part start -->
<div <div
v-if="$slots.icon" v-if="$slots.icon"
class="n-menu-item__icon" class="n-menu-item__icon"
:style="{ :style="{
width: iconSize && (iconSize + 'px'), width: maxIconSize && (maxIconSize + 'px'),
height: iconSize && (iconSize + 'px'), height: maxIconSize && (maxIconSize + 'px'),
fontSize: iconSize && (iconSize + 'px'), fontSize: activeIconSize && (activeIconSize + 'px'),
}" }"
> >
<slot name="icon" /> <slot name="icon" />
</div> </div>
<!-- identical part start end -->
<div class="n-menu-item__header"> <div class="n-menu-item__header">
<slot> <slot>
<render :render="title" /> <render :render="title" />
</slot> </slot>
</div> </div>
</li> </li>
<n-dropdown-item v-else :name="name" :label="title" :value="value" :selected="selected" />
</template> </template>
<script> <script>
@ -74,18 +33,16 @@ import collectable from '../../../mixins/collectable'
import withapp from '../../../mixins/withapp' import withapp from '../../../mixins/withapp'
import themeable from '../../../mixins/themeable' import themeable from '../../../mixins/themeable'
import render from '../../../utils/render' import render from '../../../utils/render'
import NTooltip from '../../Tooltip'
import NDropdownItem from '../../Dropdown/src/DropdownItem'
export default { export default {
name: 'NMenuItem', name: 'NMenuItem',
components: { components: {
NTooltip,
NDropdownItem,
render render
}, },
mixins: [ mixins: [
collectable('NSubmenu', 'menuItemNames', 'name', true), collectable('NSubmenu', 'menuItemNames', 'name', true, function (injection) {
return this.NMenu !== injection.NMenu
}),
withapp, withapp,
themeable themeable
], ],
@ -98,9 +55,6 @@ export default {
}, },
NMenuItemGroup: { NMenuItemGroup: {
default: null default: null
},
NMenuUl: {
default: null
} }
}, },
props: { props: {
@ -127,13 +81,6 @@ export default {
} }
}, },
computed: { computed: {
disabledCollectable () {
return !this.NMenuUl
},
shouldBeRenderedAsDropdownItem () {
if (this.NMenuUl) return false
return !this.isFirstLevel && this.NMenu.collapsed
},
useCollapsedIconSize () { useCollapsedIconSize () {
return this.NMenu.collapsed && this.isFirstLevel return this.NMenu.collapsed && this.isFirstLevel
}, },

View File

@ -1,16 +0,0 @@
<template>
<ul>
<slot />
</ul>
</template>
<script>
export default {
name: 'NMenuUl',
provide () {
return {
NMenuUl: true
}
}
}
</script>

View File

@ -1,117 +1,116 @@
<template> <template>
<n-dropdown-submenu
v-if="shouldBeRenderedAsDropdownSubmenu && !NMenuUl"
:value="value"
:label="title"
:name="name"
:selected="selectedInside"
>
<template v-slot:activator>
<render :render="title" />
</template>
<slot />
</n-dropdown-submenu>
<li <li
v-else
class="n-submenu" class="n-submenu"
:class="{ :class="{
'n-submenu--selected-inside': selectedInside 'n-submenu--selected-inside': selectedInside
}" }"
> >
<n-dropdown <template v-if="isFirstLevel">
v-if="isFirstLevel" <n-popover trigger="click" placement="right-start" :disabled="!usePopover">
size="large" <template v-slot:activator>
trigger="click"
:focusable="false"
:disabled="!NMenu.collapsed"
placement="right-start"
type="menu"
@select="handleDropdownSelect"
>
<template v-slot:activator>
<div
class="n-submenu-item n-dropdown"
:style="{ paddingLeft: delayedPaddingLeft + 'px' }"
:class="{
'n-submenu-item--collapsed': contentCollapsed,
'n-submenu-item--active': !contentCollapsed,
'n-submenu-item--disabled': disabled
}"
@click="handleClick"
>
<div <div
v-if="$slots.icon" class="n-submenu-item"
class="n-submenu-item__icon" :style="{ paddingLeft: delayedPaddingLeft + 'px' }"
:style="{ :class="{
width: maxIconSize && (maxIconSize + 'px'), 'n-submenu-item--collapsed': synthesizedCollapsed,
height: maxIconSize && (maxIconSize + 'px'), 'n-submenu-item--active': !synthesizedCollapsed,
fontSize: activeIconSize && (activeIconSize + 'px'), 'n-submenu-item--disabled': disabled
}" }"
@click="handleClick"
> >
<slot name="icon" /> <div
v-if="$slots.icon"
class="n-submenu-item__icon"
:style="{
width: maxIconSize && (maxIconSize + 'px'),
height: maxIconSize && (maxIconSize + 'px'),
fontSize: activeIconSize && (activeIconSize + 'px'),
}"
>
<slot name="icon" />
</div>
<div class="n-submenu-item__header">
<slot name="header">
<render :render="title" />
</slot>
</div>
</div> </div>
<div class="n-submenu-item__header"> </template>
<slot name="header"> <n-menu
<render :render="title" /> :style="{
</slot> width: '272px'
</div> }"
</div> :root-indent="24"
</template> in-popover
<slot /> :value="popMenuValue"
</n-dropdown> :open-names="popMenuOpenNames"
<div @select="handlePopMenuSelect"
v-else @open-names-change="handlePopMenuOpenNamesChange"
class="n-submenu-item n-submenu-item--as-dropdown" >
:style="{ paddingLeft: delayedPaddingLeft + 'px' }" <slot />
:class="{ </n-menu>
'n-submenu-item--collapsed': contentCollapsed, </n-popover>
'n-submenu-item--active': !contentCollapsed, <fade-in-height-expand-transition>
'n-submenu-item--disabled': disabled <ul
}" v-show="!synthesizedCollapsed"
@click="handleClick" class="n-submenu-content"
> >
<slot />
</ul>
</fade-in-height-expand-transition>
</template>
<template v-else>
<div <div
v-if="$slots.icon" class="n-submenu-item"
class="n-submenu-item__icon" :style="{ paddingLeft: delayedPaddingLeft + 'px' }"
:style="{ :class="{
width: iconSize && (iconSize + 'px'), 'n-submenu-item--collapsed': synthesizedCollapsed,
height: iconSize && (iconSize + 'px'), 'n-submenu-item--active': !synthesizedCollapsed,
fontSize: iconSize && (iconSize + 'px'), 'n-submenu-item--disabled': disabled
}" }"
@click="handleClick"
> >
<slot name="icon" /> <div
v-if="$slots.icon"
class="n-submenu-item__icon"
:style="{
width: iconSize && (iconSize + 'px'),
height: iconSize && (iconSize + 'px'),
fontSize: iconSize && (iconSize + 'px'),
}"
>
<slot name="icon" />
</div>
<div class="n-submenu-item__header">
<slot name="header">
<render :render="title" />
</slot>
</div>
</div> </div>
<div class="n-submenu-item__header"> <fade-in-height-expand-transition>
<slot name="header"> <ul
<render :render="title" /> v-show="!synthesizedCollapsed"
</slot> class="n-submenu-content"
</div> >
</div> <slot />
<fade-in-height-expand-transition> </ul>
<n-menu-ul </fade-in-height-expand-transition>
v-show="!contentCollapsed" </template>
class="n-submenu-content"
>
<slot />
</n-menu-ul>
</fade-in-height-expand-transition>
</li> </li>
</template> </template>
<script> <script>
import FadeInHeightExpandTransition from '../../../transition/FadeInHeightExpandTransition' import FadeInHeightExpandTransition from '../../../transition/FadeInHeightExpandTransition'
import render from '../../../utils/render' import render from '../../../utils/render'
import NDropdown from '../../Dropdown/src/Dropdown' import NPopover from '../../../common/Popover'
import NDropdownSubmenu from '../../Dropdown/src/DropdownSubmenu' import NMenu from './Menu'
import NMenuUl from './MenuUl'
export default { export default {
name: 'NSubmenu', name: 'NSubmenu',
components: { components: {
FadeInHeightExpandTransition, FadeInHeightExpandTransition,
NDropdown, NPopover,
NDropdownSubmenu, NMenu,
NMenuUl,
render render
}, },
props: { props: {
@ -142,8 +141,8 @@ export default {
selectedInside () { selectedInside () {
return this.menuItemNames.includes(this.NMenu.value) return this.menuItemNames.includes(this.NMenu.value)
}, },
shouldBeRenderedAsDropdownSubmenu () { usePopover () {
return this.NMenu.collapsed && !this.isFirstLevel return this.useCollapsedIconSize
}, },
useCollapsedIconSize () { useCollapsedIconSize () {
return this.NMenu.collapsed && this.isFirstLevel return this.NMenu.collapsed && this.isFirstLevel
@ -182,8 +181,25 @@ export default {
return this.NMenu.rootIndent || this.NMenu.indent return this.NMenu.rootIndent || this.NMenu.indent
} }
}, },
contentCollapsed () { rootMenuCollapsed () {
return this.NMenu.collapsed || !(this.NMenu.synthesizedOpenNames.includes(this.name)) return this.NMenu.collapsed
},
rootMenuInPopover () {
return this.NMenu.inPopover
},
selfCollapsed () {
return !this.NMenu.synthesizedOpenNames.includes(this.name)
},
synthesizedCollapsed () {
if (this.rootMenuInPopover) return this.selfCollapsed
else if (this.rootMenuCollapsed) return true
return this.selfCollapsed
},
popMenuValue () {
return this.NMenu.value
},
popMenuOpenNames () {
return this.NMenu.synthesizedOpenNames
} }
}, },
watch: { watch: {
@ -211,24 +227,23 @@ export default {
}, },
NMenuItemGroup: { NMenuItemGroup: {
default: null default: null
},
NMenuUl: {
default: null
} }
}, },
created () { created () {
// console.log('submenu created', this.name)
this.delayedPaddingLeft = this.paddingLeft this.delayedPaddingLeft = this.paddingLeft
}, },
methods: { methods: {
handleDropdownSelect (value) {
this.NMenu.handleSelect(value)
},
handleClick () { handleClick () {
if (!this.disabled && !this.NMenu.collapsed) { if (!this.disabled && !this.NMenu.collapsed) {
this.NMenu.handleOpenNamesChange(this.name) this.NMenu.toggleOpenName(this.name)
this.$emit('click', this) this.$emit('click', this)
} }
},
handlePopMenuSelect (value) {
this.NMenu.handleSelect(value)
},
handlePopMenuOpenNamesChange (value) {
this.NMenu.handleOpenNamesChange(value)
} }
} }
} }

View File

@ -2,7 +2,8 @@ export default function (
injectionName, injectionName,
collectionProperty, collectionProperty,
registerProperty = 'value', registerProperty = 'value',
bubble = false bubble = false,
disabledCollectable = null
) { ) {
const registerPropertyChangeHandler = function (value, oldValue) { const registerPropertyChangeHandler = function (value, oldValue) {
if (this.activeCollectableInjection) { if (this.activeCollectableInjection) {
@ -38,20 +39,20 @@ export default function (
} }
}, },
beforeDestroy () { beforeDestroy () {
console.log('before destroy', this.name, this.$el) // console.log('before destroy', this.name, this.$el)
if (this.activeCollectableInjection) { if (this.activeCollectableInjection) {
this.registerValue(undefined, this[registerProperty]) this.registerValue(undefined, this[registerProperty])
} }
}, },
destroyed () { destroyed () {
console.log('destroyed', this.name) // console.log('destroyed', this.name)
}, },
methods: { methods: {
registerValue (value = undefined, oldValue = undefined) { registerValue (value = undefined, oldValue = undefined) {
if (this.disabledCollectable) return // console.log('registerValue')
console.log('registerValue')
let currentInjection = this.activeCollectableInjection let currentInjection = this.activeCollectableInjection
while (currentInjection) { while (currentInjection) {
if (disabledCollectable && disabledCollectable.call(this, currentInjection)) return
const collectedValues = currentInjection[collectionProperty] const collectedValues = currentInjection[collectionProperty]
if (oldValue !== undefined) { if (oldValue !== undefined) {
const oldValueIndex = collectedValues.findIndex(collectedValue => collectedValue === oldValue) const oldValueIndex = collectedValues.findIndex(collectedValue => collectedValue === oldValue)