mirror of
https://github.com/Dvorinka/swingmusic-extended.git
synced 2026-06-04 12:33:03 +00:00
normalize context menu using @popperjs
+ normalize context children too + add setting to toggle context children via click or hover + add a select setting component + remove dead teleport code from sidebar tabs wrapper + general clean up
This commit is contained in:
committed by
Mungai Njoroge
parent
4e0837a627
commit
bbe7984e4e
@@ -1,22 +1,8 @@
|
||||
<template>
|
||||
<div
|
||||
class="context-menu rounded shadow-lg"
|
||||
class="context-menu rounded shadow-lg no-select"
|
||||
ref="contextMenu"
|
||||
:class="[
|
||||
{ 'context-menu-visible': context.visible },
|
||||
{ 'context-normalizedX': context.normalizedX },
|
||||
{
|
||||
'context-normalizedY': context.normalizedY,
|
||||
},
|
||||
{
|
||||
'context-many-kids': context.hasManyChildren(),
|
||||
},
|
||||
]"
|
||||
id="context-menu"
|
||||
:style="{
|
||||
left: context.x + 'px',
|
||||
top: context.y + 'px',
|
||||
}"
|
||||
>
|
||||
<ContextItem
|
||||
class="context-item"
|
||||
@@ -24,7 +10,8 @@
|
||||
:key="option.label"
|
||||
:class="[{ critical: option.critical }, option.type]"
|
||||
:option="option"
|
||||
@click="option.action && option.action()"
|
||||
:childrenShowMode="settings.contextChildrenShowMode"
|
||||
@hideContextMenu="context.hideContextMenu()"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -34,9 +21,12 @@ import { ref } from "vue";
|
||||
import { onClickOutside } from "@vueuse/core";
|
||||
|
||||
import useContextStore from "../stores/context";
|
||||
import ContextItem from "./Contextmenu/ContextItem.vue";
|
||||
const context = useContextStore();
|
||||
import useSettingsStore from "../stores/settings";
|
||||
|
||||
import ContextItem from "./Contextmenu/ContextItem.vue";
|
||||
|
||||
const context = useContextStore();
|
||||
const settings = useSettingsStore();
|
||||
const contextMenu = ref<HTMLElement>();
|
||||
|
||||
let clickCount = 0;
|
||||
@@ -64,6 +54,7 @@ onClickOutside(contextMenu, (e) => {
|
||||
width: 12rem;
|
||||
z-index: 10000 !important;
|
||||
transform: scale(0);
|
||||
height: min-content;
|
||||
|
||||
padding: $small 0;
|
||||
background: $context;
|
||||
@@ -81,32 +72,4 @@ onClickOutside(contextMenu, (e) => {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.context-menu-visible {
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
.context-normalizedX {
|
||||
.more {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.context-item > .children {
|
||||
left: -13rem;
|
||||
transform-origin: center right;
|
||||
}
|
||||
}
|
||||
|
||||
.context-normalizedY {
|
||||
.context-item > .children {
|
||||
transform-origin: bottom right;
|
||||
top: -0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.context-many-kids {
|
||||
.context-item > .children {
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,15 +1,33 @@
|
||||
<template>
|
||||
<div class="context-item">
|
||||
<div
|
||||
class="context-item"
|
||||
@mouseenter="
|
||||
option.children &&
|
||||
childrenShowMode === contextChildrenShowMode.hover &&
|
||||
showChildren()
|
||||
"
|
||||
@mouseleave="
|
||||
option.children &&
|
||||
childrenShowMode === contextChildrenShowMode.hover &&
|
||||
hideChildren()
|
||||
"
|
||||
@click="runAction"
|
||||
ref="parentRef"
|
||||
>
|
||||
<div class="icon image" :class="option.icon"></div>
|
||||
<div class="label ellip">{{ option.label }}</div>
|
||||
<div class="more image" v-if="option.children"></div>
|
||||
<div class="children rounded shadow-sm" v-if="option.children">
|
||||
<div
|
||||
class="children rounded shadow-sm"
|
||||
v-if="option.children"
|
||||
ref="childRef"
|
||||
>
|
||||
<div
|
||||
class="context-item"
|
||||
v-for="child in option.children"
|
||||
:key="child.label"
|
||||
:class="[{ critical: child.critical }, child.type]"
|
||||
@click="child.action && child.action()"
|
||||
@click="child.action && runChildAction(child.action)"
|
||||
>
|
||||
<div class="label ellip">
|
||||
{{ child.label }}
|
||||
@@ -20,11 +38,83 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Option } from "@/interfaces";
|
||||
import { ref } from "vue";
|
||||
import { createPopper, Instance } from "@popperjs/core";
|
||||
|
||||
defineProps<{
|
||||
import { Option } from "@/interfaces";
|
||||
import { contextChildrenShowMode } from "@/composables/enums";
|
||||
|
||||
const props = defineProps<{
|
||||
option: Option;
|
||||
childrenShowMode: contextChildrenShowMode;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: "hideContextMenu"): void;
|
||||
}>();
|
||||
|
||||
const parentRef = ref<HTMLElement>();
|
||||
const childRef = ref<HTMLElement>();
|
||||
const childrenShown = ref(false);
|
||||
|
||||
let popperInstance: Instance | null = null;
|
||||
|
||||
function showChildren() {
|
||||
if (childrenShown.value) {
|
||||
childrenShown.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
popperInstance = createPopper(
|
||||
parentRef.value as HTMLElement,
|
||||
childRef.value as HTMLElement,
|
||||
{
|
||||
placement: "right-start",
|
||||
modifiers: [
|
||||
{
|
||||
name: "offset",
|
||||
options: {
|
||||
offset: [-5, -2],
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
);
|
||||
childRef.value ? (childRef.value.style.visibility = "visible") : null;
|
||||
childrenShown.value = true;
|
||||
}
|
||||
|
||||
function hideChildren() {
|
||||
childRef.value ? (childRef.value.style.visibility = "hidden") : null;
|
||||
popperInstance?.destroy();
|
||||
childrenShown.value = false;
|
||||
}
|
||||
|
||||
function hideContextMenu() {
|
||||
if (props.option.children) return;
|
||||
emit("hideContextMenu");
|
||||
}
|
||||
|
||||
function runAction() {
|
||||
if (props.option.children) {
|
||||
if (childrenShown.value) {
|
||||
console.log("what");
|
||||
hideChildren();
|
||||
return;
|
||||
}
|
||||
|
||||
showChildren();
|
||||
return;
|
||||
}
|
||||
|
||||
props.option.action && props.option.action();
|
||||
hideContextMenu();
|
||||
}
|
||||
|
||||
function runChildAction(action: () => void) {
|
||||
action();
|
||||
emit("hideContextMenu");
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@@ -40,19 +130,15 @@ defineProps<{
|
||||
width: 1.5rem;
|
||||
position: absolute;
|
||||
right: $small;
|
||||
background-image: url("../assets/icons/expand.svg");
|
||||
background-image: url("../../assets/icons/expand.svg");
|
||||
}
|
||||
|
||||
.children {
|
||||
position: absolute;
|
||||
right: -13rem;
|
||||
width: 13rem;
|
||||
top: -0.5rem;
|
||||
max-height: 23.5rem;
|
||||
|
||||
background-color: $context;
|
||||
transform: scale(0);
|
||||
transform-origin: top left;
|
||||
padding: $small 0;
|
||||
|
||||
.context-item {
|
||||
@@ -66,12 +152,12 @@ defineProps<{
|
||||
|
||||
&:hover {
|
||||
background: $darkestblue;
|
||||
}
|
||||
|
||||
.children {
|
||||
transform: scale(1);
|
||||
transition: transform 0.1s ease-in-out;
|
||||
transition-delay: 0.3s;
|
||||
}
|
||||
.children {
|
||||
transform: scale(0);
|
||||
overflow: auto;
|
||||
max-height: calc(100vh - 10rem);
|
||||
}
|
||||
|
||||
.icon {
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
key-field="id"
|
||||
v-slot="{ item, index }"
|
||||
>
|
||||
|
||||
<TrackItem
|
||||
:index="index"
|
||||
:track="item.track"
|
||||
@@ -52,7 +51,7 @@ function playFromQueue(index: number) {
|
||||
function scrollToCurrent() {
|
||||
const elem = document.getElementById("queue-scrollable") as HTMLElement;
|
||||
|
||||
const top = queue.currentindex * itemHeight - itemHeight;
|
||||
const top = (queue.currentindex - 1) * itemHeight;
|
||||
elem.scroll({
|
||||
top,
|
||||
behavior: "smooth",
|
||||
|
||||
@@ -1,19 +1,17 @@
|
||||
<template>
|
||||
<div id="right-tabs" class="rounded">
|
||||
<div class="tab-buttons-wrapper">
|
||||
<Teleport :disabled="!isOnSearchPage" to="#nav-tab-headers">
|
||||
<div class="tabheaders rounded-sm no-scroll">
|
||||
<div
|
||||
class="tab"
|
||||
v-for="tab in tabs"
|
||||
:key="tab"
|
||||
@click="switchTab(tab)"
|
||||
:class="{ activetab: tab === currentTab }"
|
||||
>
|
||||
{{ tab }}
|
||||
</div>
|
||||
<div class="tabheaders rounded-sm no-scroll">
|
||||
<div
|
||||
class="tab"
|
||||
v-for="tab in tabs"
|
||||
:key="tab"
|
||||
@click="switchTab(tab)"
|
||||
:class="{ activetab: tab === currentTab }"
|
||||
>
|
||||
{{ tab }}
|
||||
</div>
|
||||
</Teleport>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="tab-content" v-auto-animate>
|
||||
@@ -24,7 +22,6 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
isOnSearchPage?: boolean;
|
||||
tabs: string[];
|
||||
currentTab: string;
|
||||
}>();
|
||||
|
||||
@@ -26,11 +26,13 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from "vue";
|
||||
import { ref } from "vue";
|
||||
|
||||
import useTabStore from "@/stores/tabs";
|
||||
import useSearchStore from "@/stores/search";
|
||||
|
||||
import BackSvg from "@/assets/icons/arrow.svg";
|
||||
import SearchSvg from "@/assets/icons/search.svg";
|
||||
import useSearchStore from "@/stores/search";
|
||||
import useTabStore from "@/stores/tabs";
|
||||
|
||||
const props = defineProps<{
|
||||
on_nav?: boolean;
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
<template>
|
||||
<div class="setting-select rounded-sm no-scroll">
|
||||
<div
|
||||
class="option"
|
||||
v-for="option in optionsWithActive"
|
||||
:key="option.title"
|
||||
:class="{ active: option.active }"
|
||||
@click="setterFn(option.value)"
|
||||
>
|
||||
{{ option.title }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { SettingOption } from "@/interfaces/settings";
|
||||
import { computed } from "vue";
|
||||
|
||||
const props = defineProps<{
|
||||
options: SettingOption[] | undefined;
|
||||
source: () => string;
|
||||
setterFn: (value: any) => void;
|
||||
}>();
|
||||
|
||||
const optionsWithActive = computed(() => {
|
||||
return props.options?.map((option) => {
|
||||
return {
|
||||
...option,
|
||||
active: option.value === props.source(),
|
||||
};
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.setting-select {
|
||||
display: flex;
|
||||
background-color: $gray3;
|
||||
|
||||
.option {
|
||||
padding: 0.5rem;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
min-width: 4rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.option.active {
|
||||
background-color: $darkestblue;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -19,6 +19,12 @@
|
||||
@click="setting.action()"
|
||||
:state="setting.source()"
|
||||
/>
|
||||
<Select
|
||||
v-if="setting.type === SettingType.select"
|
||||
:options="setting.options"
|
||||
:source="setting.source"
|
||||
:setterFn="setting.action"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -26,8 +32,11 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { SettingGroup, SettingType } from "@/interfaces/settings";
|
||||
import { SettingType } from "@/settings/enums";
|
||||
import { SettingGroup } from "@/interfaces/settings";
|
||||
|
||||
import Switch from "./Components/Switch.vue";
|
||||
import Select from "./Components/Select.vue";
|
||||
|
||||
defineProps<{
|
||||
group: SettingGroup;
|
||||
@@ -39,7 +48,7 @@ defineProps<{
|
||||
display: grid;
|
||||
gap: $small;
|
||||
margin-top: 2rem;
|
||||
|
||||
|
||||
&:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
@@ -51,7 +60,7 @@ defineProps<{
|
||||
h4 {
|
||||
margin: $small auto;
|
||||
}
|
||||
|
||||
|
||||
.desc {
|
||||
opacity: 0.5;
|
||||
font-size: 0.8rem;
|
||||
|
||||
@@ -94,7 +94,7 @@ function emitUpdate() {
|
||||
emit("playThis");
|
||||
}
|
||||
|
||||
function showMenu(e: Event) {
|
||||
function showMenu(e: MouseEvent) {
|
||||
showContext(e, props.track, options_button_clicked);
|
||||
}
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user