diff options
| author | syuilo <Syuilotan@yahoo.co.jp> | 2022-09-06 18:21:49 +0900 |
|---|---|---|
| committer | syuilo <Syuilotan@yahoo.co.jp> | 2022-09-06 18:21:49 +0900 |
| commit | a9e13693a593ff1fb4b2ed1f2e1cb90a8ef7bd3b (patch) | |
| tree | de951d9242addd6195c20c23ce6cdf853884760f /packages/client/src/components/ui | |
| parent | refactor(client): use setup syntax (diff) | |
| download | sharkey-a9e13693a593ff1fb4b2ed1f2e1cb90a8ef7bd3b.tar.gz sharkey-a9e13693a593ff1fb4b2ed1f2e1cb90a8ef7bd3b.tar.bz2 sharkey-a9e13693a593ff1fb4b2ed1f2e1cb90a8ef7bd3b.zip | |
refactor(client): refactor file name and directory structure
Diffstat (limited to 'packages/client/src/components/ui')
| -rw-r--r-- | packages/client/src/components/ui/button.vue | 226 | ||||
| -rw-r--r-- | packages/client/src/components/ui/container.vue | 264 | ||||
| -rw-r--r-- | packages/client/src/components/ui/context-menu.vue | 85 | ||||
| -rw-r--r-- | packages/client/src/components/ui/folder.vue | 156 | ||||
| -rw-r--r-- | packages/client/src/components/ui/info.vue | 34 | ||||
| -rw-r--r-- | packages/client/src/components/ui/menu.child.vue | 65 | ||||
| -rw-r--r-- | packages/client/src/components/ui/menu.vue | 364 | ||||
| -rw-r--r-- | packages/client/src/components/ui/modal-window.vue | 146 | ||||
| -rw-r--r-- | packages/client/src/components/ui/modal.vue | 406 | ||||
| -rw-r--r-- | packages/client/src/components/ui/pagination.vue | 317 | ||||
| -rw-r--r-- | packages/client/src/components/ui/popup-menu.vue | 36 | ||||
| -rw-r--r-- | packages/client/src/components/ui/super-menu.vue | 148 | ||||
| -rw-r--r-- | packages/client/src/components/ui/tooltip.vue | 101 | ||||
| -rw-r--r-- | packages/client/src/components/ui/window.vue | 563 |
14 files changed, 0 insertions, 2911 deletions
diff --git a/packages/client/src/components/ui/button.vue b/packages/client/src/components/ui/button.vue deleted file mode 100644 index a052f8f7a9..0000000000 --- a/packages/client/src/components/ui/button.vue +++ /dev/null @@ -1,226 +0,0 @@ -<template> -<button - v-if="!link" class="bghgjjyj _button" - :class="{ inline, primary, gradate, danger, rounded, full }" - :type="type" - @click="emit('click', $event)" - @mousedown="onMousedown" -> - <div ref="ripples" class="ripples"></div> - <div class="content"> - <slot></slot> - </div> -</button> -<MkA - v-else class="bghgjjyj _button" - :class="{ inline, primary, gradate, danger, rounded, full }" - :to="to" - @mousedown="onMousedown" -> - <div ref="ripples" class="ripples"></div> - <div class="content"> - <slot></slot> - </div> -</MkA> -</template> - -<script lang="ts" setup> -import { nextTick, onMounted } from 'vue'; - -const props = defineProps<{ - type?: 'button' | 'submit' | 'reset'; - primary?: boolean; - gradate?: boolean; - rounded?: boolean; - inline?: boolean; - link?: boolean; - to?: string; - autofocus?: boolean; - wait?: boolean; - danger?: boolean; - full?: boolean; -}>(); - -const emit = defineEmits<{ - (ev: 'click', payload: MouseEvent): void; -}>(); - -let el = $ref<HTMLElement | null>(null); -let ripples = $ref<HTMLElement | null>(null); - -onMounted(() => { - if (props.autofocus) { - nextTick(() => { - el!.focus(); - }); - } -}); - -function distance(p, q): number { - return Math.hypot(p.x - q.x, p.y - q.y); -} - -function calcCircleScale(boxW, boxH, circleCenterX, circleCenterY): number { - const origin = { x: circleCenterX, y: circleCenterY }; - const dist1 = distance({ x: 0, y: 0 }, origin); - const dist2 = distance({ x: boxW, y: 0 }, origin); - const dist3 = distance({ x: 0, y: boxH }, origin); - const dist4 = distance({ x: boxW, y: boxH }, origin); - return Math.max(dist1, dist2, dist3, dist4) * 2; -} - -function onMousedown(evt: MouseEvent): void { - const target = evt.target! as HTMLElement; - const rect = target.getBoundingClientRect(); - - const ripple = document.createElement('div'); - ripple.style.top = (evt.clientY - rect.top - 1).toString() + 'px'; - ripple.style.left = (evt.clientX - rect.left - 1).toString() + 'px'; - - ripples!.appendChild(ripple); - - const circleCenterX = evt.clientX - rect.left; - const circleCenterY = evt.clientY - rect.top; - - const scale = calcCircleScale(target.clientWidth, target.clientHeight, circleCenterX, circleCenterY); - - window.setTimeout(() => { - ripple.style.transform = 'scale(' + (scale / 2) + ')'; - }, 1); - window.setTimeout(() => { - ripple.style.transition = 'all 1s ease'; - ripple.style.opacity = '0'; - }, 1000); - window.setTimeout(() => { - if (ripples) ripples.removeChild(ripple); - }, 2000); -} -</script> - -<style lang="scss" scoped> -.bghgjjyj { - position: relative; - z-index: 1; // 他コンポーネントのbox-shadowに隠されないようにするため - display: block; - min-width: 100px; - width: max-content; - padding: 8px 16px; - text-align: center; - font-weight: normal; - font-size: 1em; - box-shadow: none; - text-decoration: none; - background: var(--buttonBg); - border-radius: 5px; - overflow: clip; - box-sizing: border-box; - transition: background 0.1s ease; - - &:not(:disabled):hover { - background: var(--buttonHoverBg); - } - - &:not(:disabled):active { - background: var(--buttonHoverBg); - } - - &.full { - width: 100%; - } - - &.rounded { - border-radius: 999px; - } - - &.primary { - font-weight: bold; - color: var(--fgOnAccent) !important; - background: var(--accent); - - &:not(:disabled):hover { - background: var(--X8); - } - - &:not(:disabled):active { - background: var(--X8); - } - } - - &.gradate { - font-weight: bold; - color: var(--fgOnAccent) !important; - background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB)); - - &:not(:disabled):hover { - background: linear-gradient(90deg, var(--X8), var(--X8)); - } - - &:not(:disabled):active { - background: linear-gradient(90deg, var(--X8), var(--X8)); - } - } - - &.danger { - color: #ff2a2a; - - &.primary { - color: #fff; - background: #ff2a2a; - - &:not(:disabled):hover { - background: #ff4242; - } - - &:not(:disabled):active { - background: #d42e2e; - } - } - } - - &:disabled { - opacity: 0.7; - } - - &:focus-visible { - outline: solid 2px var(--focus); - outline-offset: 2px; - } - - &.inline { - display: inline-block; - width: auto; - min-width: 100px; - } - - > .ripples { - position: absolute; - z-index: 0; - top: 0; - left: 0; - width: 100%; - height: 100%; - border-radius: 6px; - overflow: hidden; - - ::v-deep(div) { - position: absolute; - width: 2px; - height: 2px; - border-radius: 100%; - background: rgba(0, 0, 0, 0.1); - opacity: 1; - transform: scale(1); - transition: all 0.5s cubic-bezier(0,.5,0,1); - } - } - - &.primary > .ripples ::v-deep(div) { - background: rgba(0, 0, 0, 0.15); - } - - > .content { - position: relative; - z-index: 1; - } -} -</style> diff --git a/packages/client/src/components/ui/container.vue b/packages/client/src/components/ui/container.vue deleted file mode 100644 index 4be59adc2a..0000000000 --- a/packages/client/src/components/ui/container.vue +++ /dev/null @@ -1,264 +0,0 @@ -<template> -<div v-size="{ max: [380] }" class="ukygtjoj _panel" :class="{ naked, thin, hideHeader: !showHeader, scrollable, closed: !showBody }"> - <header v-if="showHeader" ref="header"> - <div class="title"><slot name="header"></slot></div> - <div class="sub"> - <slot name="func"></slot> - <button v-if="foldable" class="_button" @click="() => showBody = !showBody"> - <template v-if="showBody"><i class="fas fa-angle-up"></i></template> - <template v-else><i class="fas fa-angle-down"></i></template> - </button> - </div> - </header> - <transition - :name="$store.state.animation ? 'container-toggle' : ''" - @enter="enter" - @after-enter="afterEnter" - @leave="leave" - @after-leave="afterLeave" - > - <div v-show="showBody" ref="content" class="content" :class="{ omitted }"> - <slot></slot> - <button v-if="omitted" class="fade _button" @click="() => { ignoreOmit = true; omitted = false; }"> - <span>{{ $ts.showMore }}</span> - </button> - </div> - </transition> -</div> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; - -export default defineComponent({ - props: { - showHeader: { - type: Boolean, - required: false, - default: true, - }, - thin: { - type: Boolean, - required: false, - default: false, - }, - naked: { - type: Boolean, - required: false, - default: false, - }, - foldable: { - type: Boolean, - required: false, - default: false, - }, - expanded: { - type: Boolean, - required: false, - default: true, - }, - scrollable: { - type: Boolean, - required: false, - default: false, - }, - maxHeight: { - type: Number, - required: false, - default: null, - }, - }, - data() { - return { - showBody: this.expanded, - omitted: null, - ignoreOmit: false, - }; - }, - mounted() { - this.$watch('showBody', showBody => { - const headerHeight = this.showHeader ? this.$refs.header.offsetHeight : 0; - this.$el.style.minHeight = `${headerHeight}px`; - if (showBody) { - this.$el.style.flexBasis = 'auto'; - } else { - this.$el.style.flexBasis = `${headerHeight}px`; - } - }, { - immediate: true, - }); - - this.$el.style.setProperty('--maxHeight', this.maxHeight + 'px'); - - const calcOmit = () => { - if (this.omitted || this.ignoreOmit || this.maxHeight == null) return; - const height = this.$refs.content.offsetHeight; - this.omitted = height > this.maxHeight; - }; - - calcOmit(); - new ResizeObserver((entries, observer) => { - calcOmit(); - }).observe(this.$refs.content); - }, - methods: { - toggleContent(show: boolean) { - if (!this.foldable) return; - this.showBody = show; - }, - - enter(el) { - const elementHeight = el.getBoundingClientRect().height; - el.style.height = 0; - el.offsetHeight; // reflow - el.style.height = elementHeight + 'px'; - }, - afterEnter(el) { - el.style.height = null; - }, - leave(el) { - const elementHeight = el.getBoundingClientRect().height; - el.style.height = elementHeight + 'px'; - el.offsetHeight; // reflow - el.style.height = 0; - }, - afterLeave(el) { - el.style.height = null; - }, - }, -}); -</script> - -<style lang="scss" scoped> -.container-toggle-enter-active, .container-toggle-leave-active { - overflow-y: hidden; - transition: opacity 0.5s, height 0.5s !important; -} -.container-toggle-enter-from { - opacity: 0; -} -.container-toggle-leave-to { - opacity: 0; -} - -.ukygtjoj { - position: relative; - overflow: clip; - contain: content; - - &.naked { - background: transparent !important; - box-shadow: none !important; - } - - &.scrollable { - display: flex; - flex-direction: column; - - > .content { - overflow: auto; - } - } - - > header { - position: sticky; - top: var(--stickyTop, 0px); - left: 0; - color: var(--panelHeaderFg); - background: var(--panelHeaderBg); - border-bottom: solid 0.5px var(--panelHeaderDivider); - z-index: 2; - line-height: 1.4em; - - > .title { - margin: 0; - padding: 12px 16px; - - > ::v-deep(i) { - margin-right: 6px; - } - - &:empty { - display: none; - } - } - - > .sub { - position: absolute; - z-index: 2; - top: 0; - right: 0; - height: 100%; - - > ::v-deep(button) { - width: 42px; - height: 100%; - } - } - } - - > .content { - --stickyTop: 0px; - - &.omitted { - position: relative; - max-height: var(--maxHeight); - overflow: hidden; - - > .fade { - display: block; - position: absolute; - z-index: 10; - bottom: 0; - left: 0; - width: 100%; - height: 64px; - background: linear-gradient(0deg, var(--panel), var(--X15)); - - > span { - display: inline-block; - background: var(--panel); - padding: 6px 10px; - font-size: 0.8em; - border-radius: 999px; - box-shadow: 0 2px 6px rgb(0 0 0 / 20%); - } - - &:hover { - > span { - background: var(--panelHighlight); - } - } - } - } - } - - &.max-width_380px, &.thin { - > header { - > .title { - padding: 8px 10px; - font-size: 0.9em; - } - } - - > .content { - } - } -} - -._forceContainerFull_ .ukygtjoj { - > header { - > .title { - padding: 12px 16px !important; - } - } -} - -._forceContainerFull_.ukygtjoj { - > header { - > .title { - padding: 12px 16px !important; - } - } -} -</style> diff --git a/packages/client/src/components/ui/context-menu.vue b/packages/client/src/components/ui/context-menu.vue deleted file mode 100644 index 165c3db462..0000000000 --- a/packages/client/src/components/ui/context-menu.vue +++ /dev/null @@ -1,85 +0,0 @@ -<template> -<transition :name="$store.state.animation ? 'fade' : ''" appear> - <div ref="rootEl" class="nvlagfpb" :style="{ zIndex }" @contextmenu.prevent.stop="() => {}"> - <MkMenu :items="items" :align="'left'" @close="$emit('closed')"/> - </div> -</transition> -</template> - -<script lang="ts" setup> -import { onMounted, onBeforeUnmount } from 'vue'; -import MkMenu from './menu.vue'; -import { MenuItem } from './types/menu.vue'; -import contains from '@/scripts/contains'; -import * as os from '@/os'; - -const props = defineProps<{ - items: MenuItem[]; - ev: MouseEvent; -}>(); - -const emit = defineEmits<{ - (ev: 'closed'): void; -}>(); - -let rootEl = $ref<HTMLDivElement>(); - -let zIndex = $ref<number>(os.claimZIndex('high')); - -onMounted(() => { - let left = props.ev.pageX + 1; // 間違って右ダブルクリックした場合に意図せずアイテムがクリックされるのを防ぐため + 1 - let top = props.ev.pageY + 1; // 間違って右ダブルクリックした場合に意図せずアイテムがクリックされるのを防ぐため + 1 - - const width = rootEl.offsetWidth; - const height = rootEl.offsetHeight; - - if (left + width - window.pageXOffset > window.innerWidth) { - left = window.innerWidth - width + window.pageXOffset; - } - - if (top + height - window.pageYOffset > window.innerHeight) { - top = window.innerHeight - height + window.pageYOffset; - } - - if (top < 0) { - top = 0; - } - - if (left < 0) { - left = 0; - } - - rootEl.style.top = `${top}px`; - rootEl.style.left = `${left}px`; - - for (const el of Array.from(document.querySelectorAll('body *'))) { - el.addEventListener('mousedown', onMousedown); - } -}); - -onBeforeUnmount(() => { - for (const el of Array.from(document.querySelectorAll('body *'))) { - el.removeEventListener('mousedown', onMousedown); - } -}); - -function onMousedown(evt: Event) { - if (!contains(rootEl, evt.target) && (rootEl !== evt.target)) emit('closed'); -} -</script> - -<style lang="scss" scoped> -.nvlagfpb { - position: absolute; -} - -.fade-enter-active, .fade-leave-active { - transition: opacity 0.5s cubic-bezier(0.16, 1, 0.3, 1), transform 0.5s cubic-bezier(0.16, 1, 0.3, 1); - transform-origin: left top; -} - -.fade-enter-from, .fade-leave-to { - opacity: 0; - transform: scale(0.9); -} -</style> diff --git a/packages/client/src/components/ui/folder.vue b/packages/client/src/components/ui/folder.vue deleted file mode 100644 index 7daa82cbd3..0000000000 --- a/packages/client/src/components/ui/folder.vue +++ /dev/null @@ -1,156 +0,0 @@ -<template> -<div v-size="{ max: [500] }" class="ssazuxis"> - <header class="_button" :style="{ background: bg }" @click="showBody = !showBody"> - <div class="title"><slot name="header"></slot></div> - <div class="divider"></div> - <button class="_button"> - <template v-if="showBody"><i class="fas fa-angle-up"></i></template> - <template v-else><i class="fas fa-angle-down"></i></template> - </button> - </header> - <transition :name="$store.state.animation ? 'folder-toggle' : ''" - @enter="enter" - @after-enter="afterEnter" - @leave="leave" - @after-leave="afterLeave" - > - <div v-show="showBody"> - <slot></slot> - </div> - </transition> -</div> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; -import tinycolor from 'tinycolor2'; - -const localStoragePrefix = 'ui:folder:'; - -export default defineComponent({ - props: { - expanded: { - type: Boolean, - required: false, - default: true - }, - persistKey: { - type: String, - required: false, - default: null - }, - }, - data() { - return { - bg: null, - showBody: (this.persistKey && localStorage.getItem(localStoragePrefix + this.persistKey)) ? localStorage.getItem(localStoragePrefix + this.persistKey) === 't' : this.expanded, - }; - }, - watch: { - showBody() { - if (this.persistKey) { - localStorage.setItem(localStoragePrefix + this.persistKey, this.showBody ? 't' : 'f'); - } - } - }, - mounted() { - function getParentBg(el: Element | null): string { - if (el == null || el.tagName === 'BODY') return 'var(--bg)'; - const bg = el.style.background || el.style.backgroundColor; - if (bg) { - return bg; - } else { - return getParentBg(el.parentElement); - } - } - const rawBg = getParentBg(this.$el); - const bg = tinycolor(rawBg.startsWith('var(') ? getComputedStyle(document.documentElement).getPropertyValue(rawBg.slice(4, -1)) : rawBg); - bg.setAlpha(0.85); - this.bg = bg.toRgbString(); - }, - methods: { - toggleContent(show: boolean) { - this.showBody = show; - }, - - enter(el) { - const elementHeight = el.getBoundingClientRect().height; - el.style.height = 0; - el.offsetHeight; // reflow - el.style.height = elementHeight + 'px'; - }, - afterEnter(el) { - el.style.height = null; - }, - leave(el) { - const elementHeight = el.getBoundingClientRect().height; - el.style.height = elementHeight + 'px'; - el.offsetHeight; // reflow - el.style.height = 0; - }, - afterLeave(el) { - el.style.height = null; - }, - } -}); -</script> - -<style lang="scss" scoped> -.folder-toggle-enter-active, .folder-toggle-leave-active { - overflow-y: hidden; - transition: opacity 0.5s, height 0.5s !important; -} -.folder-toggle-enter-from { - opacity: 0; -} -.folder-toggle-leave-to { - opacity: 0; -} - -.ssazuxis { - position: relative; - - > header { - display: flex; - position: relative; - z-index: 10; - position: sticky; - top: var(--stickyTop, 0px); - padding: var(--x-padding); - -webkit-backdrop-filter: var(--blur, blur(8px)); - backdrop-filter: var(--blur, blur(20px)); - - > .title { - margin: 0; - padding: 12px 16px 12px 0; - - > i { - margin-right: 6px; - } - - &:empty { - display: none; - } - } - - > .divider { - flex: 1; - margin: auto; - height: 1px; - background: var(--divider); - } - - > button { - padding: 12px 0 12px 16px; - } - } - - &.max-width_500px { - > header { - > .title { - padding: 8px 10px 8px 0; - } - } - } -} -</style> diff --git a/packages/client/src/components/ui/info.vue b/packages/client/src/components/ui/info.vue deleted file mode 100644 index 4fdfc5c5e6..0000000000 --- a/packages/client/src/components/ui/info.vue +++ /dev/null @@ -1,34 +0,0 @@ -<template> -<div class="fpezltsf" :class="{ warn }"> - <i v-if="warn" class="fas fa-exclamation-triangle"></i> - <i v-else class="fas fa-info-circle"></i> - <slot></slot> -</div> -</template> - -<script lang="ts" setup> -import { } from 'vue'; - -const props = defineProps<{ - warn?: boolean; -}>(); -</script> - -<style lang="scss" scoped> -.fpezltsf { - padding: 16px; - font-size: 90%; - background: var(--infoBg); - color: var(--infoFg); - border-radius: var(--radius); - - &.warn { - background: var(--infoWarnBg); - color: var(--infoWarnFg); - } - - > i { - margin-right: 4px; - } -} -</style> diff --git a/packages/client/src/components/ui/menu.child.vue b/packages/client/src/components/ui/menu.child.vue deleted file mode 100644 index b67224d3e1..0000000000 --- a/packages/client/src/components/ui/menu.child.vue +++ /dev/null @@ -1,65 +0,0 @@ -<template> -<div ref="el" class="sfhdhdhr"> - <MkMenu ref="menu" :items="items" :align="align" :width="width" :as-drawer="false" @close="onChildClosed"/> -</div> -</template> - -<script lang="ts" setup> -import { on } from 'events'; -import { nextTick, onBeforeUnmount, onMounted, onUnmounted, ref, watch } from 'vue'; -import MkMenu from './menu.vue'; -import { MenuItem } from '@/types/menu'; -import * as os from '@/os'; - -const props = defineProps<{ - items: MenuItem[]; - targetElement: HTMLElement; - rootElement: HTMLElement; - width?: number; - viaKeyboard?: boolean; -}>(); - -const emit = defineEmits<{ - (ev: 'closed'): void; - (ev: 'actioned'): void; -}>(); - -const el = ref<HTMLElement>(); -const align = 'left'; - -function setPosition() { - const rootRect = props.rootElement.getBoundingClientRect(); - const rect = props.targetElement.getBoundingClientRect(); - const left = props.targetElement.offsetWidth; - const top = (rect.top - rootRect.top) - 8; - el.value.style.left = left + 'px'; - el.value.style.top = top + 'px'; -} - -function onChildClosed(actioned?: boolean) { - if (actioned) { - emit('actioned'); - } else { - emit('closed'); - } -} - -onMounted(() => { - setPosition(); - nextTick(() => { - setPosition(); - }); -}); - -defineExpose({ - checkHit: (ev: MouseEvent) => { - return (ev.target === el.value || el.value.contains(ev.target)); - }, -}); -</script> - -<style lang="scss" scoped> -.sfhdhdhr { - position: absolute; -} -</style> diff --git a/packages/client/src/components/ui/menu.vue b/packages/client/src/components/ui/menu.vue deleted file mode 100644 index 60b68954d6..0000000000 --- a/packages/client/src/components/ui/menu.vue +++ /dev/null @@ -1,364 +0,0 @@ -<template> -<div> - <div - ref="itemsEl" v-hotkey="keymap" - class="rrevdjwt _popup _shadow" - :class="{ center: align === 'center', asDrawer }" - :style="{ width: (width && !asDrawer) ? width + 'px' : '', maxHeight: maxHeight ? maxHeight + 'px' : '' }" - @contextmenu.self="e => e.preventDefault()" - > - <template v-for="(item, i) in items2"> - <div v-if="item === null" class="divider"></div> - <span v-else-if="item.type === 'label'" class="label item"> - <span>{{ item.text }}</span> - </span> - <span v-else-if="item.type === 'pending'" :tabindex="i" class="pending item"> - <span><MkEllipsis/></span> - </span> - <MkA v-else-if="item.type === 'link'" :to="item.to" :tabindex="i" class="_button item" @click.passive="close(true)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)"> - <i v-if="item.icon" class="fa-fw" :class="item.icon"></i> - <MkAvatar v-if="item.avatar" :user="item.avatar" class="avatar"/> - <span>{{ item.text }}</span> - <span v-if="item.indicate" class="indicator"><i class="fas fa-circle"></i></span> - </MkA> - <a v-else-if="item.type === 'a'" :href="item.href" :target="item.target" :download="item.download" :tabindex="i" class="_button item" @click="close(true)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)"> - <i v-if="item.icon" class="fa-fw" :class="item.icon"></i> - <span>{{ item.text }}</span> - <span v-if="item.indicate" class="indicator"><i class="fas fa-circle"></i></span> - </a> - <button v-else-if="item.type === 'user'" :tabindex="i" class="_button item" :class="{ active: item.active }" :disabled="item.active" @click="clicked(item.action, $event)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)"> - <MkAvatar :user="item.user" class="avatar"/><MkUserName :user="item.user"/> - <span v-if="item.indicate" class="indicator"><i class="fas fa-circle"></i></span> - </button> - <span v-else-if="item.type === 'switch'" :tabindex="i" class="item" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)"> - <FormSwitch v-model="item.ref" :disabled="item.disabled" class="form-switch">{{ item.text }}</FormSwitch> - </span> - <button v-else-if="item.type === 'parent'" :tabindex="i" class="_button item parent" :class="{ childShowing: childShowingItem === item }" @mouseenter="showChildren(item, $event)"> - <i v-if="item.icon" class="fa-fw" :class="item.icon"></i> - <span>{{ item.text }}</span> - <span class="caret"><i class="fas fa-caret-right fa-fw"></i></span> - </button> - <button v-else :tabindex="i" class="_button item" :class="{ danger: item.danger, active: item.active }" :disabled="item.active" @click="clicked(item.action, $event)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)"> - <i v-if="item.icon" class="fa-fw" :class="item.icon"></i> - <MkAvatar v-if="item.avatar" :user="item.avatar" class="avatar"/> - <span>{{ item.text }}</span> - <span v-if="item.indicate" class="indicator"><i class="fas fa-circle"></i></span> - </button> - </template> - <span v-if="items2.length === 0" class="none item"> - <span>{{ i18n.ts.none }}</span> - </span> - </div> - <div v-if="childMenu" class="child"> - <XChild ref="child" :items="childMenu" :target-element="childTarget" :root-element="itemsEl" showing @actioned="childActioned"/> - </div> -</div> -</template> - -<script lang="ts" setup> -import { defineAsyncComponent, nextTick, onBeforeUnmount, onMounted, onUnmounted, Ref, ref, watch } from 'vue'; -import { focusPrev, focusNext } from '@/scripts/focus'; -import FormSwitch from '@/components/form/switch.vue'; -import { MenuItem, InnerMenuItem, MenuPending, MenuAction } from '@/types/menu'; -import * as os from '@/os'; -import { i18n } from '@/i18n'; - -const XChild = defineAsyncComponent(() => import('./menu.child.vue')); - -const props = defineProps<{ - items: MenuItem[]; - viaKeyboard?: boolean; - asDrawer?: boolean; - align?: 'center' | string; - width?: number; - maxHeight?: number; -}>(); - -const emit = defineEmits<{ - (ev: 'close', actioned?: boolean): void; -}>(); - -let itemsEl = $ref<HTMLDivElement>(); - -let items2: InnerMenuItem[] = $ref([]); - -let child = $ref<InstanceType<typeof XChild>>(); - -let keymap = $computed(() => ({ - 'up|k|shift+tab': focusUp, - 'down|j|tab': focusDown, - 'esc': close, -})); - -let childShowingItem = $ref<MenuItem | null>(); - -watch(() => props.items, () => { - const items: (MenuItem | MenuPending)[] = [...props.items].filter(item => item !== undefined); - - for (let i = 0; i < items.length; i++) { - const item = items[i]; - - if (item && 'then' in item) { // if item is Promise - items[i] = { type: 'pending' }; - item.then(actualItem => { - items2[i] = actualItem; - }); - } - } - - items2 = items as InnerMenuItem[]; -}, { - immediate: true, -}); - -let childMenu = $ref<MenuItem[] | null>(); -let childTarget = $ref<HTMLElement | null>(); - -function closeChild() { - childMenu = null; - childShowingItem = null; -} - -function childActioned() { - closeChild(); - close(true); -} - -function onGlobalMousedown(event: MouseEvent) { - if (childTarget && (event.target === childTarget || childTarget.contains(event.target))) return; - if (child && child.checkHit(event)) return; - closeChild(); -} - -let childCloseTimer: null | number = null; -function onItemMouseEnter(item) { - childCloseTimer = window.setTimeout(() => { - closeChild(); - }, 300); -} -function onItemMouseLeave(item) { - if (childCloseTimer) window.clearTimeout(childCloseTimer); -} - -async function showChildren(item: MenuItem, ev: MouseEvent) { - if (props.asDrawer) { - os.popupMenu(item.children, ev.currentTarget ?? ev.target); - close(); - } else { - childTarget = ev.currentTarget ?? ev.target; - childMenu = item.children; - childShowingItem = item; - } -} - -function clicked(fn: MenuAction, ev: MouseEvent) { - fn(ev); - close(true); -} - -function close(actioned = false) { - emit('close', actioned); -} - -function focusUp() { - focusPrev(document.activeElement); -} - -function focusDown() { - focusNext(document.activeElement); -} - -onMounted(() => { - if (props.viaKeyboard) { - nextTick(() => { - focusNext(itemsEl.children[0], true, false); - }); - } - - document.addEventListener('mousedown', onGlobalMousedown, { passive: true }); -}); - -onBeforeUnmount(() => { - document.removeEventListener('mousedown', onGlobalMousedown); -}); -</script> - -<style lang="scss" scoped> -.rrevdjwt { - padding: 8px 0; - box-sizing: border-box; - min-width: 200px; - overflow: auto; - overscroll-behavior: contain; - - &.center { - > .item { - text-align: center; - } - } - - > .item { - display: block; - position: relative; - padding: 6px 16px; - width: 100%; - box-sizing: border-box; - white-space: nowrap; - font-size: 0.9em; - line-height: 20px; - text-align: left; - overflow: hidden; - text-overflow: ellipsis; - - &:before { - content: ""; - display: block; - position: absolute; - top: 0; - left: 0; - right: 0; - margin: auto; - width: calc(100% - 16px); - height: 100%; - border-radius: 6px; - } - - > * { - position: relative; - } - - &:not(:disabled):hover { - color: var(--accent); - text-decoration: none; - - &:before { - background: var(--accentedBg); - } - } - - &.danger { - color: #ff2a2a; - - &:hover { - color: #fff; - - &:before { - background: #ff4242; - } - } - - &:active { - color: #fff; - - &:before { - background: #d42e2e; - } - } - } - - &.active { - color: var(--fgOnAccent); - opacity: 1; - - &:before { - background: var(--accent); - } - } - - &:not(:active):focus-visible { - box-shadow: 0 0 0 2px var(--focus) inset; - } - - &.label { - pointer-events: none; - font-size: 0.7em; - padding-bottom: 4px; - - > span { - opacity: 0.7; - } - } - - &.pending { - pointer-events: none; - opacity: 0.7; - } - - &.none { - pointer-events: none; - opacity: 0.7; - } - - &.parent { - display: flex; - align-items: center; - cursor: default; - - > .caret { - margin-left: auto; - } - - &.childShowing { - color: var(--accent); - text-decoration: none; - - &:before { - background: var(--accentedBg); - } - } - } - - > i { - margin-right: 5px; - width: 20px; - } - - > .avatar { - margin-right: 5px; - width: 20px; - height: 20px; - } - - > .indicator { - position: absolute; - top: 5px; - left: 13px; - color: var(--indicator); - font-size: 12px; - animation: blink 1s infinite; - } - } - - > .divider { - margin: 8px 0; - border-top: solid 0.5px var(--divider); - } - - &.asDrawer { - padding: 12px 0 calc(env(safe-area-inset-bottom, 0px) + 12px) 0; - width: 100%; - border-radius: 24px; - border-bottom-right-radius: 0; - border-bottom-left-radius: 0; - - > .item { - font-size: 1em; - padding: 12px 24px; - - &:before { - width: calc(100% - 24px); - border-radius: 12px; - } - - > i { - margin-right: 14px; - width: 24px; - } - } - - > .divider { - margin: 12px 0; - } - } -} -</style> diff --git a/packages/client/src/components/ui/modal-window.vue b/packages/client/src/components/ui/modal-window.vue deleted file mode 100644 index b29ea4fd81..0000000000 --- a/packages/client/src/components/ui/modal-window.vue +++ /dev/null @@ -1,146 +0,0 @@ -<template> -<MkModal ref="modal" :prefer-type="'dialog'" @click="onBgClick" @closed="$emit('closed')"> - <div ref="rootEl" class="ebkgoccj _narrow_" :style="{ width: `${width}px`, height: scroll ? (height ? `${height}px` : null) : (height ? `min(${height}px, 100%)` : '100%') }" @keydown="onKeydown"> - <div ref="headerEl" class="header"> - <button v-if="withOkButton" class="_button" @click="$emit('close')"><i class="fas fa-times"></i></button> - <span class="title"> - <slot name="header"></slot> - </span> - <button v-if="!withOkButton" class="_button" @click="$emit('close')"><i class="fas fa-times"></i></button> - <button v-if="withOkButton" class="_button" :disabled="okButtonDisabled" @click="$emit('ok')"><i class="fas fa-check"></i></button> - </div> - <div class="body"> - <slot :width="bodyWidth" :height="bodyHeight"></slot> - </div> - </div> -</MkModal> -</template> - -<script lang="ts" setup> -import { onMounted, onUnmounted } from 'vue'; -import MkModal from './modal.vue'; - -const props = withDefaults(defineProps<{ - withOkButton: boolean; - okButtonDisabled: boolean; - width: number; - height: number | null; - scroll: boolean; -}>(), { - withOkButton: false, - okButtonDisabled: false, - width: 400, - height: null, - scroll: true, -}); - -const emit = defineEmits<{ - (event: 'click'): void; - (event: 'close'): void; - (event: 'closed'): void; - (event: 'ok'): void; -}>(); - -let modal = $ref<InstanceType<typeof MkModal>>(); -let rootEl = $ref<HTMLElement>(); -let headerEl = $ref<HTMLElement>(); -let bodyWidth = $ref(0); -let bodyHeight = $ref(0); - -const close = () => { - modal.close(); -}; - -const onBgClick = () => { - emit('click'); -}; - -const onKeydown = (evt) => { - if (evt.which === 27) { // Esc - evt.preventDefault(); - evt.stopPropagation(); - close(); - } -}; - -const ro = new ResizeObserver((entries, observer) => { - bodyWidth = rootEl.offsetWidth; - bodyHeight = rootEl.offsetHeight - headerEl.offsetHeight; -}); - -onMounted(() => { - bodyWidth = rootEl.offsetWidth; - bodyHeight = rootEl.offsetHeight - headerEl.offsetHeight; - ro.observe(rootEl); -}); - -onUnmounted(() => { - ro.disconnect(); -}); - -defineExpose({ - close, -}); -</script> - -<style lang="scss" scoped> -.ebkgoccj { - overflow: hidden; - display: flex; - flex-direction: column; - contain: content; - border-radius: var(--radius); - - --root-margin: 24px; - - @media (max-width: 500px) { - --root-margin: 16px; - } - - > .header { - $height: 46px; - $height-narrow: 42px; - display: flex; - flex-shrink: 0; - background: var(--windowHeader); - -webkit-backdrop-filter: var(--blur, blur(15px)); - backdrop-filter: var(--blur, blur(15px)); - - > button { - height: $height; - width: $height; - - @media (max-width: 500px) { - height: $height-narrow; - width: $height-narrow; - } - } - - > .title { - flex: 1; - line-height: $height; - padding-left: 32px; - font-weight: bold; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - pointer-events: none; - - @media (max-width: 500px) { - line-height: $height-narrow; - padding-left: 16px; - } - } - - > button + .title { - padding-left: 0; - } - } - - > .body { - flex: 1; - overflow: auto; - background: var(--panel); - } -} -</style> diff --git a/packages/client/src/components/ui/modal.vue b/packages/client/src/components/ui/modal.vue deleted file mode 100644 index 2305a02794..0000000000 --- a/packages/client/src/components/ui/modal.vue +++ /dev/null @@ -1,406 +0,0 @@ -<template> -<transition :name="$store.state.animation ? (type === 'drawer') ? 'modal-drawer' : (type === 'popup') ? 'modal-popup' : 'modal' : ''" :duration="$store.state.animation ? 200 : 0" appear @after-leave="emit('closed')" @enter="emit('opening')" @after-enter="onOpened"> - <div v-show="manualShowing != null ? manualShowing : showing" v-hotkey.global="keymap" class="qzhlnise" :class="{ drawer: type === 'drawer', dialog: type === 'dialog' || type === 'dialog:top', popup: type === 'popup' }" :style="{ zIndex, pointerEvents: (manualShowing != null ? manualShowing : showing) ? 'auto' : 'none', '--transformOrigin': transformOrigin }"> - <div class="bg _modalBg" :class="{ transparent: transparentBg && (type === 'popup') }" :style="{ zIndex }" @click="onBgClick" @contextmenu.prevent.stop="() => {}"></div> - <div ref="content" class="content" :class="{ fixed, top: type === 'dialog:top' }" :style="{ zIndex }" @click.self="onBgClick"> - <slot :max-height="maxHeight" :type="type"></slot> - </div> - </div> -</transition> -</template> - -<script lang="ts" setup> -import { nextTick, onMounted, watch, provide } from 'vue'; -import * as os from '@/os'; -import { isTouchUsing } from '@/scripts/touch'; -import { defaultStore } from '@/store'; -import { deviceKind } from '@/scripts/device-kind'; - -function getFixedContainer(el: Element | null): Element | null { - if (el == null || el.tagName === 'BODY') return null; - const position = window.getComputedStyle(el).getPropertyValue('position'); - if (position === 'fixed') { - return el; - } else { - return getFixedContainer(el.parentElement); - } -} - -type ModalTypes = 'popup' | 'dialog' | 'dialog:top' | 'drawer'; - -const props = withDefaults(defineProps<{ - manualShowing?: boolean | null; - anchor?: { x: string; y: string; }; - src?: HTMLElement; - preferType?: ModalTypes | 'auto'; - zPriority?: 'low' | 'middle' | 'high'; - noOverlap?: boolean; - transparentBg?: boolean; -}>(), { - manualShowing: null, - src: null, - anchor: () => ({ x: 'center', y: 'bottom' }), - preferType: 'auto', - zPriority: 'low', - noOverlap: true, - transparentBg: false, -}); - -const emit = defineEmits<{ - (ev: 'opening'): void; - (ev: 'opened'): void; - (ev: 'click'): void; - (ev: 'esc'): void; - (ev: 'close'): void; - (ev: 'closed'): void; -}>(); - -provide('modal', true); - -let maxHeight = $ref<number>(); -let fixed = $ref(false); -let transformOrigin = $ref('center'); -let showing = $ref(true); -let content = $ref<HTMLElement>(); -const zIndex = os.claimZIndex(props.zPriority); -const type = $computed(() => { - if (props.preferType === 'auto') { - if (!defaultStore.state.disableDrawer && isTouchUsing && deviceKind === 'smartphone') { - return 'drawer'; - } else { - return props.src != null ? 'popup' : 'dialog'; - } - } else { - return props.preferType!; - } -}); - -let contentClicking = false; - -const close = () => { - // eslint-disable-next-line vue/no-mutating-props - if (props.src) props.src.style.pointerEvents = 'auto'; - showing = false; - emit('close'); -}; - -const onBgClick = () => { - if (contentClicking) return; - emit('click'); -}; - -if (type === 'drawer') { - maxHeight = window.innerHeight / 1.5; -} - -const keymap = { - 'esc': () => emit('esc'), -}; - -const MARGIN = 16; - -const align = () => { - if (props.src == null) return; - if (type === 'drawer') return; - if (type === 'dialog') return; - - if (content == null) return; - - const srcRect = props.src.getBoundingClientRect(); - - const width = content!.offsetWidth; - const height = content!.offsetHeight; - - let left; - let top; - - const x = srcRect.left + (fixed ? 0 : window.pageXOffset); - const y = srcRect.top + (fixed ? 0 : window.pageYOffset); - - if (props.anchor.x === 'center') { - left = x + (props.src.offsetWidth / 2) - (width / 2); - } else if (props.anchor.x === 'left') { - // TODO - } else if (props.anchor.x === 'right') { - left = x + props.src.offsetWidth; - } - - if (props.anchor.y === 'center') { - top = (y - (height / 2)); - } else if (props.anchor.y === 'top') { - // TODO - } else if (props.anchor.y === 'bottom') { - top = y + props.src.offsetHeight; - } - - if (fixed) { - // 画面から横にはみ出る場合 - if (left + width > window.innerWidth) { - left = window.innerWidth - width; - } - - const underSpace = (window.innerHeight - MARGIN) - top; - const upperSpace = (srcRect.top - MARGIN); - - // 画面から縦にはみ出る場合 - if (top + height > (window.innerHeight - MARGIN)) { - if (props.noOverlap && props.anchor.x === 'center') { - if (underSpace >= (upperSpace / 3)) { - maxHeight = underSpace; - } else { - maxHeight = upperSpace; - top = (upperSpace + MARGIN) - height; - } - } else { - top = (window.innerHeight - MARGIN) - height; - } - } else { - maxHeight = underSpace; - } - } else { - // 画面から横にはみ出る場合 - if (left + width - window.pageXOffset > window.innerWidth) { - left = window.innerWidth - width + window.pageXOffset - 1; - } - - const underSpace = (window.innerHeight - MARGIN) - (top - window.pageYOffset); - const upperSpace = (srcRect.top - MARGIN); - - // 画面から縦にはみ出る場合 - if (top + height - window.pageYOffset > (window.innerHeight - MARGIN)) { - if (props.noOverlap && props.anchor.x === 'center') { - if (underSpace >= (upperSpace / 3)) { - maxHeight = underSpace; - } else { - maxHeight = upperSpace; - top = window.pageYOffset + ((upperSpace + MARGIN) - height); - } - } else { - top = (window.innerHeight - MARGIN) - height + window.pageYOffset - 1; - } - } else { - maxHeight = underSpace; - } - } - - if (top < 0) { - top = MARGIN; - } - - if (left < 0) { - left = 0; - } - - let transformOriginX = 'center'; - let transformOriginY = 'center'; - - if (top >= srcRect.top + props.src.offsetHeight + (fixed ? 0 : window.pageYOffset)) { - transformOriginY = 'top'; - } else if ((top + height) <= srcRect.top + (fixed ? 0 : window.pageYOffset)) { - transformOriginY = 'bottom'; - } - - if (left >= srcRect.left + props.src.offsetWidth + (fixed ? 0 : window.pageXOffset)) { - transformOriginX = 'left'; - } else if ((left + width) <= srcRect.left + (fixed ? 0 : window.pageXOffset)) { - transformOriginX = 'right'; - } - - transformOrigin = `${transformOriginX} ${transformOriginY}`; - - content.style.left = left + 'px'; - content.style.top = top + 'px'; -}; - -const onOpened = () => { - emit('opened'); - - // モーダルコンテンツにマウスボタンが押され、コンテンツ外でマウスボタンが離されたときにモーダルバックグラウンドクリックと判定させないためにマウスイベントを監視しフラグ管理する - const el = content!.children[0]; - el.addEventListener('mousedown', ev => { - contentClicking = true; - window.addEventListener('mouseup', ev => { - // click イベントより先に mouseup イベントが発生するかもしれないのでちょっと待つ - window.setTimeout(() => { - contentClicking = false; - }, 100); - }, { passive: true, once: true }); - }, { passive: true }); -}; - -onMounted(() => { - watch(() => props.src, async () => { - if (props.src) { - // eslint-disable-next-line vue/no-mutating-props - props.src.style.pointerEvents = 'none'; - } - fixed = (type === 'drawer') || (getFixedContainer(props.src) != null); - - await nextTick(); - - align(); - }, { immediate: true }); - - nextTick(() => { - new ResizeObserver((entries, observer) => { - align(); - }).observe(content!); - }); -}); - -defineExpose({ - close, -}); -</script> - -<style lang="scss" scoped> -.modal-enter-active, .modal-leave-active { - > .bg { - transition: opacity 0.2s !important; - } - - > .content { - transform-origin: var(--transformOrigin); - transition: opacity 0.2s, transform 0.2s !important; - } -} -.modal-enter-from, .modal-leave-to { - > .bg { - opacity: 0; - } - - > .content { - pointer-events: none; - opacity: 0; - transform-origin: var(--transformOrigin); - transform: scale(0.9); - } -} - -.modal-popup-enter-active, .modal-popup-leave-active { - > .bg { - transition: opacity 0.2s !important; - } - - > .content { - transform-origin: var(--transformOrigin); - transition: opacity 0.2s cubic-bezier(0, 0, 0.2, 1), transform 0.2s cubic-bezier(0, 0, 0.2, 1) !important; - } -} -.modal-popup-enter-from, .modal-popup-leave-to { - > .bg { - opacity: 0; - } - - > .content { - pointer-events: none; - opacity: 0; - transform-origin: var(--transformOrigin); - transform: scale(0.9); - } -} - -.modal-drawer-enter-active { - > .bg { - transition: opacity 0.2s !important; - } - - > .content { - transition: transform 0.2s cubic-bezier(0,.5,0,1) !important; - } -} -.modal-drawer-leave-active { - > .bg { - transition: opacity 0.2s !important; - } - - > .content { - transition: transform 0.2s cubic-bezier(0,.5,0,1) !important; - } -} -.modal-drawer-enter-from, .modal-drawer-leave-to { - > .bg { - opacity: 0; - } - - > .content { - pointer-events: none; - transform: translateY(100%); - } -} - -.qzhlnise { - > .bg { - &.transparent { - background: transparent; - -webkit-backdrop-filter: none; - backdrop-filter: none; - } - } - - &.dialog { - > .content { - position: fixed; - top: 0; - bottom: 0; - left: 0; - right: 0; - margin: auto; - padding: 32px; - // TODO: mask-imageはiOSだとやたら重い。なんとかしたい - -webkit-mask-image: linear-gradient(0deg, rgba(0,0,0,0) 0%, rgba(0,0,0,1) 32px, rgba(0,0,0,1) calc(100% - 32px), rgba(0,0,0,0) 100%); - mask-image: linear-gradient(0deg, rgba(0,0,0,0) 0%, rgba(0,0,0,1) 32px, rgba(0,0,0,1) calc(100% - 32px), rgba(0,0,0,0) 100%); - overflow: auto; - display: flex; - - @media (max-width: 500px) { - padding: 16px; - -webkit-mask-image: linear-gradient(0deg, rgba(0,0,0,0) 0%, rgba(0,0,0,1) 16px, rgba(0,0,0,1) calc(100% - 16px), rgba(0,0,0,0) 100%); - mask-image: linear-gradient(0deg, rgba(0,0,0,0) 0%, rgba(0,0,0,1) 16px, rgba(0,0,0,1) calc(100% - 16px), rgba(0,0,0,0) 100%); - } - - > ::v-deep(*) { - margin: auto; - } - - &.top { - > ::v-deep(*) { - margin-top: 0; - } - } - } - } - - &.popup { - > .content { - position: absolute; - - &.fixed { - position: fixed; - } - } - } - - &.drawer { - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - overflow: clip; - - > .content { - position: fixed; - bottom: 0; - left: 0; - right: 0; - margin: auto; - - > ::v-deep(*) { - margin: auto; - } - } - } - -} -</style> diff --git a/packages/client/src/components/ui/pagination.vue b/packages/client/src/components/ui/pagination.vue deleted file mode 100644 index 7650c5b33a..0000000000 --- a/packages/client/src/components/ui/pagination.vue +++ /dev/null @@ -1,317 +0,0 @@ -<template> -<transition :name="$store.state.animation ? 'fade' : ''" mode="out-in"> - <MkLoading v-if="fetching"/> - - <MkError v-else-if="error" @retry="init()"/> - - <div v-else-if="empty" key="_empty_" class="empty"> - <slot name="empty"> - <div class="_fullinfo"> - <img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/> - <div>{{ i18n.ts.nothing }}</div> - </div> - </slot> - </div> - - <div v-else ref="rootEl"> - <div v-show="pagination.reversed && more" key="_more_" class="cxiknjgy _gap"> - <MkButton v-if="!moreFetching" class="button" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }" primary @click="fetchMoreAhead"> - {{ i18n.ts.loadMore }} - </MkButton> - <MkLoading v-else class="loading"/> - </div> - <slot :items="items"></slot> - <div v-show="!pagination.reversed && more" key="_more_" class="cxiknjgy _gap"> - <MkButton v-if="!moreFetching" v-appear="($store.state.enableInfiniteScroll && !disableAutoLoad) ? fetchMore : null" class="button" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }" primary @click="fetchMore"> - {{ i18n.ts.loadMore }} - </MkButton> - <MkLoading v-else class="loading"/> - </div> - </div> -</transition> -</template> - -<script lang="ts" setup> -import { computed, ComputedRef, isRef, markRaw, onActivated, onDeactivated, Ref, ref, watch } from 'vue'; -import * as misskey from 'misskey-js'; -import * as os from '@/os'; -import { onScrollTop, isTopVisible, getScrollPosition, getScrollContainer } from '@/scripts/scroll'; -import MkButton from '@/components/ui/button.vue'; -import { i18n } from '@/i18n'; - -const SECOND_FETCH_LIMIT = 30; - -export type Paging<E extends keyof misskey.Endpoints = keyof misskey.Endpoints> = { - endpoint: E; - limit: number; - params?: misskey.Endpoints[E]['req'] | ComputedRef<misskey.Endpoints[E]['req']>; - - /** - * 検索APIのような、ページング不可なエンドポイントを利用する場合 - * (そのようなAPIをこの関数で使うのは若干矛盾してるけど) - */ - noPaging?: boolean; - - /** - * items 配列の中身を逆順にする(新しい方が最後) - */ - reversed?: boolean; - - offsetMode?: boolean; -}; - -const props = withDefaults(defineProps<{ - pagination: Paging; - disableAutoLoad?: boolean; - displayLimit?: number; -}>(), { - displayLimit: 30, -}); - -const emit = defineEmits<{ - (ev: 'queue', count: number): void; -}>(); - -type Item = { id: string; [another: string]: unknown; }; - -const rootEl = ref<HTMLElement>(); -const items = ref<Item[]>([]); -const queue = ref<Item[]>([]); -const offset = ref(0); -const fetching = ref(true); -const moreFetching = ref(false); -const more = ref(false); -const backed = ref(false); // 遡り中か否か -const isBackTop = ref(false); -const empty = computed(() => items.value.length === 0); -const error = ref(false); - -const init = async (): Promise<void> => { - queue.value = []; - fetching.value = true; - const params = props.pagination.params ? isRef(props.pagination.params) ? props.pagination.params.value : props.pagination.params : {}; - await os.api(props.pagination.endpoint, { - ...params, - limit: props.pagination.noPaging ? (props.pagination.limit || 10) : (props.pagination.limit || 10) + 1, - }).then(res => { - for (let i = 0; i < res.length; i++) { - const item = res[i]; - if (props.pagination.reversed) { - if (i === res.length - 2) item._shouldInsertAd_ = true; - } else { - if (i === 3) item._shouldInsertAd_ = true; - } - } - if (!props.pagination.noPaging && (res.length > (props.pagination.limit || 10))) { - res.pop(); - items.value = props.pagination.reversed ? [...res].reverse() : res; - more.value = true; - } else { - items.value = props.pagination.reversed ? [...res].reverse() : res; - more.value = false; - } - offset.value = res.length; - error.value = false; - fetching.value = false; - }, err => { - error.value = true; - fetching.value = false; - }); -}; - -const reload = (): void => { - items.value = []; - init(); -}; - -const fetchMore = async (): Promise<void> => { - if (!more.value || fetching.value || moreFetching.value || items.value.length === 0) return; - moreFetching.value = true; - backed.value = true; - const params = props.pagination.params ? isRef(props.pagination.params) ? props.pagination.params.value : props.pagination.params : {}; - await os.api(props.pagination.endpoint, { - ...params, - limit: SECOND_FETCH_LIMIT + 1, - ...(props.pagination.offsetMode ? { - offset: offset.value, - } : props.pagination.reversed ? { - sinceId: items.value[0].id, - } : { - untilId: items.value[items.value.length - 1].id, - }), - }).then(res => { - for (let i = 0; i < res.length; i++) { - const item = res[i]; - if (props.pagination.reversed) { - if (i === res.length - 9) item._shouldInsertAd_ = true; - } else { - if (i === 10) item._shouldInsertAd_ = true; - } - } - if (res.length > SECOND_FETCH_LIMIT) { - res.pop(); - items.value = props.pagination.reversed ? [...res].reverse().concat(items.value) : items.value.concat(res); - more.value = true; - } else { - items.value = props.pagination.reversed ? [...res].reverse().concat(items.value) : items.value.concat(res); - more.value = false; - } - offset.value += res.length; - moreFetching.value = false; - }, err => { - moreFetching.value = false; - }); -}; - -const fetchMoreAhead = async (): Promise<void> => { - if (!more.value || fetching.value || moreFetching.value || items.value.length === 0) return; - moreFetching.value = true; - const params = props.pagination.params ? isRef(props.pagination.params) ? props.pagination.params.value : props.pagination.params : {}; - await os.api(props.pagination.endpoint, { - ...params, - limit: SECOND_FETCH_LIMIT + 1, - ...(props.pagination.offsetMode ? { - offset: offset.value, - } : props.pagination.reversed ? { - untilId: items.value[0].id, - } : { - sinceId: items.value[items.value.length - 1].id, - }), - }).then(res => { - if (res.length > SECOND_FETCH_LIMIT) { - res.pop(); - items.value = props.pagination.reversed ? [...res].reverse().concat(items.value) : items.value.concat(res); - more.value = true; - } else { - items.value = props.pagination.reversed ? [...res].reverse().concat(items.value) : items.value.concat(res); - more.value = false; - } - offset.value += res.length; - moreFetching.value = false; - }, err => { - moreFetching.value = false; - }); -}; - -const prepend = (item: Item): void => { - if (props.pagination.reversed) { - if (rootEl.value) { - const container = getScrollContainer(rootEl.value); - if (container == null) { - // TODO? - } else { - const pos = getScrollPosition(rootEl.value); - const viewHeight = container.clientHeight; - const height = container.scrollHeight; - const isBottom = (pos + viewHeight > height - 32); - if (isBottom) { - // オーバーフローしたら古いアイテムは捨てる - if (items.value.length >= props.displayLimit) { - // このやり方だとVue 3.2以降アニメーションが動かなくなる - //items.value = items.value.slice(-props.displayLimit); - while (items.value.length >= props.displayLimit) { - items.value.shift(); - } - more.value = true; - } - } - } - } - items.value.push(item); - // TODO - } else { - // 初回表示時はunshiftだけでOK - if (!rootEl.value) { - items.value.unshift(item); - return; - } - - const isTop = isBackTop.value || (document.body.contains(rootEl.value) && isTopVisible(rootEl.value)); - - if (isTop) { - // Prepend the item - items.value.unshift(item); - - // オーバーフローしたら古いアイテムは捨てる - if (items.value.length >= props.displayLimit) { - // このやり方だとVue 3.2以降アニメーションが動かなくなる - //this.items = items.value.slice(0, props.displayLimit); - while (items.value.length >= props.displayLimit) { - items.value.pop(); - } - more.value = true; - } - } else { - queue.value.push(item); - onScrollTop(rootEl.value, () => { - for (const item of queue.value) { - prepend(item); - } - queue.value = []; - }); - } - } -}; - -const append = (item: Item): void => { - items.value.push(item); -}; - -const removeItem = (finder: (item: Item) => boolean) => { - const i = items.value.findIndex(finder); - items.value.splice(i, 1); -}; - -const updateItem = (id: Item['id'], replacer: (old: Item) => Item): void => { - const i = items.value.findIndex(item => item.id === id); - items.value[i] = replacer(items.value[i]); -}; - -if (props.pagination.params && isRef(props.pagination.params)) { - watch(props.pagination.params, init, { deep: true }); -} - -watch(queue, (a, b) => { - if (a.length === 0 && b.length === 0) return; - emit('queue', queue.value.length); -}, { deep: true }); - -init(); - -onActivated(() => { - isBackTop.value = false; -}); - -onDeactivated(() => { - isBackTop.value = window.scrollY === 0; -}); - -defineExpose({ - items, - queue, - backed, - reload, - prepend, - append, - removeItem, - updateItem, -}); -</script> - -<style lang="scss" scoped> -.fade-enter-active, -.fade-leave-active { - transition: opacity 0.125s ease; -} -.fade-enter-from, -.fade-leave-to { - opacity: 0; -} - -.cxiknjgy { - > .button { - margin-left: auto; - margin-right: auto; - } -} -</style> diff --git a/packages/client/src/components/ui/popup-menu.vue b/packages/client/src/components/ui/popup-menu.vue deleted file mode 100644 index c29aff45e7..0000000000 --- a/packages/client/src/components/ui/popup-menu.vue +++ /dev/null @@ -1,36 +0,0 @@ -<template> -<MkModal ref="modal" v-slot="{ type, maxHeight }" :z-priority="'high'" :src="src" :transparent-bg="true" @click="modal.close()" @closed="emit('closed')"> - <MkMenu :items="items" :align="align" :width="width" :max-height="maxHeight" :as-drawer="type === 'drawer'" class="sfhdhdhq" :class="{ drawer: type === 'drawer' }" @close="modal.close()"/> -</MkModal> -</template> - -<script lang="ts" setup> -import { } from 'vue'; -import MkModal from './modal.vue'; -import MkMenu from './menu.vue'; -import { MenuItem } from '@/types/menu'; - -defineProps<{ - items: MenuItem[]; - align?: 'center' | string; - width?: number; - viaKeyboard?: boolean; - src?: any; -}>(); - -const emit = defineEmits<{ - (ev: 'closed'): void; -}>(); - -let modal = $ref<InstanceType<typeof MkModal>>(); -</script> - -<style lang="scss" scoped> -.sfhdhdhq { - &.drawer { - border-radius: 24px; - border-bottom-right-radius: 0; - border-bottom-left-radius: 0; - } -} -</style> diff --git a/packages/client/src/components/ui/super-menu.vue b/packages/client/src/components/ui/super-menu.vue deleted file mode 100644 index 8ce2dc5dc2..0000000000 --- a/packages/client/src/components/ui/super-menu.vue +++ /dev/null @@ -1,148 +0,0 @@ -<template> -<div class="rrevdjwu" :class="{ grid }"> - <div v-for="group in def" class="group"> - <div v-if="group.title" class="title">{{ group.title }}</div> - - <div class="items"> - <template v-for="(item, i) in group.items"> - <a v-if="item.type === 'a'" :href="item.href" :target="item.target" :tabindex="i" class="_button item" :class="{ danger: item.danger, active: item.active }"> - <i v-if="item.icon" class="icon fa-fw" :class="item.icon"></i> - <span class="text">{{ item.text }}</span> - </a> - <button v-else-if="item.type === 'button'" :tabindex="i" class="_button item" :class="{ danger: item.danger, active: item.active }" :disabled="item.active" @click="ev => item.action(ev)"> - <i v-if="item.icon" class="icon fa-fw" :class="item.icon"></i> - <span class="text">{{ item.text }}</span> - </button> - <MkA v-else :to="item.to" :tabindex="i" class="_button item" :class="{ danger: item.danger, active: item.active }"> - <i v-if="item.icon" class="icon fa-fw" :class="item.icon"></i> - <span class="text">{{ item.text }}</span> - </MkA> - </template> - </div> - </div> -</div> -</template> - -<script lang="ts"> -import { defineComponent, ref, unref } from 'vue'; - -export default defineComponent({ - props: { - def: { - type: Array, - required: true, - }, - grid: { - type: Boolean, - required: false, - default: false, - }, - }, -}); -</script> - -<style lang="scss" scoped> -.rrevdjwu { - > .group { - & + .group { - margin-top: 16px; - padding-top: 16px; - border-top: solid 0.5px var(--divider); - } - - > .title { - opacity: 0.7; - margin: 0 0 8px 0; - font-size: 0.9em; - } - - > .items { - > .item { - display: flex; - align-items: center; - width: 100%; - box-sizing: border-box; - padding: 10px 16px 10px 8px; - border-radius: 9px; - font-size: 0.9em; - - &:hover { - text-decoration: none; - background: var(--panelHighlight); - } - - &.active { - color: var(--accent); - background: var(--accentedBg); - } - - &.danger { - color: var(--error); - } - - > .icon { - width: 32px; - margin-right: 2px; - flex-shrink: 0; - text-align: center; - opacity: 0.8; - } - - > .text { - white-space: nowrap; - text-overflow: ellipsis; - overflow: hidden; - padding-right: 12px; - } - - } - } - } - - &.grid { - > .group { - & + .group { - padding-top: 0; - border-top: none; - } - - margin-left: 0; - margin-right: 0; - - > .title { - font-size: 1em; - opacity: 0.7; - margin: 0 0 8px 16px; - } - - > .items { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(130px, 1fr)); - grid-gap: 8px; - padding: 0 16px; - - > .item { - flex-direction: column; - padding: 18px 16px 16px 16px; - background: var(--panel); - border-radius: 8px; - text-align: center; - - > .icon { - display: block; - margin-right: 0; - margin-bottom: 12px; - font-size: 1.5em; - } - - > .text { - padding-right: 0; - width: 100%; - font-size: 0.8em; - } - } - } - } - } -} -</style> diff --git a/packages/client/src/components/ui/tooltip.vue b/packages/client/src/components/ui/tooltip.vue deleted file mode 100644 index 4c6258d245..0000000000 --- a/packages/client/src/components/ui/tooltip.vue +++ /dev/null @@ -1,101 +0,0 @@ -<template> -<transition :name="$store.state.animation ? 'tooltip' : ''" appear @after-leave="emit('closed')"> - <div v-show="showing" ref="el" class="buebdbiu _acrylic _shadow" :style="{ zIndex, maxWidth: maxWidth + 'px' }"> - <slot> - <Mfm v-if="asMfm" :text="text"/> - <span v-else>{{ text }}</span> - </slot> - </div> -</transition> -</template> - -<script lang="ts" setup> -import { nextTick, onMounted, onUnmounted, ref } from 'vue'; -import * as os from '@/os'; -import { calcPopupPosition } from '@/scripts/popup-position'; - -const props = withDefaults(defineProps<{ - showing: boolean; - targetElement?: HTMLElement; - x?: number; - y?: number; - text?: string; - asMfm?: boolean; - maxWidth?: number; - direction?: 'top' | 'bottom' | 'right' | 'left'; - innerMargin?: number; -}>(), { - maxWidth: 250, - direction: 'top', - innerMargin: 0, -}); - -const emit = defineEmits<{ - (ev: 'closed'): void; -}>(); - -const el = ref<HTMLElement>(); -const zIndex = os.claimZIndex('high'); - -function setPosition() { - const data = calcPopupPosition(el.value, { - anchorElement: props.targetElement, - direction: props.direction, - align: 'center', - innerMargin: props.innerMargin, - x: props.x, - y: props.y, - }); - - el.value.style.transformOrigin = data.transformOrigin; - el.value.style.left = data.left + 'px'; - el.value.style.top = data.top + 'px'; -} - -let loopHandler; - -onMounted(() => { - nextTick(() => { - setPosition(); - - const loop = () => { - loopHandler = window.requestAnimationFrame(() => { - setPosition(); - loop(); - }); - }; - - loop(); - }); -}); - -onUnmounted(() => { - window.cancelAnimationFrame(loopHandler); -}); -</script> - -<style lang="scss" scoped> -.tooltip-enter-active, -.tooltip-leave-active { - opacity: 1; - transform: scale(1); - transition: transform 200ms cubic-bezier(0.23, 1, 0.32, 1), opacity 200ms cubic-bezier(0.23, 1, 0.32, 1); -} -.tooltip-enter-from, -.tooltip-leave-active { - opacity: 0; - transform: scale(0.75); -} - -.buebdbiu { - position: absolute; - font-size: 0.8em; - padding: 8px 12px; - box-sizing: border-box; - text-align: center; - border-radius: 4px; - border: solid 0.5px var(--divider); - pointer-events: none; - transform-origin: center center; -} -</style> diff --git a/packages/client/src/components/ui/window.vue b/packages/client/src/components/ui/window.vue deleted file mode 100644 index 758d4d47b6..0000000000 --- a/packages/client/src/components/ui/window.vue +++ /dev/null @@ -1,563 +0,0 @@ -<template> -<transition :name="$store.state.animation ? 'window' : ''" appear @after-leave="$emit('closed')"> - <div v-if="showing" ref="rootEl" class="ebkgocck" :class="{ maximized }"> - <div class="body _shadow _narrow_" @mousedown="onBodyMousedown" @keydown="onKeydown"> - <div class="header" :class="{ mini }" @contextmenu.prevent.stop="onContextmenu"> - <span class="left"> - <button v-for="button in buttonsLeft" v-tooltip="button.title" class="button _button" :class="{ highlighted: button.highlighted }" @click="button.onClick"><i :class="button.icon"></i></button> - </span> - <span class="title" @mousedown.prevent="onHeaderMousedown" @touchstart.prevent="onHeaderMousedown"> - <slot name="header"></slot> - </span> - <span class="right"> - <button v-for="button in buttonsRight" v-tooltip="button.title" class="button _button" :class="{ highlighted: button.highlighted }" @click="button.onClick"><i :class="button.icon"></i></button> - <button v-if="canResize && maximized" class="button _button" @click="unMaximize()"><i class="fas fa-window-restore"></i></button> - <button v-else-if="canResize && !maximized" class="button _button" @click="maximize()"><i class="fas fa-window-maximize"></i></button> - <button v-if="closeButton" class="button _button" @click="close()"><i class="fas fa-times"></i></button> - </span> - </div> - <div class="body"> - <slot></slot> - </div> - </div> - <template v-if="canResize"> - <div class="handle top" @mousedown.prevent="onTopHandleMousedown"></div> - <div class="handle right" @mousedown.prevent="onRightHandleMousedown"></div> - <div class="handle bottom" @mousedown.prevent="onBottomHandleMousedown"></div> - <div class="handle left" @mousedown.prevent="onLeftHandleMousedown"></div> - <div class="handle top-left" @mousedown.prevent="onTopLeftHandleMousedown"></div> - <div class="handle top-right" @mousedown.prevent="onTopRightHandleMousedown"></div> - <div class="handle bottom-right" @mousedown.prevent="onBottomRightHandleMousedown"></div> - <div class="handle bottom-left" @mousedown.prevent="onBottomLeftHandleMousedown"></div> - </template> - </div> -</transition> -</template> - -<script lang="ts" setup> -import { onBeforeUnmount, onMounted, provide } from 'vue'; -import contains from '@/scripts/contains'; -import * as os from '@/os'; -import { MenuItem } from '@/types/menu'; - -const minHeight = 50; -const minWidth = 250; - -function dragListen(fn: (ev: MouseEvent) => void) { - window.addEventListener('mousemove', fn); - window.addEventListener('touchmove', fn); - window.addEventListener('mouseleave', dragClear.bind(null, fn)); - window.addEventListener('mouseup', dragClear.bind(null, fn)); - window.addEventListener('touchend', dragClear.bind(null, fn)); -} - -function dragClear(fn) { - window.removeEventListener('mousemove', fn); - window.removeEventListener('touchmove', fn); - window.removeEventListener('mouseleave', dragClear); - window.removeEventListener('mouseup', dragClear); - window.removeEventListener('touchend', dragClear); -} - -const props = withDefaults(defineProps<{ - initialWidth?: number; - initialHeight?: number | null; - canResize?: boolean; - closeButton?: boolean; - mini?: boolean; - front?: boolean; - contextmenu?: MenuItem[] | null; - buttonsLeft?: any[]; - buttonsRight?: any[]; -}>(), { - initialWidth: 400, - initialHeight: null, - canResize: false, - closeButton: true, - mini: false, - front: false, - contextmenu: null, - buttonsLeft: () => [], - buttonsRight: () => [], -}); - -const emit = defineEmits<{ - (ev: 'closed'): void; -}>(); - -provide('inWindow', true); - -let rootEl = $ref<HTMLElement | null>(); -let showing = $ref(true); -let beforeClickedAt = 0; -let maximized = $ref(false); -let unMaximizedTop = ''; -let unMaximizedLeft = ''; -let unMaximizedWidth = ''; -let unMaximizedHeight = ''; - -function close() { - showing = false; -} - -function onKeydown(evt) { - if (evt.which === 27) { // Esc - evt.preventDefault(); - evt.stopPropagation(); - close(); - } -} - -function onContextmenu(ev: MouseEvent) { - if (props.contextmenu) { - os.contextMenu(props.contextmenu, ev); - } -} - -// 最前面へ移動 -function top() { - if (rootEl) { - rootEl.style.zIndex = os.claimZIndex(props.front ? 'middle' : 'low'); - } -} - -function maximize() { - maximized = true; - unMaximizedTop = rootEl.style.top; - unMaximizedLeft = rootEl.style.left; - unMaximizedWidth = rootEl.style.width; - unMaximizedHeight = rootEl.style.height; - rootEl.style.top = '0'; - rootEl.style.left = '0'; - rootEl.style.width = '100%'; - rootEl.style.height = '100%'; -} - -function unMaximize() { - maximized = false; - rootEl.style.top = unMaximizedTop; - rootEl.style.left = unMaximizedLeft; - rootEl.style.width = unMaximizedWidth; - rootEl.style.height = unMaximizedHeight; -} - -function onBodyMousedown() { - top(); -} - -function onDblClick() { - maximize(); -} - -function onHeaderMousedown(evt: MouseEvent) { - // 右クリックはコンテキストメニューを開こうとした可能性が高いため無視 - if (evt.button === 2) return; - - let beforeMaximized = false; - - if (maximized) { - beforeMaximized = true; - unMaximize(); - } - - // ダブルクリック判定 - if (Date.now() - beforeClickedAt < 300) { - beforeClickedAt = Date.now(); - onDblClick(); - return; - } - - beforeClickedAt = Date.now(); - - const main = rootEl; - if (main == null) return; - - if (!contains(main, document.activeElement)) main.focus(); - - const position = main.getBoundingClientRect(); - - const clickX = evt.touches && evt.touches.length > 0 ? evt.touches[0].clientX : evt.clientX; - const clickY = evt.touches && evt.touches.length > 0 ? evt.touches[0].clientY : evt.clientY; - const moveBaseX = beforeMaximized ? parseInt(unMaximizedWidth, 10) / 2 : clickX - position.left; // TODO: parseIntやめる - const moveBaseY = beforeMaximized ? 20 : clickY - position.top; - const browserWidth = window.innerWidth; - const browserHeight = window.innerHeight; - const windowWidth = main.offsetWidth; - const windowHeight = main.offsetHeight; - - function move(x: number, y: number) { - let moveLeft = x - moveBaseX; - let moveTop = y - moveBaseY; - - // 下はみ出し - if (moveTop + windowHeight > browserHeight) moveTop = browserHeight - windowHeight; - - // 左はみ出し - if (moveLeft < 0) moveLeft = 0; - - // 上はみ出し - if (moveTop < 0) moveTop = 0; - - // 右はみ出し - if (moveLeft + windowWidth > browserWidth) moveLeft = browserWidth - windowWidth; - - rootEl.style.left = moveLeft + 'px'; - rootEl.style.top = moveTop + 'px'; - } - - if (beforeMaximized) { - move(clickX, clickY); - } - - // 動かした時 - dragListen(me => { - const x = me.touches && me.touches.length > 0 ? me.touches[0].clientX : me.clientX; - const y = me.touches && me.touches.length > 0 ? me.touches[0].clientY : me.clientY; - - move(x, y); - }); -} - -// 上ハンドル掴み時 -function onTopHandleMousedown(evt) { - const main = rootEl; - - const base = evt.clientY; - const height = parseInt(getComputedStyle(main, '').height, 10); - const top = parseInt(getComputedStyle(main, '').top, 10); - - // 動かした時 - dragListen(me => { - const move = me.clientY - base; - if (top + move > 0) { - if (height + -move > minHeight) { - applyTransformHeight(height + -move); - applyTransformTop(top + move); - } else { // 最小の高さより小さくなろうとした時 - applyTransformHeight(minHeight); - applyTransformTop(top + (height - minHeight)); - } - } else { // 上のはみ出し時 - applyTransformHeight(top + height); - applyTransformTop(0); - } - }); -} - -// 右ハンドル掴み時 -function onRightHandleMousedown(evt) { - const main = rootEl; - - const base = evt.clientX; - const width = parseInt(getComputedStyle(main, '').width, 10); - const left = parseInt(getComputedStyle(main, '').left, 10); - const browserWidth = window.innerWidth; - - // 動かした時 - dragListen(me => { - const move = me.clientX - base; - if (left + width + move < browserWidth) { - if (width + move > minWidth) { - applyTransformWidth(width + move); - } else { // 最小の幅より小さくなろうとした時 - applyTransformWidth(minWidth); - } - } else { // 右のはみ出し時 - applyTransformWidth(browserWidth - left); - } - }); -} - -// 下ハンドル掴み時 -function onBottomHandleMousedown(evt) { - const main = rootEl; - - const base = evt.clientY; - const height = parseInt(getComputedStyle(main, '').height, 10); - const top = parseInt(getComputedStyle(main, '').top, 10); - const browserHeight = window.innerHeight; - - // 動かした時 - dragListen(me => { - const move = me.clientY - base; - if (top + height + move < browserHeight) { - if (height + move > minHeight) { - applyTransformHeight(height + move); - } else { // 最小の高さより小さくなろうとした時 - applyTransformHeight(minHeight); - } - } else { // 下のはみ出し時 - applyTransformHeight(browserHeight - top); - } - }); -} - -// 左ハンドル掴み時 -function onLeftHandleMousedown(evt) { - const main = rootEl; - - const base = evt.clientX; - const width = parseInt(getComputedStyle(main, '').width, 10); - const left = parseInt(getComputedStyle(main, '').left, 10); - - // 動かした時 - dragListen(me => { - const move = me.clientX - base; - if (left + move > 0) { - if (width + -move > minWidth) { - applyTransformWidth(width + -move); - applyTransformLeft(left + move); - } else { // 最小の幅より小さくなろうとした時 - applyTransformWidth(minWidth); - applyTransformLeft(left + (width - minWidth)); - } - } else { // 左のはみ出し時 - applyTransformWidth(left + width); - applyTransformLeft(0); - } - }); -} - -// 左上ハンドル掴み時 -function onTopLeftHandleMousedown(evt) { - onTopHandleMousedown(evt); - onLeftHandleMousedown(evt); -} - -// 右上ハンドル掴み時 -function onTopRightHandleMousedown(evt) { - onTopHandleMousedown(evt); - onRightHandleMousedown(evt); -} - -// 右下ハンドル掴み時 -function onBottomRightHandleMousedown(evt) { - onBottomHandleMousedown(evt); - onRightHandleMousedown(evt); -} - -// 左下ハンドル掴み時 -function onBottomLeftHandleMousedown(evt) { - onBottomHandleMousedown(evt); - onLeftHandleMousedown(evt); -} - -// 高さを適用 -function applyTransformHeight(height) { - if (height > window.innerHeight) height = window.innerHeight; - rootEl.style.height = height + 'px'; -} - -// 幅を適用 -function applyTransformWidth(width) { - if (width > window.innerWidth) width = window.innerWidth; - rootEl.style.width = width + 'px'; -} - -// Y座標を適用 -function applyTransformTop(top) { - rootEl.style.top = top + 'px'; -} - -// X座標を適用 -function applyTransformLeft(left) { - rootEl.style.left = left + 'px'; -} - -function onBrowserResize() { - const main = rootEl; - const position = main.getBoundingClientRect(); - const browserWidth = window.innerWidth; - const browserHeight = window.innerHeight; - const windowWidth = main.offsetWidth; - const windowHeight = main.offsetHeight; - if (position.left < 0) main.style.left = '0'; // 左はみ出し - if (position.top + windowHeight > browserHeight) main.style.top = browserHeight - windowHeight + 'px'; // 下はみ出し - if (position.left + windowWidth > browserWidth) main.style.left = browserWidth - windowWidth + 'px'; // 右はみ出し - if (position.top < 0) main.style.top = '0'; // 上はみ出し -} - -onMounted(() => { - if (props.initialWidth) applyTransformWidth(props.initialWidth); - if (props.initialHeight) applyTransformHeight(props.initialHeight); - - applyTransformTop((window.innerHeight / 2) - (rootEl.offsetHeight / 2)); - applyTransformLeft((window.innerWidth / 2) - (rootEl.offsetWidth / 2)); - - // 他のウィンドウ内のボタンなどを押してこのウィンドウが開かれた場合、親が最前面になろうとするのでそれに隠されないようにする - top(); - - window.addEventListener('resize', onBrowserResize); -}); - -onBeforeUnmount(() => { - window.removeEventListener('resize', onBrowserResize); -}); - -defineExpose({ - close, -}); -</script> - -<style lang="scss" scoped> -.window-enter-active, .window-leave-active { - transition: opacity 0.2s, transform 0.2s !important; -} -.window-enter-from, .window-leave-to { - pointer-events: none; - opacity: 0; - transform: scale(0.9); -} - -.ebkgocck { - position: fixed; - top: 0; - left: 0; - - > .body { - overflow: clip; - display: flex; - flex-direction: column; - contain: content; - width: 100%; - height: 100%; - border-radius: var(--radius); - - > .header { - --height: 42px; - - &.mini { - --height: 38px; - } - - display: flex; - position: relative; - z-index: 1; - flex-shrink: 0; - user-select: none; - height: var(--height); - background: var(--windowHeader); - -webkit-backdrop-filter: var(--blur, blur(15px)); - backdrop-filter: var(--blur, blur(15px)); - //border-bottom: solid 1px var(--divider); - font-size: 95%; - font-weight: bold; - - > .left, > .right { - > .button { - height: var(--height); - width: var(--height); - - &:hover { - color: var(--fgHighlighted); - } - - &.highlighted { - color: var(--accent); - } - } - } - - > .left { - margin-right: 16px; - } - - > .right { - min-width: 16px; - } - - > .title { - flex: 1; - position: relative; - line-height: var(--height); - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - cursor: move; - } - } - - > .body { - flex: 1; - overflow: auto; - background: var(--panel); - } - } - - > .handle { - $size: 8px; - - position: absolute; - - &.top { - top: -($size); - left: 0; - width: 100%; - height: $size; - cursor: ns-resize; - } - - &.right { - top: 0; - right: -($size); - width: $size; - height: 100%; - cursor: ew-resize; - } - - &.bottom { - bottom: -($size); - left: 0; - width: 100%; - height: $size; - cursor: ns-resize; - } - - &.left { - top: 0; - left: -($size); - width: $size; - height: 100%; - cursor: ew-resize; - } - - &.top-left { - top: -($size); - left: -($size); - width: $size * 2; - height: $size * 2; - cursor: nwse-resize; - } - - &.top-right { - top: -($size); - right: -($size); - width: $size * 2; - height: $size * 2; - cursor: nesw-resize; - } - - &.bottom-right { - bottom: -($size); - right: -($size); - width: $size * 2; - height: $size * 2; - cursor: nwse-resize; - } - - &.bottom-left { - bottom: -($size); - left: -($size); - width: $size * 2; - height: $size * 2; - cursor: nesw-resize; - } - } - - &.maximized { - > .body { - border-radius: 0; - } - } -} -</style> |