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:
geoffrey45
2022-10-09 00:23:01 +03:00
committed by Mungai Njoroge
parent 4e0837a627
commit bbe7984e4e
24 changed files with 314 additions and 143 deletions
+9 -46
View File
@@ -1,22 +1,8 @@
<template> <template>
<div <div
class="context-menu rounded shadow-lg" class="context-menu rounded shadow-lg no-select"
ref="contextMenu" ref="contextMenu"
:class="[
{ 'context-menu-visible': context.visible },
{ 'context-normalizedX': context.normalizedX },
{
'context-normalizedY': context.normalizedY,
},
{
'context-many-kids': context.hasManyChildren(),
},
]"
id="context-menu" id="context-menu"
:style="{
left: context.x + 'px',
top: context.y + 'px',
}"
> >
<ContextItem <ContextItem
class="context-item" class="context-item"
@@ -24,7 +10,8 @@
:key="option.label" :key="option.label"
:class="[{ critical: option.critical }, option.type]" :class="[{ critical: option.critical }, option.type]"
:option="option" :option="option"
@click="option.action && option.action()" :childrenShowMode="settings.contextChildrenShowMode"
@hideContextMenu="context.hideContextMenu()"
/> />
</div> </div>
</template> </template>
@@ -34,9 +21,12 @@ import { ref } from "vue";
import { onClickOutside } from "@vueuse/core"; import { onClickOutside } from "@vueuse/core";
import useContextStore from "../stores/context"; import useContextStore from "../stores/context";
import ContextItem from "./Contextmenu/ContextItem.vue"; import useSettingsStore from "../stores/settings";
const context = useContextStore();
import ContextItem from "./Contextmenu/ContextItem.vue";
const context = useContextStore();
const settings = useSettingsStore();
const contextMenu = ref<HTMLElement>(); const contextMenu = ref<HTMLElement>();
let clickCount = 0; let clickCount = 0;
@@ -64,6 +54,7 @@ onClickOutside(contextMenu, (e) => {
width: 12rem; width: 12rem;
z-index: 10000 !important; z-index: 10000 !important;
transform: scale(0); transform: scale(0);
height: min-content;
padding: $small 0; padding: $small 0;
background: $context; 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> </style>
+100 -14
View File
@@ -1,15 +1,33 @@
<template> <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="icon image" :class="option.icon"></div>
<div class="label ellip">{{ option.label }}</div> <div class="label ellip">{{ option.label }}</div>
<div class="more image" v-if="option.children"></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 <div
class="context-item" class="context-item"
v-for="child in option.children" v-for="child in option.children"
:key="child.label" :key="child.label"
:class="[{ critical: child.critical }, child.type]" :class="[{ critical: child.critical }, child.type]"
@click="child.action && child.action()" @click="child.action && runChildAction(child.action)"
> >
<div class="label ellip"> <div class="label ellip">
{{ child.label }} {{ child.label }}
@@ -20,11 +38,83 @@
</template> </template>
<script setup lang="ts"> <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; 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> </script>
<style lang="scss"> <style lang="scss">
@@ -40,19 +130,15 @@ defineProps<{
width: 1.5rem; width: 1.5rem;
position: absolute; position: absolute;
right: $small; right: $small;
background-image: url("../assets/icons/expand.svg"); background-image: url("../../assets/icons/expand.svg");
} }
.children { .children {
position: absolute; position: absolute;
right: -13rem;
width: 13rem; width: 13rem;
top: -0.5rem;
max-height: 23.5rem;
background-color: $context; background-color: $context;
transform: scale(0); transform: scale(0);
transform-origin: top left;
padding: $small 0; padding: $small 0;
.context-item { .context-item {
@@ -66,12 +152,12 @@ defineProps<{
&:hover { &:hover {
background: $darkestblue; background: $darkestblue;
}
.children { .children {
transform: scale(1); transform: scale(0);
transition: transform 0.1s ease-in-out; overflow: auto;
transition-delay: 0.3s; max-height: calc(100vh - 10rem);
}
} }
.icon { .icon {
+1 -2
View File
@@ -13,7 +13,6 @@
key-field="id" key-field="id"
v-slot="{ item, index }" v-slot="{ item, index }"
> >
<TrackItem <TrackItem
:index="index" :index="index"
:track="item.track" :track="item.track"
@@ -52,7 +51,7 @@ function playFromQueue(index: number) {
function scrollToCurrent() { function scrollToCurrent() {
const elem = document.getElementById("queue-scrollable") as HTMLElement; const elem = document.getElementById("queue-scrollable") as HTMLElement;
const top = queue.currentindex * itemHeight - itemHeight; const top = (queue.currentindex - 1) * itemHeight;
elem.scroll({ elem.scroll({
top, top,
behavior: "smooth", behavior: "smooth",
@@ -1,7 +1,6 @@
<template> <template>
<div id="right-tabs" class="rounded"> <div id="right-tabs" class="rounded">
<div class="tab-buttons-wrapper"> <div class="tab-buttons-wrapper">
<Teleport :disabled="!isOnSearchPage" to="#nav-tab-headers">
<div class="tabheaders rounded-sm no-scroll"> <div class="tabheaders rounded-sm no-scroll">
<div <div
class="tab" class="tab"
@@ -13,7 +12,6 @@
{{ tab }} {{ tab }}
</div> </div>
</div> </div>
</Teleport>
</div> </div>
<div id="tab-content" v-auto-animate> <div id="tab-content" v-auto-animate>
@@ -24,7 +22,6 @@
<script setup lang="ts"> <script setup lang="ts">
defineProps<{ defineProps<{
isOnSearchPage?: boolean;
tabs: string[]; tabs: string[];
currentTab: string; currentTab: string;
}>(); }>();
+5 -3
View File
@@ -26,11 +26,13 @@
</template> </template>
<script setup lang="ts"> <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 BackSvg from "@/assets/icons/arrow.svg";
import SearchSvg from "@/assets/icons/search.svg"; import SearchSvg from "@/assets/icons/search.svg";
import useSearchStore from "@/stores/search";
import useTabStore from "@/stores/tabs";
const props = defineProps<{ const props = defineProps<{
on_nav?: boolean; 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>
+10 -1
View File
@@ -19,6 +19,12 @@
@click="setting.action()" @click="setting.action()"
:state="setting.source()" :state="setting.source()"
/> />
<Select
v-if="setting.type === SettingType.select"
:options="setting.options"
:source="setting.source"
:setterFn="setting.action"
/>
</div> </div>
</div> </div>
</div> </div>
@@ -26,8 +32,11 @@
</template> </template>
<script setup lang="ts"> <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 Switch from "./Components/Switch.vue";
import Select from "./Components/Select.vue";
defineProps<{ defineProps<{
group: SettingGroup; group: SettingGroup;
+1 -1
View File
@@ -94,7 +94,7 @@ function emitUpdate() {
emit("playThis"); emit("playThis");
} }
function showMenu(e: Event) { function showMenu(e: MouseEvent) {
showContext(e, props.track, options_button_clicked); showContext(e, props.track, options_button_clicked);
} }
</script> </script>
+2 -2
View File
@@ -9,13 +9,13 @@ import { Track } from "@/interfaces";
import trackContext from "@/contexts/track_context"; import trackContext from "@/contexts/track_context";
export const showTrackContextMenu = ( export const showTrackContextMenu = (
e: Event, e: MouseEvent,
track: Track, track: Track,
flag: Ref<boolean> flag: Ref<boolean>
) => { ) => {
const menu = useContextStore(); const menu = useContextStore();
const options = trackContext(track, useModalStore, useQueueStore); const options = () => trackContext(track, useModalStore, useQueueStore);
menu.showContextMenu(e, options, ContextSrc.Track); menu.showContextMenu(e, options, ContextSrc.Track);
flag.value = true; flag.value = true;
+5
View File
@@ -46,3 +46,8 @@ export const FuseTrackOptions = {
{ name: "albumartist", weight: 0.25 }, { name: "albumartist", weight: 0.25 },
], ],
}; };
export enum contextChildrenShowMode {
click = "click",
hover = "hover",
}
+1 -6
View File
@@ -1,9 +1,4 @@
export enum SettingType { import { SettingType } from "@/settings/enums";
text,
select,
multiselect,
binary,
}
export interface SettingOption { export interface SettingOption {
title: string; title: string;
+6
View File
@@ -0,0 +1,6 @@
export enum SettingType {
text,
select,
multiselect,
binary,
}
@@ -0,0 +1,27 @@
import { Setting } from "@/interfaces/settings";
import { SettingType } from "@/settings/enums";
import { contextChildrenShowModeStrings as showModeStr } from "./../strings";
import useSettingsStore from "@/stores/settings";
import { contextChildrenShowMode as mode } from "@/composables/enums";
const settings = useSettingsStore;
const context_children_show_mode: Setting = {
title: showModeStr.settings.show_mode,
type: SettingType.select,
options: [
{
title: mode.click,
value: mode.click,
},
{
title: mode.hover,
value: mode.hover,
},
],
source: () => settings().contextChildrenShowMode,
action: (value: mode) => settings().setContextChildrenShowMode(value),
};
export default [context_children_show_mode];
+4 -2
View File
@@ -1,7 +1,9 @@
import { Setting, SettingType } from "@/interfaces/settings"; import { SettingType } from "../enums";
import useSettingsStore from "@/stores/settings"; import { Setting } from "@/interfaces/settings";
import { appWidthStrings } from "./../strings"; import { appWidthStrings } from "./../strings";
import useSettingsStore from "@/stores/settings";
const settings = useSettingsStore; const settings = useSettingsStore;
const extend_to_full_width: Setting = { const extend_to_full_width: Setting = {
+6 -1
View File
@@ -1,5 +1,6 @@
import { SettingCategory } from "@/interfaces/settings"; import { SettingCategory } from "@/interfaces/settings";
import * as strings from "../strings"; import * as strings from "../strings";
import contextChildrenShowMode from "./context-children-show-mode";
import extendWidth from "./extend-width"; import extendWidth from "./extend-width";
import nowPlaying from "./now-playing"; import nowPlaying from "./now-playing";
import sidebarSettings from "./sidebar"; import sidebarSettings from "./sidebar";
@@ -10,7 +11,11 @@ export default {
title: "General", title: "General",
groups: [ groups: [
{ {
settings: [...sidebarSettings, ...extendWidth], settings: [
...sidebarSettings,
...extendWidth,
...contextChildrenShowMode,
],
}, },
{ {
title: npStrings.title, title: npStrings.title,
+4 -2
View File
@@ -1,7 +1,9 @@
import { Setting, SettingType } from "@/interfaces/settings"; import { SettingType } from "../enums";
import useSettingsStore from "@/stores/settings"; import { Setting } from "@/interfaces/settings";
import { nowPlayingStrings as data } from "../strings"; import { nowPlayingStrings as data } from "../strings";
import useSettingsStore from "@/stores/settings";
const settings = useSettingsStore; const settings = useSettingsStore;
const disable_np_img: Setting = { const disable_np_img: Setting = {
+3 -1
View File
@@ -1,5 +1,7 @@
import { SettingType } from "../enums";
import { sidebarStrings } from "./../strings"; import { sidebarStrings } from "./../strings";
import { Setting, SettingType } from "@/interfaces/settings"; import { Setting } from "@/interfaces/settings";
import useSettingsStore from "@/stores/settings"; import useSettingsStore from "@/stores/settings";
const settings = useSettingsStore; const settings = useSettingsStore;
+6
View File
@@ -28,3 +28,9 @@ export const sidebarStrings = <S>{
use_sidebar: "Show right sidebar", use_sidebar: "Show right sidebar",
}, },
}; };
export const contextChildrenShowModeStrings = <S>{
settings: {
show_mode: "Show context children on",
},
};
+48 -39
View File
@@ -1,70 +1,79 @@
import { defineStore } from "pinia"; import { defineStore } from "pinia";
import normalize from "../composables/normalizeContextMenu";
import { Option } from "../interfaces"; import { Option } from "../interfaces";
import { ContextSrc } from "../composables/enums"; import { ContextSrc } from "../composables/enums";
import { createPopper, VirtualElement } from "@popperjs/core";
function getPlaceholders(length: number) { function generateGetBoundingClientRect(x = 0, y = 0) {
let list: Option[] = []; return () => ({
width: 0,
for (let index = 0; index < length; index++) { height: 0,
list.push("" as Option); top: y,
} right: x,
bottom: y,
return list; left: x,
});
} }
getPlaceholders(5);
export default defineStore("context-menu", { export default defineStore("context-menu", {
state: () => ({ state: () => ({
visible: false, visible: false,
options: getPlaceholders(5), options: {} as Option[],
x: 500,
y: 500,
normalizedX: false,
normalizedY: false,
src: <null | string>"", src: <null | string>"",
elem: <HTMLElement | null>null,
}), }),
actions: { actions: {
showContextMenu( showContextMenu(
e: any, e: MouseEvent,
context_options: Promise<Option[]>, getContextOptions: () => Promise<Option[]>,
src: ContextSrc src: ContextSrc
) { ) {
if (this.visible) { if (this.visible) {
this.visible = false; this.hideContextMenu();
return; return;
} }
this.visible = true; if (this.elem === null) {
context_options.then((options) => { this.elem = document.getElementById("context-menu");
}
const virtualElement = {
getBoundingClientRect: generateGetBoundingClientRect(e.x, e.y),
} as VirtualElement;
getContextOptions()
.then((options) => {
this.options = options; this.options = options;
})
.then(() => {
createPopper(virtualElement, this.elem as HTMLElement, {
placement: "right-start",
modifiers: [
{
name: "flip",
options: {
fallbackPlacements: ["left-start"],
},
},
],
});
}); });
const xy = normalize(e.clientX, e.clientY); this.visible = true;
this.x = xy.normalX;
this.y = xy.normalY;
this.normalizedX = xy.normalizedX;
this.normalizedY = xy.normalizedY;
this.src = src; this.src = src;
// const xy = normalize(e.clientX, e.clientY);
// this.x = xy.normalX;
// this.y = xy.normalY;
// this.normalizedX = xy.normalizedX;
// this.normalizedY = xy.normalizedY;
}, },
hideContextMenu() { hideContextMenu() {
this.visible = false; this.visible = false;
this.src = null; this.src = null;
this.options = []; this.options = [];
}, this.elem ? (this.elem.style.transform = "scale(0)") : null;
hasManyChildren() {
let result = false;
this.options.forEach((option: Option) => {
if (option.children && option.children.length > 9) {
result = true;
}
});
return result;
}, },
}, },
}); });
-1
View File
@@ -1 +0,0 @@
+6 -1
View File
@@ -1,3 +1,4 @@
import { contextChildrenShowMode } from "@/composables/enums";
import { xxl } from "@/composables/useBreakpoints"; import { xxl } from "@/composables/useBreakpoints";
import { defineStore } from "pinia"; import { defineStore } from "pinia";
@@ -6,6 +7,7 @@ export default defineStore("settings", {
use_np_img: false, use_np_img: false,
use_sidebar: true, use_sidebar: true,
extend_width: false, extend_width: false,
contextChildrenShowMode: contextChildrenShowMode.click,
}), }),
actions: { actions: {
toggleUseNPImg() { toggleUseNPImg() {
@@ -17,10 +19,13 @@ export default defineStore("settings", {
toggleExtendWidth() { toggleExtendWidth() {
this.extend_width = !this.extend_width; this.extend_width = !this.extend_width;
}, },
setContextChildrenShowMode(mode: contextChildrenShowMode) {
this.contextChildrenShowMode = mode;
},
}, },
getters: { getters: {
can_extend_width(): boolean { can_extend_width(): boolean {
return xxl.value return xxl.value;
}, },
}, },
persist: true, persist: true,
+1 -1
View File
@@ -1,4 +1,4 @@
declare module "*.svg" { declare module "*.svg" {
const content: string; const content: any;
export default content; export default content;
} }
+1 -1
View File
@@ -54,7 +54,7 @@ function playFromQueue(index: number) {
function scrollToCurrent() { function scrollToCurrent() {
const scrollable = document.getElementById("queue-page-scrollable"); const scrollable = document.getElementById("queue-page-scrollable");
const itemHeight = 64; const itemHeight = 64;
const top = queue.currentindex * itemHeight - itemHeight; const top = (queue.currentindex - 1) * itemHeight;
scrollable?.scrollTo({ scrollable?.scrollTo({
top, top,
+1 -1
View File
@@ -10,7 +10,7 @@
"sourceMap": true, "sourceMap": true,
"resolveJsonModule": true, "resolveJsonModule": true,
"isolatedModules": true, "isolatedModules": true,
"esModuleInterop": true, "esModuleInterop": false,
"lib": ["ESNext", "DOM"], "lib": ["ESNext", "DOM"],
"skipLibCheck": true, "skipLibCheck": true,
"paths": { "paths": {