mirror of
https://github.com/moku-project/Moku.git
synced 2026-06-14 18:00:04 -05:00
Fix: Settings Drop-down Fix V1 (WIP) (#102)
This commit is contained in:
@@ -4,7 +4,7 @@
|
||||
import { settingsState, updateSettings } from '$lib/state/settings.svelte'
|
||||
import { eventToKeybind } from '$lib/core/keybinds/keybindEngine'
|
||||
import type { Keybinds } from '$lib/core/keybinds/defaultBinds'
|
||||
import { anchorToModal } from '$lib/core/ui/selectPortal'
|
||||
import { selectPortal } from '$lib/core/ui/selectPortal'
|
||||
|
||||
import GeneralSettings from './sections/GeneralSettings.svelte'
|
||||
import AppearanceSettings from './sections/AppearanceSettings.svelte'
|
||||
@@ -175,13 +175,13 @@
|
||||
|
||||
<div class="s-content-body" bind:this={contentBodyEl}>
|
||||
{#if tab === 'general'}
|
||||
<GeneralSettings {selectOpen} {closingSelect} {toggleSelect} {registerTrigger} {getTrigger} {anchorToModal} {modalEl} {anims} />
|
||||
<GeneralSettings {selectOpen} {closingSelect} {toggleSelect} {registerTrigger} {getTrigger} {selectPortal} {modalEl} {anims} />
|
||||
{:else if tab === 'appearance'}
|
||||
<AppearanceSettings {selectOpen} {closingSelect} {toggleSelect} {registerTrigger} {getTrigger} {anchorToModal} {modalEl} {anims} {onOpenThemeEditor} />
|
||||
<AppearanceSettings {selectOpen} {closingSelect} {toggleSelect} {registerTrigger} {getTrigger} {selectPortal} {modalEl} {anims} {onOpenThemeEditor} />
|
||||
{:else if tab === 'reader'}
|
||||
<ReaderSettings {selectOpen} {closingSelect} {toggleSelect} {registerTrigger} {getTrigger} {anchorToModal} {modalEl} {anims} />
|
||||
<ReaderSettings {selectOpen} {closingSelect} {toggleSelect} {registerTrigger} {getTrigger} {selectPortal} {modalEl} {anims} />
|
||||
{:else if tab === 'library'}
|
||||
<LibrarySettings {selectOpen} {closingSelect} {toggleSelect} {registerTrigger} {getTrigger} {anchorToModal} {modalEl} {anims} />
|
||||
<LibrarySettings {selectOpen} {closingSelect} {toggleSelect} {registerTrigger} {getTrigger} {selectPortal} {modalEl} {anims} />
|
||||
{:else if tab === 'automation'}
|
||||
<AutomationSettings />
|
||||
{:else if tab === 'performance'}
|
||||
@@ -189,13 +189,13 @@
|
||||
{:else if tab === 'keybinds'}
|
||||
<KeybindsSettings bind:listeningKey />
|
||||
{:else if tab === 'storage'}
|
||||
<StorageSettings {selectOpen} {closingSelect} {toggleSelect} {registerTrigger} {getTrigger} {anchorToModal} {modalEl} />
|
||||
<StorageSettings {selectOpen} {closingSelect} {toggleSelect} {registerTrigger} {getTrigger} {selectPortal} {modalEl} />
|
||||
{:else if tab === 'folders'}
|
||||
<FoldersSettings />
|
||||
{:else if tab === 'tracking'}
|
||||
<TrackingSettings />
|
||||
{:else if tab === 'security'}
|
||||
<SecuritySettings {selectOpen} {toggleSelect} {registerTrigger} {getTrigger} {anchorToModal} {modalEl} />
|
||||
<SecuritySettings {selectOpen} {toggleSelect} {registerTrigger} {getTrigger} {selectPortal} {modalEl} />
|
||||
{:else if tab === 'content'}
|
||||
<ContentSettings />
|
||||
{:else if tab === 'about'}
|
||||
|
||||
@@ -2,17 +2,23 @@
|
||||
import { settingsState, updateSettings } from '$lib/state/settings.svelte'
|
||||
import { homeState } from '$lib/state/home.svelte'
|
||||
import type { Settings } from '$lib/types/settings'
|
||||
import type { Action } from 'svelte/action'
|
||||
|
||||
interface Props {
|
||||
selectOpen: string | null
|
||||
closingSelect?: string | null
|
||||
toggleSelect: (id: string) => void
|
||||
anims: boolean
|
||||
selectOpen: string | null
|
||||
closingSelect?: string | null
|
||||
toggleSelect: (id: string) => void
|
||||
registerTrigger: (id: string, el: HTMLElement) => void
|
||||
getTrigger: (id: string) => HTMLElement | undefined
|
||||
selectPortal: Action<HTMLElement, HTMLElement | undefined>
|
||||
anims: boolean
|
||||
}
|
||||
let { selectOpen, toggleSelect, anims }: Props = $props()
|
||||
let { selectOpen, closingSelect, toggleSelect, registerTrigger, getTrigger, selectPortal, anims }: Props = $props()
|
||||
|
||||
let triggerSortDir = $state<HTMLButtonElement>(null!)
|
||||
|
||||
$effect(() => { if (triggerSortDir) registerTrigger('sort-dir', triggerSortDir) })
|
||||
|
||||
function clearHistory() {
|
||||
homeState.history = []
|
||||
}
|
||||
@@ -54,8 +60,8 @@
|
||||
<span>{{ 'desc':'Newest first','asc':'Oldest first' }[settingsState.settings.chapterSortDir]}</span>
|
||||
<svg class="s-select-caret" class:open={selectOpen === 'sort-dir'} width="10" height="6" viewBox="0 0 10 6"><path d="M0 0l5 6 5-6" fill="currentColor"/></svg>
|
||||
</button>
|
||||
{#if selectOpen === 'sort-dir'}
|
||||
<div class="s-select-menu" class:anims>
|
||||
{#if selectOpen === 'sort-dir' || closingSelect === 'sort-dir'}
|
||||
<div use:selectPortal={getTrigger('sort-dir')} class="s-select-menu" class:anims class:closing={closingSelect === 'sort-dir'}>
|
||||
{#each [['desc','Newest first'],['asc','Oldest first']] as [v, l]}
|
||||
<button class="s-select-option" class:active={settingsState.settings.chapterSortDir === v} onclick={() => { updateSettings({ chapterSortDir: v as Settings['chapterSortDir'] }); toggleSelect('sort-dir') }}>{l}</button>
|
||||
{/each}
|
||||
|
||||
@@ -1,18 +1,26 @@
|
||||
<script lang="ts">
|
||||
import { settingsState, updateSettings } from '$lib/state/settings.svelte'
|
||||
import type { Settings, FitMode } from '$lib/types/settings'
|
||||
import type { Action } from 'svelte/action'
|
||||
|
||||
interface Props {
|
||||
selectOpen: string | null
|
||||
closingSelect?: string | null
|
||||
toggleSelect: (id: string) => void
|
||||
anims: boolean
|
||||
selectOpen: string | null
|
||||
closingSelect?: string | null
|
||||
toggleSelect: (id: string) => void
|
||||
registerTrigger: (id: string, el: HTMLElement) => void
|
||||
getTrigger: (id: string) => HTMLElement | undefined
|
||||
selectPortal: Action<HTMLElement, HTMLElement | undefined>
|
||||
anims: boolean
|
||||
}
|
||||
let { selectOpen, toggleSelect, anims }: Props = $props()
|
||||
let { selectOpen, closingSelect, toggleSelect, registerTrigger, getTrigger, selectPortal, anims }: Props = $props()
|
||||
|
||||
let triggerPageStyle = $state<HTMLButtonElement>(null!)
|
||||
let triggerReadingDir = $state<HTMLButtonElement>(null!)
|
||||
let triggerFitMode = $state<HTMLButtonElement>(null!)
|
||||
|
||||
$effect(() => { if (triggerPageStyle) registerTrigger('page-style', triggerPageStyle) })
|
||||
$effect(() => { if (triggerReadingDir) registerTrigger('reading-dir', triggerReadingDir) })
|
||||
$effect(() => { if (triggerFitMode) registerTrigger('fit-mode', triggerFitMode) })
|
||||
</script>
|
||||
|
||||
<div class="s-panel">
|
||||
@@ -27,8 +35,8 @@
|
||||
<span>{{ 'single':'Single page','longstrip':'Long strip' }[settingsState.settings.pageStyle === 'double' ? 'single' : settingsState.settings.pageStyle]}</span>
|
||||
<svg class="s-select-caret" class:open={selectOpen === 'page-style'} width="10" height="6" viewBox="0 0 10 6"><path d="M0 0l5 6 5-6" fill="currentColor"/></svg>
|
||||
</button>
|
||||
{#if selectOpen === 'page-style'}
|
||||
<div class="s-select-menu" class:anims>
|
||||
{#if selectOpen === 'page-style' || closingSelect === 'page-style'}
|
||||
<div use:selectPortal={getTrigger('page-style')} class="s-select-menu" class:anims class:closing={closingSelect === 'page-style'}>
|
||||
{#each [['single','Single page'],['longstrip','Long strip']] as [v, l]}
|
||||
<button class="s-select-option" class:active={(settingsState.settings.pageStyle === 'double' ? 'single' : settingsState.settings.pageStyle) === v} onclick={() => { updateSettings({ pageStyle: v as Settings['pageStyle'] }); toggleSelect('page-style') }}>{l}</button>
|
||||
{/each}
|
||||
@@ -43,8 +51,8 @@
|
||||
<span>{{ 'ltr':'Left to right','rtl':'Right to left' }[settingsState.settings.readingDirection]}</span>
|
||||
<svg class="s-select-caret" class:open={selectOpen === 'reading-dir'} width="10" height="6" viewBox="0 0 10 6"><path d="M0 0l5 6 5-6" fill="currentColor"/></svg>
|
||||
</button>
|
||||
{#if selectOpen === 'reading-dir'}
|
||||
<div class="s-select-menu" class:anims>
|
||||
{#if selectOpen === 'reading-dir' || closingSelect === 'reading-dir'}
|
||||
<div use:selectPortal={getTrigger('reading-dir')} class="s-select-menu" class:anims class:closing={closingSelect === 'reading-dir'}>
|
||||
{#each [['ltr','Left to right'],['rtl','Right to left']] as [v, l]}
|
||||
<button class="s-select-option" class:active={settingsState.settings.readingDirection === v} onclick={() => { updateSettings({ readingDirection: v as Settings['readingDirection'] }); toggleSelect('reading-dir') }}>{l}</button>
|
||||
{/each}
|
||||
@@ -81,8 +89,8 @@
|
||||
<span>{{ 'width':'Fit width','height':'Fit height','screen':'Fit screen','original':'Original (1:1)' }[settingsState.settings.fitMode ?? 'width']}</span>
|
||||
<svg class="s-select-caret" class:open={selectOpen === 'fit-mode'} width="10" height="6" viewBox="0 0 10 6"><path d="M0 0l5 6 5-6" fill="currentColor"/></svg>
|
||||
</button>
|
||||
{#if selectOpen === 'fit-mode'}
|
||||
<div class="s-select-menu" class:anims>
|
||||
{#if selectOpen === 'fit-mode' || closingSelect === 'fit-mode'}
|
||||
<div use:selectPortal={getTrigger('fit-mode')} class="s-select-menu" class:anims class:closing={closingSelect === 'fit-mode'}>
|
||||
{#each [['width','Fit width'],['height','Fit height'],['screen','Fit screen'],['original','Original (1:1)']] as [v, l]}
|
||||
<button class="s-select-option" class:active={(settingsState.settings.fitMode ?? 'width') === v} onclick={() => { updateSettings({ fitMode: v as FitMode }); toggleSelect('fit-mode') }}>{l}</button>
|
||||
{/each}
|
||||
|
||||
@@ -1,19 +1,13 @@
|
||||
/**
|
||||
* position:fixed dropdown anchored to a trigger element.
|
||||
*
|
||||
* getBoundingClientRect() returns full viewport coords.
|
||||
* position:fixed is also relative to the viewport.
|
||||
* So we just divide by zoom — no sidebar/titlebar subtraction needed
|
||||
* (those subtractions are only needed in ContextMenu because its x/y come
|
||||
* from a MouseEvent which is relative to the zoomed content area, not the viewport).
|
||||
*/
|
||||
export function selectPortal(
|
||||
node: HTMLElement,
|
||||
trigger: HTMLElement | undefined,
|
||||
): { update(t: HTMLElement | undefined): void; destroy(): void } {
|
||||
let currentTrigger = trigger
|
||||
|
||||
node.style.position = 'fixed'
|
||||
node.style.visibility = 'hidden'
|
||||
node.style.zIndex = '99999'
|
||||
document.body.appendChild(node)
|
||||
|
||||
function getZoom(): number {
|
||||
const raw = parseFloat(document.documentElement.style.zoom || '1') || 1
|
||||
@@ -23,33 +17,26 @@ export function selectPortal(
|
||||
function position() {
|
||||
if (!currentTrigger) return
|
||||
|
||||
const zoom = getZoom()
|
||||
const r = currentTrigger.getBoundingClientRect()
|
||||
|
||||
// Convert viewport px → CSS px by dividing by zoom
|
||||
const left = r.left / zoom
|
||||
const top = r.top / zoom
|
||||
const bottom = r.bottom / zoom
|
||||
const width = r.width / zoom
|
||||
|
||||
const vw = window.innerWidth / zoom
|
||||
const vh = window.innerHeight / zoom
|
||||
const zoom = getZoom()
|
||||
const r = currentTrigger.getBoundingClientRect()
|
||||
const vw = window.innerWidth
|
||||
const vh = window.innerHeight
|
||||
|
||||
const menuH = node.offsetHeight
|
||||
const menuW = node.offsetWidth
|
||||
|
||||
const above = menuH > 0 && (vh - bottom) < menuH + 8 && top > menuH + 8
|
||||
const above = menuH > 0 && (vh - r.bottom) < menuH + 8 && r.top > menuH + 8
|
||||
const cssLeft = Math.max(4, r.left + menuW > vw ? r.right - menuW : r.left) / zoom
|
||||
const cssTop = (above ? r.top - menuH - 4 : r.bottom + 4) / zoom
|
||||
|
||||
const cssLeft = Math.min(left, vw - menuW - 4)
|
||||
const cssTop = above ? top - menuH - 4 : bottom + 4
|
||||
|
||||
node.style.left = `${Math.max(4, cssLeft)}px`
|
||||
node.style.left = `${cssLeft}px`
|
||||
node.style.top = `${cssTop}px`
|
||||
node.style.minWidth = `${width}px`
|
||||
node.style.minWidth = `${r.width / zoom}px`
|
||||
node.style.visibility = 'visible'
|
||||
}
|
||||
|
||||
requestAnimationFrame(() => position())
|
||||
|
||||
window.addEventListener('scroll', position, { capture: true, passive: true })
|
||||
window.addEventListener('resize', position, { passive: true })
|
||||
|
||||
@@ -62,6 +49,7 @@ export function selectPortal(
|
||||
destroy() {
|
||||
window.removeEventListener('scroll', position, true)
|
||||
window.removeEventListener('resize', position)
|
||||
if (document.body.contains(node)) node.remove()
|
||||
},
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user