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>`
},
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) {
const type = ordered ? 'n-ol' : 'n-ul'

View File

@ -21,7 +21,8 @@ export default {
name: 'Menu',
provide () {
return {
NMenu: this
NMenu: this,
NSubmenu: null
}
},
mixins: [withapp, themeable],
@ -65,6 +66,10 @@ export default {
openNames: {
type: Array,
default: undefined
},
inPopover: {
type: Boolean,
default: false
}
},
data () {
@ -83,7 +88,7 @@ export default {
this.$emit('select', value)
this.$emit('input', value)
},
handleOpenNamesChange (name) {
toggleOpenName (name) {
const currentOpenNames = Array.from(this.synthesizedOpenNames)
const index = currentOpenNames.findIndex(openName => openName === name)
if (~index) {
@ -94,7 +99,13 @@ export default {
if (this.openNames === undefined) {
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>
<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"
class="n-menu-item"
:style="{ paddingLeft: delayedPaddingLeft + 'px' }"
@ -47,26 +9,23 @@
}"
@click="handleClick"
>
<!-- identical part start -->
<div
v-if="$slots.icon"
class="n-menu-item__icon"
:style="{
width: iconSize && (iconSize + 'px'),
height: iconSize && (iconSize + 'px'),
fontSize: iconSize && (iconSize + 'px'),
width: maxIconSize && (maxIconSize + 'px'),
height: maxIconSize && (maxIconSize + 'px'),
fontSize: activeIconSize && (activeIconSize + 'px'),
}"
>
<slot name="icon" />
</div>
<!-- identical part start end -->
<div class="n-menu-item__header">
<slot>
<render :render="title" />
</slot>
</div>
</li>
<n-dropdown-item v-else :name="name" :label="title" :value="value" :selected="selected" />
</template>
<script>
@ -74,18 +33,16 @@ import collectable from '../../../mixins/collectable'
import withapp from '../../../mixins/withapp'
import themeable from '../../../mixins/themeable'
import render from '../../../utils/render'
import NTooltip from '../../Tooltip'
import NDropdownItem from '../../Dropdown/src/DropdownItem'
export default {
name: 'NMenuItem',
components: {
NTooltip,
NDropdownItem,
render
},
mixins: [
collectable('NSubmenu', 'menuItemNames', 'name', true),
collectable('NSubmenu', 'menuItemNames', 'name', true, function (injection) {
return this.NMenu !== injection.NMenu
}),
withapp,
themeable
],
@ -98,9 +55,6 @@ export default {
},
NMenuItemGroup: {
default: null
},
NMenuUl: {
default: null
}
},
props: {
@ -127,13 +81,6 @@ export default {
}
},
computed: {
disabledCollectable () {
return !this.NMenuUl
},
shouldBeRenderedAsDropdownItem () {
if (this.NMenuUl) return false
return !this.isFirstLevel && this.NMenu.collapsed
},
useCollapsedIconSize () {
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>
<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
v-else
class="n-submenu"
:class="{
'n-submenu--selected-inside': selectedInside
}"
>
<n-dropdown
v-if="isFirstLevel"
size="large"
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"
>
<template v-if="isFirstLevel">
<n-popover trigger="click" placement="right-start" :disabled="!usePopover">
<template v-slot:activator>
<div
v-if="$slots.icon"
class="n-submenu-item__icon"
:style="{
width: maxIconSize && (maxIconSize + 'px'),
height: maxIconSize && (maxIconSize + 'px'),
fontSize: activeIconSize && (activeIconSize + 'px'),
class="n-submenu-item"
:style="{ paddingLeft: delayedPaddingLeft + 'px' }"
:class="{
'n-submenu-item--collapsed': synthesizedCollapsed,
'n-submenu-item--active': !synthesizedCollapsed,
'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 class="n-submenu-item__header">
<slot name="header">
<render :render="title" />
</slot>
</div>
</div>
</template>
<slot />
</n-dropdown>
<div
v-else
class="n-submenu-item n-submenu-item--as-dropdown"
:style="{ paddingLeft: delayedPaddingLeft + 'px' }"
:class="{
'n-submenu-item--collapsed': contentCollapsed,
'n-submenu-item--active': !contentCollapsed,
'n-submenu-item--disabled': disabled
}"
@click="handleClick"
>
</template>
<n-menu
:style="{
width: '272px'
}"
:root-indent="24"
in-popover
:value="popMenuValue"
:open-names="popMenuOpenNames"
@select="handlePopMenuSelect"
@open-names-change="handlePopMenuOpenNamesChange"
>
<slot />
</n-menu>
</n-popover>
<fade-in-height-expand-transition>
<ul
v-show="!synthesizedCollapsed"
class="n-submenu-content"
>
<slot />
</ul>
</fade-in-height-expand-transition>
</template>
<template v-else>
<div
v-if="$slots.icon"
class="n-submenu-item__icon"
:style="{
width: iconSize && (iconSize + 'px'),
height: iconSize && (iconSize + 'px'),
fontSize: iconSize && (iconSize + 'px'),
class="n-submenu-item"
:style="{ paddingLeft: delayedPaddingLeft + 'px' }"
:class="{
'n-submenu-item--collapsed': synthesizedCollapsed,
'n-submenu-item--active': !synthesizedCollapsed,
'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 class="n-submenu-item__header">
<slot name="header">
<render :render="title" />
</slot>
</div>
</div>
<fade-in-height-expand-transition>
<n-menu-ul
v-show="!contentCollapsed"
class="n-submenu-content"
>
<slot />
</n-menu-ul>
</fade-in-height-expand-transition>
<fade-in-height-expand-transition>
<ul
v-show="!synthesizedCollapsed"
class="n-submenu-content"
>
<slot />
</ul>
</fade-in-height-expand-transition>
</template>
</li>
</template>
<script>
import FadeInHeightExpandTransition from '../../../transition/FadeInHeightExpandTransition'
import render from '../../../utils/render'
import NDropdown from '../../Dropdown/src/Dropdown'
import NDropdownSubmenu from '../../Dropdown/src/DropdownSubmenu'
import NMenuUl from './MenuUl'
import NPopover from '../../../common/Popover'
import NMenu from './Menu'
export default {
name: 'NSubmenu',
components: {
FadeInHeightExpandTransition,
NDropdown,
NDropdownSubmenu,
NMenuUl,
NPopover,
NMenu,
render
},
props: {
@ -142,8 +141,8 @@ export default {
selectedInside () {
return this.menuItemNames.includes(this.NMenu.value)
},
shouldBeRenderedAsDropdownSubmenu () {
return this.NMenu.collapsed && !this.isFirstLevel
usePopover () {
return this.useCollapsedIconSize
},
useCollapsedIconSize () {
return this.NMenu.collapsed && this.isFirstLevel
@ -182,8 +181,25 @@ export default {
return this.NMenu.rootIndent || this.NMenu.indent
}
},
contentCollapsed () {
return this.NMenu.collapsed || !(this.NMenu.synthesizedOpenNames.includes(this.name))
rootMenuCollapsed () {
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: {
@ -211,24 +227,23 @@ export default {
},
NMenuItemGroup: {
default: null
},
NMenuUl: {
default: null
}
},
created () {
// console.log('submenu created', this.name)
this.delayedPaddingLeft = this.paddingLeft
},
methods: {
handleDropdownSelect (value) {
this.NMenu.handleSelect(value)
},
handleClick () {
if (!this.disabled && !this.NMenu.collapsed) {
this.NMenu.handleOpenNamesChange(this.name)
this.NMenu.toggleOpenName(this.name)
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,
collectionProperty,
registerProperty = 'value',
bubble = false
bubble = false,
disabledCollectable = null
) {
const registerPropertyChangeHandler = function (value, oldValue) {
if (this.activeCollectableInjection) {
@ -38,20 +39,20 @@ export default function (
}
},
beforeDestroy () {
console.log('before destroy', this.name, this.$el)
// console.log('before destroy', this.name, this.$el)
if (this.activeCollectableInjection) {
this.registerValue(undefined, this[registerProperty])
}
},
destroyed () {
console.log('destroyed', this.name)
// console.log('destroyed', this.name)
},
methods: {
registerValue (value = undefined, oldValue = undefined) {
if (this.disabledCollectable) return
console.log('registerValue')
// console.log('registerValue')
let currentInjection = this.activeCollectableInjection
while (currentInjection) {
if (disabledCollectable && disabledCollectable.call(this, currentInjection)) return
const collectedValues = currentInjection[collectionProperty]
if (oldValue !== undefined) {
const oldValueIndex = collectedValues.findIndex(collectedValue => collectedValue === oldValue)