forked from platypush/platypush
[UI] Dropdown component rewrite.
Dropdown components should always be rendered under the root element, or nasty effects caused by absolute parenting may end up hiding dropdown elements regardless of their `z-index`. The new approach uses a single `<DropdownContainer>` element in the main `App` file. Each `<Dropdown>` component will push updates to the bus whenever it triggers open/close events, and the dropdown component to be rendered will be pushed upstream and rendered in the root element.
This commit is contained in:
parent
fafc1747d6
commit
f7a25a478d
6 changed files with 182 additions and 67 deletions
|
@ -8,24 +8,31 @@
|
|||
Would you like to install this application locally?
|
||||
</ConfirmDialog>
|
||||
|
||||
<DropdownContainer />
|
||||
<router-view />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ConfirmDialog from "@/components/elements/ConfirmDialog";
|
||||
import DropdownContainer from "@/components/elements/DropdownContainer";
|
||||
import Notifications from "@/components/Notifications";
|
||||
import Utils from "@/Utils";
|
||||
import Events from "@/Events";
|
||||
import VoiceAssistant from "@/components/VoiceAssistant";
|
||||
import { bus } from "@/bus";
|
||||
import Ntfy from "@/components/Ntfy";
|
||||
import Pushbullet from "@/components/Pushbullet";
|
||||
import { bus } from "@/bus";
|
||||
|
||||
export default {
|
||||
name: 'App',
|
||||
mixins: [Utils],
|
||||
components: {
|
||||
ConfirmDialog, Pushbullet, Ntfy, Notifications, Events, VoiceAssistant
|
||||
ConfirmDialog,
|
||||
DropdownContainer,
|
||||
Events,
|
||||
Notifications,
|
||||
Ntfy,
|
||||
Pushbullet,
|
||||
VoiceAssistant,
|
||||
},
|
||||
|
||||
data() {
|
||||
|
@ -104,7 +111,6 @@ export default {
|
|||
}
|
||||
</script>
|
||||
|
||||
<!--suppress CssUnusedSymbol -->
|
||||
<style lang="scss">
|
||||
$fa-font-path: "~@fortawesome/fontawesome-free/webfonts";
|
||||
@import "~@fortawesome/fontawesome-free/scss/fontawesome";
|
||||
|
|
|
@ -1,30 +1,30 @@
|
|||
<template>
|
||||
<div class="dropdown-container" ref="container">
|
||||
<div class="dropdown-container">
|
||||
<button :title="title" ref="button" @click.stop="toggle($event)">
|
||||
<i class="icon" :class="iconClass" v-if="iconClass" />
|
||||
<span class="text" v-text="text" v-if="text" />
|
||||
</button>
|
||||
|
||||
<div class="dropdown fade-in" :id="id" :class="{hidden: !visible}" ref="dropdown">
|
||||
<div class="body-container hidden" ref="dropdownContainer">
|
||||
<DropdownBody :id="id" :keepOpenOnItemClick="keepOpenOnItemClick" ref="dropdown">
|
||||
<slot />
|
||||
</DropdownBody>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import DropdownBody from "./DropdownBody";
|
||||
import { bus } from "@/bus";
|
||||
|
||||
export default {
|
||||
name: "Dropdown",
|
||||
components: { DropdownBody },
|
||||
emits: ['click'],
|
||||
props: {
|
||||
id: {
|
||||
type: String,
|
||||
},
|
||||
|
||||
items: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
|
||||
iconClass: {
|
||||
default: 'fa fa-ellipsis-h',
|
||||
},
|
||||
|
@ -49,6 +49,39 @@ export default {
|
|||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
dropdownWidth() {
|
||||
const dropdown = this.$refs.dropdown?.$el
|
||||
if (!dropdown)
|
||||
return 0
|
||||
|
||||
return parseFloat(getComputedStyle(dropdown).width)
|
||||
},
|
||||
|
||||
dropdownHeight() {
|
||||
const dropdown = this.$refs.dropdown?.$el
|
||||
if (!dropdown)
|
||||
return 0
|
||||
|
||||
return parseFloat(getComputedStyle(dropdown).height)
|
||||
},
|
||||
|
||||
buttonStyle() {
|
||||
if (!this.$refs.button)
|
||||
return {}
|
||||
|
||||
return getComputedStyle(this.$refs.button)
|
||||
},
|
||||
|
||||
buttonWidth() {
|
||||
return parseFloat(this.buttonStyle.width || 0)
|
||||
},
|
||||
|
||||
buttonHeight() {
|
||||
return parseFloat(this.buttonStyle.height || 0)
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
documentClickHndl(event) {
|
||||
if (!this.visible)
|
||||
|
@ -56,9 +89,7 @@ export default {
|
|||
|
||||
let element = event.target
|
||||
while (element) {
|
||||
if (!this.$refs.dropdown)
|
||||
break
|
||||
if (element === this.$refs.dropdown.element)
|
||||
if (element.classList.contains('dropdown'))
|
||||
return
|
||||
|
||||
element = element.parentElement
|
||||
|
@ -70,23 +101,40 @@ export default {
|
|||
close() {
|
||||
this.visible = false
|
||||
document.removeEventListener('click', this.documentClickHndl)
|
||||
bus.emit('dropdown-close')
|
||||
},
|
||||
|
||||
open() {
|
||||
document.addEventListener('click', this.documentClickHndl)
|
||||
this.visible = true
|
||||
this.$refs.dropdownContainer.classList.remove('hidden')
|
||||
this.$nextTick(() => {
|
||||
const buttonRect = this.$refs.button.getBoundingClientRect()
|
||||
const buttonPos = {
|
||||
left: buttonRect.left + window.scrollX,
|
||||
top: buttonRect.top + window.scrollY,
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
const element = this.$refs.dropdown
|
||||
element.style.left = 0
|
||||
element.style.top = parseFloat(getComputedStyle(this.$refs.button).height) + 'px'
|
||||
const pos = {
|
||||
left: buttonPos.left,
|
||||
top: buttonPos.top + this.buttonHeight,
|
||||
}
|
||||
|
||||
if (element.getBoundingClientRect().left > window.innerWidth/2)
|
||||
element.style.left = (-element.clientWidth + parseFloat(getComputedStyle(this.$refs.button).width)) + 'px'
|
||||
if ((pos.left + this.dropdownWidth) > (window.innerWidth + window.scrollX) / 2) {
|
||||
pos.left -= (this.dropdownWidth - this.buttonWidth)
|
||||
}
|
||||
|
||||
if (element.getBoundingClientRect().top > window.innerHeight/2)
|
||||
element.style.top = (-element.clientHeight + parseFloat(getComputedStyle(this.$refs.button).height)) + 'px'
|
||||
}, 10)
|
||||
if ((pos.top + this.dropdownHeight) > (window.innerHeight + window.scrollY) / 2) {
|
||||
pos.top -= (this.dropdownHeight + this.buttonHeight - 10)
|
||||
}
|
||||
|
||||
const element = this.$refs.dropdown.$el
|
||||
element.classList.add('fade-in')
|
||||
element.style.top = `${pos.top}px`
|
||||
element.style.left = `${pos.left}px`
|
||||
bus.emit('dropdown-open', this.$refs.dropdown)
|
||||
this.$refs.dropdownContainer.classList.add('hidden')
|
||||
})
|
||||
},
|
||||
|
||||
toggle(event) {
|
||||
|
@ -128,40 +176,5 @@ export default {
|
|||
color: $default-hover-fg;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
position: absolute;
|
||||
width: max-content;
|
||||
background: $dropdown-bg;
|
||||
border-radius: .25em;
|
||||
border: $default-border-3;
|
||||
box-shadow: $dropdown-shadow;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.dropdown-container) {
|
||||
button {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
color: $default-fg-2;
|
||||
background: $dropdown-bg;
|
||||
border: 0;
|
||||
padding: 0.75em 0.5em;
|
||||
text-align: left;
|
||||
letter-spacing: 0.01em;
|
||||
|
||||
&:hover {
|
||||
background: $hover-bg;
|
||||
color: $default-fg-2;
|
||||
}
|
||||
|
||||
.text {
|
||||
padding-left: 0.25em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
<template>
|
||||
<div class="dropdown" :id="id">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
emits: ['click'],
|
||||
props: {
|
||||
id: {
|
||||
type: String,
|
||||
},
|
||||
|
||||
keepOpenOnItemClick: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.dropdown {
|
||||
position: absolute;
|
||||
width: max-content;
|
||||
background: $dropdown-bg;
|
||||
border-radius: .25em;
|
||||
box-shadow: $dropdown-shadow;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
:deep(.dropdown-container) {
|
||||
button {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
color: $default-fg-2;
|
||||
background: $dropdown-bg;
|
||||
border: 0;
|
||||
padding: 0.75em 0.5em;
|
||||
text-align: left;
|
||||
letter-spacing: 0.01em;
|
||||
|
||||
&:hover {
|
||||
background: $hover-bg;
|
||||
color: $default-fg-2;
|
||||
}
|
||||
|
||||
.text {
|
||||
padding-left: 0.25em;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,38 @@
|
|||
<template>
|
||||
<div class="dropdown-container" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { bus } from "@/bus";
|
||||
|
||||
export default {
|
||||
methods: {
|
||||
onOpen(component) {
|
||||
if (!component?.$el)
|
||||
return
|
||||
|
||||
if (!component.keepOpenOnItemClick)
|
||||
this.onClose()
|
||||
|
||||
this.$el.appendChild(component.$el)
|
||||
},
|
||||
|
||||
onClose() {
|
||||
this.$el.innerHTML = ''
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
bus.on('dropdown-open', this.onOpen)
|
||||
bus.on('dropdown-close', this.onClose)
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.dropdown-container {
|
||||
:deep(.dropdown) {
|
||||
border: $default-border-2;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -9,9 +9,9 @@
|
|||
|
||||
<script>
|
||||
import Icon from "@/components/elements/Icon";
|
||||
import { bus } from "@/bus";
|
||||
|
||||
export default {
|
||||
name: "DropdownItem",
|
||||
components: {Icon},
|
||||
props: {
|
||||
iconClass: {
|
||||
|
@ -35,13 +35,12 @@ export default {
|
|||
},
|
||||
|
||||
methods: {
|
||||
clicked(event) {
|
||||
clicked() {
|
||||
if (this.disabled)
|
||||
return false
|
||||
|
||||
this.$parent.$emit('click', event)
|
||||
if (!this.$parent.keepOpenOnItemClick)
|
||||
this.$parent.visible = false
|
||||
bus.emit('dropdown-close')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -50,20 +49,22 @@ export default {
|
|||
<style lang="scss" scoped>
|
||||
.item {
|
||||
display: flex;
|
||||
flex-direction: row !important;
|
||||
min-width: 7.5em;
|
||||
padding: 0.75em 0.5em;
|
||||
cursor: pointer;
|
||||
align-items: center;
|
||||
color: $default-fg-2;
|
||||
border: 0 !important;
|
||||
box-shadow: none;
|
||||
cursor: pointer !important;
|
||||
box-shadow: none !important;
|
||||
|
||||
&:hover {
|
||||
background: $hover-bg;
|
||||
background: $hover-bg !important;
|
||||
}
|
||||
|
||||
&.selected {
|
||||
font-weight: bold;
|
||||
font-weight: bold !important;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
|
|
|
@ -60,6 +60,7 @@ $border-shadow-bottom: 0 3px 2px -1px $default-shadow-color;
|
|||
$border-shadow-left: -2.5px 0 4px 0 $default-shadow-color;
|
||||
$border-shadow-right: 2.5px 0 4px 0 $default-shadow-color;
|
||||
$border-shadow-bottom-right: 2.5px 2.5px 3px 0 $default-shadow-color;
|
||||
$border-shadow: 0 0 3px 3px #c0c0c0 !default;
|
||||
$header-shadow: 0px 1px 3px 1px #bbb !default;
|
||||
$group-shadow: 3px -2px 6px 1px #98b0a0;
|
||||
$primary-btn-shadow: 1px 1px 0.5px 0.75px #32b64640 !default;
|
||||
|
|
Loading…
Reference in a new issue