diff options
| author | syuilo <Syuilotan@yahoo.co.jp> | 2021-11-12 02:02:25 +0900 |
|---|---|---|
| committer | syuilo <Syuilotan@yahoo.co.jp> | 2021-11-12 02:02:25 +0900 |
| commit | 0e4a111f81cceed275d9bec2695f6e401fb654d8 (patch) | |
| tree | 40874799472fa07416f17b50a398ac33b7771905 /src/client/components/ui | |
| parent | update deps (diff) | |
| download | sharkey-0e4a111f81cceed275d9bec2695f6e401fb654d8.tar.gz sharkey-0e4a111f81cceed275d9bec2695f6e401fb654d8.tar.bz2 sharkey-0e4a111f81cceed275d9bec2695f6e401fb654d8.zip | |
refactoring
Resolve #7779
Diffstat (limited to 'src/client/components/ui')
| -rw-r--r-- | src/client/components/ui/button.vue | 262 | ||||
| -rw-r--r-- | src/client/components/ui/container.vue | 262 | ||||
| -rw-r--r-- | src/client/components/ui/context-menu.vue | 97 | ||||
| -rw-r--r-- | src/client/components/ui/folder.vue | 156 | ||||
| -rw-r--r-- | src/client/components/ui/hr.vue | 16 | ||||
| -rw-r--r-- | src/client/components/ui/info.vue | 45 | ||||
| -rw-r--r-- | src/client/components/ui/menu.vue | 278 | ||||
| -rw-r--r-- | src/client/components/ui/modal-window.vue | 148 | ||||
| -rw-r--r-- | src/client/components/ui/modal.vue | 292 | ||||
| -rw-r--r-- | src/client/components/ui/pagination.vue | 69 | ||||
| -rw-r--r-- | src/client/components/ui/popup-menu.vue | 42 | ||||
| -rw-r--r-- | src/client/components/ui/popup.vue | 213 | ||||
| -rw-r--r-- | src/client/components/ui/super-menu.vue | 148 | ||||
| -rw-r--r-- | src/client/components/ui/tooltip.vue | 92 | ||||
| -rw-r--r-- | src/client/components/ui/window.vue | 525 |
15 files changed, 0 insertions, 2645 deletions
diff --git a/src/client/components/ui/button.vue b/src/client/components/ui/button.vue deleted file mode 100644 index b5f4547c84..0000000000 --- a/src/client/components/ui/button.vue +++ /dev/null @@ -1,262 +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"> -import { defineComponent } from 'vue'; - -export default defineComponent({ - props: { - type: { - type: String, - required: false - }, - primary: { - type: Boolean, - required: false, - default: false - }, - gradate: { - type: Boolean, - required: false, - default: false - }, - rounded: { - type: Boolean, - required: false, - default: false - }, - inline: { - type: Boolean, - required: false, - default: false - }, - link: { - type: Boolean, - required: false, - default: false - }, - to: { - type: String, - required: false - }, - autofocus: { - type: Boolean, - required: false, - default: false - }, - wait: { - type: Boolean, - required: false, - default: false - }, - danger: { - type: Boolean, - required: false, - default: false - }, - full: { - type: Boolean, - required: false, - default: false - }, - }, - emits: ['click'], - mounted() { - if (this.autofocus) { - this.$nextTick(() => { - this.$el.focus(); - }); - } - }, - methods: { - onMousedown(e: MouseEvent) { - function distance(p, q) { - return Math.hypot(p.x - q.x, p.y - q.y); - } - - function calcCircleScale(boxW, boxH, circleCenterX, circleCenterY) { - 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; - } - - const rect = e.target.getBoundingClientRect(); - - const ripple = document.createElement('div'); - ripple.style.top = (e.clientY - rect.top - 1).toString() + 'px'; - ripple.style.left = (e.clientX - rect.left - 1).toString() + 'px'; - - this.$refs.ripples.appendChild(ripple); - - const circleCenterX = e.clientX - rect.left; - const circleCenterY = e.clientY - rect.top; - - const scale = calcCircleScale(e.target.clientWidth, e.target.clientHeight, circleCenterX, circleCenterY); - - setTimeout(() => { - ripple.style.transform = 'scale(' + (scale / 2) + ')'; - }, 1); - setTimeout(() => { - ripple.style.transition = 'all 1s ease'; - ripple.style.opacity = '0'; - }, 1000); - setTimeout(() => { - if (this.$refs.ripples) this.$refs.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 14px; - text-align: center; - font-weight: normal; - font-size: 0.8em; - line-height: 22px; - box-shadow: none; - text-decoration: none; - background: var(--buttonBg); - border-radius: 4px; - 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/src/client/components/ui/container.vue b/src/client/components/ui/container.vue deleted file mode 100644 index 14673dfcd7..0000000000 --- a/src/client/components/ui/container.vue +++ /dev/null @@ -1,262 +0,0 @@ -<template> -<div class="ukygtjoj _panel" :class="{ naked, thin, hideHeader: !showHeader, scrollable, closed: !showBody }" v-size="{ max: [380] }"> - <header v-if="showHeader" ref="header"> - <div class="title"><slot name="header"></slot></div> - <div class="sub"> - <slot name="func"></slot> - <button class="_button" v-if="foldable" @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="container-toggle" - @enter="enter" - @after-enter="afterEnter" - @leave="leave" - @after-leave="afterLeave" - > - <div v-show="showBody" class="content" :class="{ omitted }" ref="content"> - <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; - - &.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/src/client/components/ui/context-menu.vue b/src/client/components/ui/context-menu.vue deleted file mode 100644 index 61f5d3bf08..0000000000 --- a/src/client/components/ui/context-menu.vue +++ /dev/null @@ -1,97 +0,0 @@ -<template> -<transition :name="$store.state.animation ? 'fade' : ''" appear> - <div class="nvlagfpb" @contextmenu.prevent.stop="() => {}"> - <MkMenu :items="items" @close="$emit('closed')" class="_popup _shadow" :align="'left'"/> - </div> -</transition> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; -import contains from '@client/scripts/contains'; -import MkMenu from './menu.vue'; - -export default defineComponent({ - components: { - MkMenu, - }, - props: { - items: { - type: Array, - required: true - }, - ev: { - required: true - }, - viaKeyboard: { - type: Boolean, - required: false - }, - }, - emits: ['closed'], - computed: { - keymap(): any { - return { - 'esc': () => this.$emit('closed'), - }; - }, - }, - mounted() { - let left = this.ev.pageX + 1; // 間違って右ダブルクリックした場合に意図せずアイテムがクリックされるのを防ぐため + 1 - let top = this.ev.pageY + 1; // 間違って右ダブルクリックした場合に意図せずアイテムがクリックされるのを防ぐため + 1 - - const width = this.$el.offsetWidth; - const height = this.$el.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; - } - - this.$el.style.top = top + 'px'; - this.$el.style.left = left + 'px'; - - for (const el of Array.from(document.querySelectorAll('body *'))) { - el.addEventListener('mousedown', this.onMousedown); - } - }, - beforeUnmount() { - for (const el of Array.from(document.querySelectorAll('body *'))) { - el.removeEventListener('mousedown', this.onMousedown); - } - }, - methods: { - onMousedown(e) { - if (!contains(this.$el, e.target) && (this.$el != e.target)) this.$emit('closed'); - }, - } -}); -</script> - -<style lang="scss" scoped> -.nvlagfpb { - position: absolute; - z-index: 65535; -} - -.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/src/client/components/ui/folder.vue b/src/client/components/ui/folder.vue deleted file mode 100644 index 3997421d08..0000000000 --- a/src/client/components/ui/folder.vue +++ /dev/null @@ -1,156 +0,0 @@ -<template> -<div class="ssazuxis" v-size="{ max: [500] }"> - <header @click="showBody = !showBody" class="_button" :style="{ background: bg }"> - <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="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 * as 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/src/client/components/ui/hr.vue b/src/client/components/ui/hr.vue deleted file mode 100644 index fb12b4985f..0000000000 --- a/src/client/components/ui/hr.vue +++ /dev/null @@ -1,16 +0,0 @@ -<template> -<div class="evrzpitu"></div> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue';import * as os from '@client/os'; - -export default defineComponent({}); -</script> - -<style lang="scss" scoped> -.evrzpitu - margin 16px 0 - border-bottom solid var(--lineWidth) var(--faceDivider) - -</style> diff --git a/src/client/components/ui/info.vue b/src/client/components/ui/info.vue deleted file mode 100644 index f6b2edf267..0000000000 --- a/src/client/components/ui/info.vue +++ /dev/null @@ -1,45 +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"> -import { defineComponent } from 'vue'; -import * as os from '@client/os'; - -export default defineComponent({ - props: { - warn: { - type: Boolean, - required: false, - default: false - }, - }, - data() { - return { - }; - } -}); -</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/src/client/components/ui/menu.vue b/src/client/components/ui/menu.vue deleted file mode 100644 index 5b3a0ae7c2..0000000000 --- a/src/client/components/ui/menu.vue +++ /dev/null @@ -1,278 +0,0 @@ -<template> -<div class="rrevdjwt" :class="{ center: align === 'center' }" - :style="{ width: width ? width + 'px' : null }" - ref="items" - @contextmenu.self="e => e.preventDefault()" - v-hotkey="keymap" -> - <template v-for="(item, i) in _items"> - <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" @click.passive="close()" :tabindex="i" class="_button 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" @click="close()" :tabindex="i" class="_button 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'" @click="clicked(item.action, $event)" :tabindex="i" class="_button 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> - <button v-else @click="clicked(item.action, $event)" :tabindex="i" class="_button item" :class="{ danger: item.danger, active: item.active }" :disabled="item.active"> - <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="_items.length === 0" class="none item"> - <span>{{ $ts.none }}</span> - </span> -</div> -</template> - -<script lang="ts"> -import { defineComponent, ref, unref } from 'vue'; -import { focusPrev, focusNext } from '@client/scripts/focus'; -import contains from '@client/scripts/contains'; - -export default defineComponent({ - props: { - items: { - type: Array, - required: true - }, - viaKeyboard: { - type: Boolean, - required: false - }, - align: { - type: String, - requried: false - }, - width: { - type: Number, - required: false - }, - }, - emits: ['close'], - data() { - return { - _items: [], - }; - }, - computed: { - keymap(): any { - return { - 'up|k|shift+tab': this.focusUp, - 'down|j|tab': this.focusDown, - 'esc': this.close, - }; - }, - }, - watch: { - items: { - handler() { - const items = ref(unref(this.items).filter(item => item !== undefined)); - - for (let i = 0; i < items.value.length; i++) { - const item = items.value[i]; - - if (item && item.then) { // if item is Promise - items.value[i] = { type: 'pending' }; - item.then(actualItem => { - items.value[i] = actualItem; - }); - } - } - - this._items = items; - }, - immediate: true - } - }, - mounted() { - if (this.viaKeyboard) { - this.$nextTick(() => { - focusNext(this.$refs.items.children[0], true, false); - }); - } - - if (this.contextmenuEvent) { - this.$el.style.top = this.contextmenuEvent.pageY + 'px'; - this.$el.style.left = this.contextmenuEvent.pageX + 'px'; - - for (const el of Array.from(document.querySelectorAll('body *'))) { - el.addEventListener('mousedown', this.onMousedown); - } - } - }, - beforeUnmount() { - for (const el of Array.from(document.querySelectorAll('body *'))) { - el.removeEventListener('mousedown', this.onMousedown); - } - }, - methods: { - clicked(fn, ev) { - fn(ev); - this.close(); - }, - close() { - this.$emit('close'); - }, - focusUp() { - focusPrev(document.activeElement); - }, - focusDown() { - focusNext(document.activeElement); - }, - onMousedown(e) { - if (!contains(this.$el, e.target) && (this.$el != e.target)) this.close(); - }, - } -}); -</script> - -<style lang="scss" scoped> -.rrevdjwt { - padding: 8px 0; - min-width: 200px; - max-height: 90vh; - overflow: auto; - - &.center { - > .item { - text-align: center; - } - } - - > .item { - display: block; - position: relative; - padding: 8px 18px; - 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; - } - - &.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(:disabled):hover { - color: var(--accent); - text-decoration: none; - - &:before { - background: var(--accentedBg); - } - } - - &: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; - } - - > 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; - height: 1px; - background: var(--divider); - } -} -</style> diff --git a/src/client/components/ui/modal-window.vue b/src/client/components/ui/modal-window.vue deleted file mode 100644 index da98192b87..0000000000 --- a/src/client/components/ui/modal-window.vue +++ /dev/null @@ -1,148 +0,0 @@ -<template> -<MkModal ref="modal" @click="$emit('click')" @closed="$emit('closed')"> - <div class="ebkgoccj _window _narrow_" @keydown="onKeydown" :style="{ width: `${width}px`, height: scroll ? (height ? `${height}px` : null) : (height ? `min(${height}px, 100%)` : '100%') }"> - <div class="header"> - <button class="_button" v-if="withOkButton" @click="$emit('close')"><i class="fas fa-times"></i></button> - <span class="title"> - <slot name="header"></slot> - </span> - <button class="_button" v-if="!withOkButton" @click="$emit('close')"><i class="fas fa-times"></i></button> - <button class="_button" v-if="withOkButton" @click="$emit('ok')" :disabled="okButtonDisabled"><i class="fas fa-check"></i></button> - </div> - <div class="body" v-if="padding"> - <div class="_section"> - <slot></slot> - </div> - </div> - <div class="body" v-else> - <slot></slot> - </div> - </div> -</MkModal> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; -import MkModal from './modal.vue'; - -export default defineComponent({ - components: { - MkModal - }, - props: { - withOkButton: { - type: Boolean, - required: false, - default: false - }, - okButtonDisabled: { - type: Boolean, - required: false, - default: false - }, - padding: { - type: Boolean, - required: false, - default: false - }, - width: { - type: Number, - required: false, - default: 400 - }, - height: { - type: Number, - required: false, - default: null - }, - canClose: { - type: Boolean, - required: false, - default: true, - }, - scroll: { - type: Boolean, - required: false, - default: true, - }, - }, - - emits: ['click', 'close', 'closed', 'ok'], - - data() { - return { - }; - }, - - methods: { - close() { - this.$refs.modal.close(); - }, - - onKeydown(e) { - if (e.which === 27) { // Esc - e.preventDefault(); - e.stopPropagation(); - this.close(); - } - }, - } -}); -</script> - -<style lang="scss" scoped> -.ebkgoccj { - overflow: hidden; - display: flex; - flex-direction: column; - contain: content; - - --root-margin: 24px; - - @media (max-width: 500px) { - --root-margin: 16px; - } - - > .header { - $height: 58px; - $height-narrow: 42px; - display: flex; - flex-shrink: 0; - box-shadow: 0px 1px var(--divider); - - > 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 { - overflow: auto; - } -} -</style> diff --git a/src/client/components/ui/modal.vue b/src/client/components/ui/modal.vue deleted file mode 100644 index 33fcdb687f..0000000000 --- a/src/client/components/ui/modal.vue +++ /dev/null @@ -1,292 +0,0 @@ -<template> -<transition :name="$store.state.animation ? popup ? 'modal-popup' : 'modal' : ''" :duration="$store.state.animation ? popup ? 500 : 300 : 0" appear @after-leave="onClosed" @enter="$emit('opening')" @after-enter="childRendered"> - <div v-show="manualShowing != null ? manualShowing : showing" class="qzhlnise" :class="{ front }" v-hotkey.global="keymap" :style="{ pointerEvents: (manualShowing != null ? manualShowing : showing) ? 'auto' : 'none', '--transformOrigin': transformOrigin }"> - <div class="bg _modalBg" @click="onBgClick" @contextmenu.prevent.stop="() => {}"></div> - <div class="content" :class="{ popup, fixed, top: position === 'top' }" @click.self="onBgClick" ref="content"> - <slot></slot> - </div> - </div> -</transition> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; - -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); - } -} - -export default defineComponent({ - provide: { - modal: true - }, - props: { - manualShowing: { - type: Boolean, - required: false, - default: null, - }, - srcCenter: { - type: Boolean, - required: false - }, - src: { - required: false, - }, - position: { - required: false - }, - front: { - type: Boolean, - required: false, - default: false, - } - }, - emits: ['opening', 'click', 'esc', 'close', 'closed'], - data() { - return { - showing: true, - fixed: false, - transformOrigin: 'center', - contentClicking: false, - }; - }, - computed: { - keymap(): any { - return { - 'esc': () => this.$emit('esc'), - }; - }, - popup(): boolean { - return this.src != null; - } - }, - mounted() { - this.$watch('src', () => { - this.fixed = getFixedContainer(this.src) != null; - this.$nextTick(() => { - this.align(); - }); - }, { immediate: true }); - - this.$nextTick(() => { - const popover = this.$refs.content as any; - new ResizeObserver((entries, observer) => { - this.align(); - }).observe(popover); - }); - }, - methods: { - align() { - if (!this.popup) return; - - const popover = this.$refs.content as any; - - if (popover == null) return; - - const rect = this.src.getBoundingClientRect(); - - const width = popover.offsetWidth; - const height = popover.offsetHeight; - - let left; - let top; - - if (this.srcCenter) { - const x = rect.left + (this.fixed ? 0 : window.pageXOffset) + (this.src.offsetWidth / 2); - const y = rect.top + (this.fixed ? 0 : window.pageYOffset) + (this.src.offsetHeight / 2); - left = (x - (width / 2)); - top = (y - (height / 2)); - } else { - const x = rect.left + (this.fixed ? 0 : window.pageXOffset) + (this.src.offsetWidth / 2); - const y = rect.top + (this.fixed ? 0 : window.pageYOffset) + this.src.offsetHeight; - left = (x - (width / 2)); - top = y; - } - - if (this.fixed) { - if (left + width > window.innerWidth) { - left = window.innerWidth - width; - } - - if (top + height > window.innerHeight) { - top = window.innerHeight - height; - } - } else { - if (left + width - window.pageXOffset > window.innerWidth) { - left = window.innerWidth - width + window.pageXOffset - 1; - } - - if (top + height - window.pageYOffset > window.innerHeight) { - top = window.innerHeight - height + window.pageYOffset - 1; - } - } - - if (top < 0) { - top = 0; - } - - if (left < 0) { - left = 0; - } - - if (top > rect.top + (this.fixed ? 0 : window.pageYOffset)) { - this.transformOrigin = 'center top'; - } else { - this.transformOrigin = 'center'; - } - - popover.style.left = left + 'px'; - popover.style.top = top + 'px'; - }, - - childRendered() { - // モーダルコンテンツにマウスボタンが押され、コンテンツ外でマウスボタンが離されたときにモーダルバックグラウンドクリックと判定させないためにマウスイベントを監視しフラグ管理する - const content = this.$refs.content.children[0]; - content.addEventListener('mousedown', e => { - this.contentClicking = true; - window.addEventListener('mouseup', e => { - // click イベントより先に mouseup イベントが発生するかもしれないのでちょっと待つ - setTimeout(() => { - this.contentClicking = false; - }, 100); - }, { passive: true, once: true }); - }, { passive: true }); - }, - - close() { - this.showing = false; - this.$emit('close'); - }, - - onBgClick() { - if (this.contentClicking) return; - this.$emit('click'); - }, - - onClosed() { - this.$emit('closed'); - } - } -}); -</script> - -<style lang="scss"> -.modal-popup-enter-active, .modal-popup-leave-active, -.modal-enter-from, .modal-leave-to { - > .content { - transform-origin: var(--transformOrigin); - } -} -</style> - -<style lang="scss" scoped> -.modal-enter-active, .modal-leave-active { - > .bg { - transition: opacity 0.3s !important; - } - - > .content { - transition: opacity 0.3s, transform 0.3s !important; - } -} -.modal-enter-from, .modal-leave-to { - > .bg { - opacity: 0; - } - - > .content { - pointer-events: none; - opacity: 0; - transform: scale(0.9); - } -} - -.modal-popup-enter-active, .modal-popup-leave-active { - > .bg { - transition: opacity 0.3s !important; - } - - > .content { - transition: opacity 0.5s cubic-bezier(0.16, 1, 0.3, 1), transform 0.5s cubic-bezier(0.16, 1, 0.3, 1) !important; - } -} -.modal-popup-enter-from, .modal-popup-leave-to { - > .bg { - opacity: 0; - } - - > .content { - pointer-events: none; - opacity: 0; - transform: scale(0.9); - } -} - -.qzhlnise { - > .bg { - z-index: 10000; - } - - > .content:not(.popup) { - position: fixed; - z-index: 10000; - 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; - } - } - } - - > .content.popup { - position: absolute; - z-index: 10000; - - &.fixed { - position: fixed; - } - } - - &.front { - > .bg { - z-index: 20000; - } - - > .content:not(.popup) { - z-index: 20000; - } - - > .content.popup { - z-index: 20000; - } - } -} -</style> diff --git a/src/client/components/ui/pagination.vue b/src/client/components/ui/pagination.vue deleted file mode 100644 index 1bd77447b7..0000000000 --- a/src/client/components/ui/pagination.vue +++ /dev/null @@ -1,69 +0,0 @@ -<template> -<transition name="fade" mode="out-in"> - <MkLoading v-if="fetching"/> - - <MkError v-else-if="error" @retry="init()"/> - - <div class="empty" v-else-if="empty" key="_empty_"> - <slot name="empty"></slot> - </div> - - <div v-else class="cxiknjgy"> - <slot :items="items"></slot> - <div class="more _gap" v-show="more" key="_more_"> - <MkButton class="button" v-appear="($store.state.enableInfiniteScroll && !disableAutoLoad) ? fetchMore : null" @click="fetchMore" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }" primary> - <template v-if="!moreFetching">{{ $ts.loadMore }}</template> - <template v-if="moreFetching"><MkLoading inline/></template> - </MkButton> - </div> - </div> -</transition> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; -import MkButton from './button.vue'; -import paging from '@client/scripts/paging'; - -export default defineComponent({ - components: { - MkButton - }, - - mixins: [ - paging({}), - ], - - props: { - pagination: { - required: true - }, - - disableAutoLoad: { - type: Boolean, - required: false, - default: false, - } - }, -}); -</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 { - > .more > .button { - margin-left: auto; - margin-right: auto; - height: 48px; - min-width: 150px; - } -} -</style> diff --git a/src/client/components/ui/popup-menu.vue b/src/client/components/ui/popup-menu.vue deleted file mode 100644 index 3ff4c658b1..0000000000 --- a/src/client/components/ui/popup-menu.vue +++ /dev/null @@ -1,42 +0,0 @@ -<template> -<MkPopup ref="popup" :src="src" @closed="$emit('closed')"> - <MkMenu :items="items" :align="align" :width="width" @close="$refs.popup.close()" class="_popup _shadow"/> -</MkPopup> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; -import MkPopup from './popup.vue'; -import MkMenu from './menu.vue'; - -export default defineComponent({ - components: { - MkPopup, - MkMenu, - }, - - props: { - items: { - type: Array, - required: true - }, - align: { - type: String, - required: false - }, - width: { - type: Number, - required: false - }, - viaKeyboard: { - type: Boolean, - required: false - }, - src: { - required: false - }, - }, - - emits: ['close', 'closed'], -}); -</script> diff --git a/src/client/components/ui/popup.vue b/src/client/components/ui/popup.vue deleted file mode 100644 index 0fb1780cc5..0000000000 --- a/src/client/components/ui/popup.vue +++ /dev/null @@ -1,213 +0,0 @@ -<template> -<transition :name="$store.state.animation ? 'popup-menu' : ''" appear @after-leave="onClosed" @enter="$emit('opening')" @after-enter="childRendered"> - <div v-show="manualShowing != null ? manualShowing : showing" class="ccczpooj" :class="{ front, fixed, top: position === 'top' }" ref="content" :style="{ pointerEvents: (manualShowing != null ? manualShowing : showing) ? 'auto' : 'none', '--transformOrigin': transformOrigin }"> - <slot></slot> - </div> -</transition> -</template> - -<script lang="ts"> -import { defineComponent, PropType } from 'vue'; - -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); - } -} - -export default defineComponent({ - props: { - manualShowing: { - type: Boolean, - required: false, - default: null, - }, - srcCenter: { - type: Boolean, - required: false - }, - src: { - type: Object as PropType<HTMLElement>, - required: false, - }, - position: { - required: false - }, - front: { - type: Boolean, - required: false, - default: false, - } - }, - - emits: ['opening', 'click', 'esc', 'close', 'closed'], - - data() { - return { - showing: true, - fixed: false, - transformOrigin: 'center', - contentClicking: false, - }; - }, - - mounted() { - this.$watch('src', () => { - if (this.src) { - this.src.style.pointerEvents = 'none'; - } - this.fixed = getFixedContainer(this.src) != null; - this.$nextTick(() => { - this.align(); - }); - }, { immediate: true }); - - this.$nextTick(() => { - const popover = this.$refs.content as any; - new ResizeObserver((entries, observer) => { - this.align(); - }).observe(popover); - }); - - document.addEventListener('mousedown', this.onDocumentClick, { passive: true }); - }, - - beforeUnmount() { - document.removeEventListener('mousedown', this.onDocumentClick); - }, - - methods: { - align() { - if (this.src == null) return; - - const popover = this.$refs.content as any; - - if (popover == null) return; - - const rect = this.src.getBoundingClientRect(); - - const width = popover.offsetWidth; - const height = popover.offsetHeight; - - let left; - let top; - - if (this.srcCenter) { - const x = rect.left + (this.fixed ? 0 : window.pageXOffset) + (this.src.offsetWidth / 2); - const y = rect.top + (this.fixed ? 0 : window.pageYOffset) + (this.src.offsetHeight / 2); - left = (x - (width / 2)); - top = (y - (height / 2)); - } else { - const x = rect.left + (this.fixed ? 0 : window.pageXOffset) + (this.src.offsetWidth / 2); - const y = rect.top + (this.fixed ? 0 : window.pageYOffset) + this.src.offsetHeight; - left = (x - (width / 2)); - top = y; - } - - if (this.fixed) { - if (left + width > window.innerWidth) { - left = window.innerWidth - width; - } - - if (top + height > window.innerHeight) { - top = window.innerHeight - height; - } - } else { - if (left + width - window.pageXOffset > window.innerWidth) { - left = window.innerWidth - width + window.pageXOffset - 1; - } - - if (top + height - window.pageYOffset > window.innerHeight) { - top = window.innerHeight - height + window.pageYOffset - 1; - } - } - - if (top < 0) { - top = 0; - } - - if (left < 0) { - left = 0; - } - - if (top > rect.top + (this.fixed ? 0 : window.pageYOffset)) { - this.transformOrigin = 'center top'; - } else { - this.transformOrigin = 'center'; - } - - popover.style.left = left + 'px'; - popover.style.top = top + 'px'; - }, - - childRendered() { - // モーダルコンテンツにマウスボタンが押され、コンテンツ外でマウスボタンが離されたときにモーダルバックグラウンドクリックと判定させないためにマウスイベントを監視しフラグ管理する - const content = this.$refs.content.children[0]; - content.addEventListener('mousedown', e => { - this.contentClicking = true; - window.addEventListener('mouseup', e => { - // click イベントより先に mouseup イベントが発生するかもしれないのでちょっと待つ - setTimeout(() => { - this.contentClicking = false; - }, 100); - }, { passive: true, once: true }); - }, { passive: true }); - }, - - close() { - if (this.src) this.src.style.pointerEvents = 'auto'; - this.showing = false; - this.$emit('close'); - }, - - onClosed() { - this.$emit('closed'); - }, - - onDocumentClick(ev) { - const flyoutElement = this.$refs.content; - let targetElement = ev.target; - do { - if (targetElement === flyoutElement) { - return; - } - targetElement = targetElement.parentNode; - } while (targetElement); - this.close(); - } - } -}); -</script> - -<style lang="scss" scoped> -.popup-menu-enter-active { - 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; -} -.popup-menu-leave-active { - transform-origin: var(--transformOrigin); - transition: opacity 0.2s cubic-bezier(0.4, 0, 1, 1), transform 0.2s cubic-bezier(0.4, 0, 1, 1) !important; -} -.popup-menu-enter-from, .popup-menu-leave-to { - pointer-events: none; - opacity: 0; - transform: scale(0.9); -} - -.ccczpooj { - position: absolute; - z-index: 10000; - - &.fixed { - position: fixed; - } - - &.front { - z-index: 20000; - } -} -</style> diff --git a/src/client/components/ui/super-menu.vue b/src/client/components/ui/super-menu.vue deleted file mode 100644 index 195cc57326..0000000000 --- a/src/client/components/ui/super-menu.vue +++ /dev/null @@ -1,148 +0,0 @@ -<template> -<div class="rrevdjwu" :class="{ grid }"> - <div class="group" v-for="group in def"> - <div class="title" v-if="group.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'" @click="ev => item.action(ev)" :tabindex="i" class="_button item" :class="{ danger: item.danger, active: item.active }" :disabled="item.active"> - <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 { - font-size: 0.9em; - opacity: 0.7; - margin: 0 0 8px 12px; - } - - > .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/src/client/components/ui/tooltip.vue b/src/client/components/ui/tooltip.vue deleted file mode 100644 index c003895c14..0000000000 --- a/src/client/components/ui/tooltip.vue +++ /dev/null @@ -1,92 +0,0 @@ -<template> -<transition name="tooltip" appear @after-leave="$emit('closed')"> - <div class="buebdbiu _acrylic _shadow" v-show="showing" ref="content" :style="{ maxWidth: maxWidth + 'px' }"> - <slot>{{ text }}</slot> - </div> -</transition> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; - -export default defineComponent({ - props: { - showing: { - type: Boolean, - required: true, - }, - source: { - required: true, - }, - text: { - type: String, - required: false - }, - maxWidth: { - type: Number, - required: false, - default: 250, - }, - }, - - emits: ['closed'], - - mounted() { - this.$nextTick(() => { - if (this.source == null) { - this.$emit('closed'); - return; - } - - const rect = this.source.getBoundingClientRect(); - - const contentWidth = this.$refs.content.offsetWidth; - const contentHeight = this.$refs.content.offsetHeight; - - let left = rect.left + window.pageXOffset + (this.source.offsetWidth / 2); - let top = rect.top + window.pageYOffset - contentHeight; - - left -= (this.$el.offsetWidth / 2); - - if (left + contentWidth - window.pageXOffset > window.innerWidth) { - left = window.innerWidth - contentWidth + window.pageXOffset - 1; - } - - if (top - window.pageYOffset < 0) { - top = rect.top + window.pageYOffset + this.source.offsetHeight; - this.$refs.content.style.transformOrigin = 'center top'; - } - - this.$el.style.left = left + 'px'; - this.$el.style.top = top + 'px'; - }); - }, -}) -</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; - z-index: 11000; - 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 bottom; -} -</style> diff --git a/src/client/components/ui/window.vue b/src/client/components/ui/window.vue deleted file mode 100644 index 00284b0467..0000000000 --- a/src/client/components/ui/window.vue +++ /dev/null @@ -1,525 +0,0 @@ -<template> -<transition :name="$store.state.animation ? 'window' : ''" appear @after-leave="$emit('closed')"> - <div class="ebkgocck" :class="{ front }" v-if="showing"> - <div class="body _window _shadow _narrow_" @mousedown="onBodyMousedown" @keydown="onKeydown"> - <div class="header" :class="{ mini }" @contextmenu.prevent.stop="onContextmenu"> - <span class="left"> - <slot name="headerLeft"></slot> - </span> - <span class="title" @mousedown.prevent="onHeaderMousedown" @touchstart.prevent="onHeaderMousedown"> - <slot name="header"></slot> - </span> - <span class="right"> - <slot name="headerRight"></slot> - <button v-if="closeButton" class="_button" @click="close()"><i class="fas fa-times"></i></button> - </span> - </div> - <div class="body" v-if="padding"> - <div class="_section"> - <slot></slot> - </div> - </div> - <div class="body" v-else> - <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"> -import { defineComponent } from 'vue'; -import contains from '@client/scripts/contains'; -import * as os from '@client/os'; - -const minHeight = 50; -const minWidth = 250; - -function dragListen(fn) { - 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); -} - -export default defineComponent({ - provide: { - inWindow: true - }, - - props: { - padding: { - type: Boolean, - required: false, - default: false - }, - initialWidth: { - type: Number, - required: false, - default: 400 - }, - initialHeight: { - type: Number, - required: false, - default: null - }, - canResize: { - type: Boolean, - required: false, - default: false, - }, - closeButton: { - type: Boolean, - required: false, - default: true, - }, - mini: { - type: Boolean, - required: false, - default: false, - }, - front: { - type: Boolean, - required: false, - default: false, - }, - contextmenu: { - type: Array, - required: false, - } - }, - - emits: ['closed'], - - data() { - return { - showing: true, - id: Math.random().toString(), // TODO: UUIDとかにする - }; - }, - - mounted() { - if (this.initialWidth) this.applyTransformWidth(this.initialWidth); - if (this.initialHeight) this.applyTransformHeight(this.initialHeight); - - this.applyTransformTop((window.innerHeight / 2) - (this.$el.offsetHeight / 2)); - this.applyTransformLeft((window.innerWidth / 2) - (this.$el.offsetWidth / 2)); - - os.windows.set(this.id, { - z: Number(document.defaultView.getComputedStyle(this.$el, null).zIndex) - }); - - // 他のウィンドウ内のボタンなどを押してこのウィンドウが開かれた場合、親が最前面になろうとするのでそれに隠されないようにする - this.top(); - - window.addEventListener('resize', this.onBrowserResize); - }, - - unmounted() { - os.windows.delete(this.id); - window.removeEventListener('resize', this.onBrowserResize); - }, - - methods: { - close() { - this.showing = false; - }, - - onKeydown(e) { - if (e.which === 27) { // Esc - e.preventDefault(); - e.stopPropagation(); - this.close(); - } - }, - - onContextmenu(e) { - if (this.contextmenu) { - os.contextMenu(this.contextmenu, e); - } - }, - - // 最前面へ移動 - top() { - let z = 0; - const ws = Array.from(os.windows.entries()).filter(([k, v]) => k !== this.id).map(([k, v]) => v); - for (const w of ws) { - if (w.z > z) z = w.z; - } - if (z > 0) { - (this.$el as any).style.zIndex = z + 1; - os.windows.set(this.id, { - z: z + 1 - }); - } - }, - - onBodyMousedown() { - this.top(); - }, - - onHeaderMousedown(e) { - const main = this.$el as any; - - if (!contains(main, document.activeElement)) main.focus(); - - const position = main.getBoundingClientRect(); - - const clickX = e.touches && e.touches.length > 0 ? e.touches[0].clientX : e.clientX; - const clickY = e.touches && e.touches.length > 0 ? e.touches[0].clientY : e.clientY; - const moveBaseX = clickX - position.left; - const moveBaseY = clickY - position.top; - const browserWidth = window.innerWidth; - const browserHeight = window.innerHeight; - const windowWidth = main.offsetWidth; - const windowHeight = main.offsetHeight; - - // 動かした時 - 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; - - 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; - - this.$el.style.left = moveLeft + 'px'; - this.$el.style.top = moveTop + 'px'; - }); - }, - - // 上ハンドル掴み時 - onTopHandleMousedown(e) { - const main = this.$el as any; - - const base = e.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) { - this.applyTransformHeight(height + -move); - this.applyTransformTop(top + move); - } else { // 最小の高さより小さくなろうとした時 - this.applyTransformHeight(minHeight); - this.applyTransformTop(top + (height - minHeight)); - } - } else { // 上のはみ出し時 - this.applyTransformHeight(top + height); - this.applyTransformTop(0); - } - }); - }, - - // 右ハンドル掴み時 - onRightHandleMousedown(e) { - const main = this.$el as any; - - const base = e.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) { - this.applyTransformWidth(width + move); - } else { // 最小の幅より小さくなろうとした時 - this.applyTransformWidth(minWidth); - } - } else { // 右のはみ出し時 - this.applyTransformWidth(browserWidth - left); - } - }); - }, - - // 下ハンドル掴み時 - onBottomHandleMousedown(e) { - const main = this.$el as any; - - const base = e.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) { - this.applyTransformHeight(height + move); - } else { // 最小の高さより小さくなろうとした時 - this.applyTransformHeight(minHeight); - } - } else { // 下のはみ出し時 - this.applyTransformHeight(browserHeight - top); - } - }); - }, - - // 左ハンドル掴み時 - onLeftHandleMousedown(e) { - const main = this.$el as any; - - const base = e.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) { - this.applyTransformWidth(width + -move); - this.applyTransformLeft(left + move); - } else { // 最小の幅より小さくなろうとした時 - this.applyTransformWidth(minWidth); - this.applyTransformLeft(left + (width - minWidth)); - } - } else { // 左のはみ出し時 - this.applyTransformWidth(left + width); - this.applyTransformLeft(0); - } - }); - }, - - // 左上ハンドル掴み時 - onTopLeftHandleMousedown(e) { - this.onTopHandleMousedown(e); - this.onLeftHandleMousedown(e); - }, - - // 右上ハンドル掴み時 - onTopRightHandleMousedown(e) { - this.onTopHandleMousedown(e); - this.onRightHandleMousedown(e); - }, - - // 右下ハンドル掴み時 - onBottomRightHandleMousedown(e) { - this.onBottomHandleMousedown(e); - this.onRightHandleMousedown(e); - }, - - // 左下ハンドル掴み時 - onBottomLeftHandleMousedown(e) { - this.onBottomHandleMousedown(e); - this.onLeftHandleMousedown(e); - }, - - // 高さを適用 - applyTransformHeight(height) { - if (height > window.innerHeight) height = window.innerHeight; - (this.$el as any).style.height = height + 'px'; - }, - - // 幅を適用 - applyTransformWidth(width) { - if (width > window.innerWidth) width = window.innerWidth; - (this.$el as any).style.width = width + 'px'; - }, - - // Y座標を適用 - applyTransformTop(top) { - (this.$el as any).style.top = top + 'px'; - }, - - // X座標を適用 - applyTransformLeft(left) { - (this.$el as any).style.left = left + 'px'; - }, - - onBrowserResize() { - const main = this.$el as any; - 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; // 上はみ出し - } - } -}); -</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; - z-index: 10000; // mk-modalのと同じでなければならない - - &.front { - z-index: 11000; // front指定の時は、mk-modalのよりも大きくなければならない - } - - > .body { - overflow: hidden; - display: flex; - flex-direction: column; - contain: content; - width: 100%; - height: 100%; - - > .header { - --height: 50px; - - &.mini { - --height: 38px; - } - - display: flex; - position: relative; - z-index: 1; - flex-shrink: 0; - user-select: none; - height: var(--height); - border-bottom: solid 1px var(--divider); - - > .left, > .right { - > ::v-deep(button) { - height: var(--height); - width: var(--height); - - &:hover { - color: var(--fgHighlighted); - } - } - } - - > .title { - flex: 1; - position: relative; - line-height: var(--height); - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - text-align: center; - cursor: move; - } - } - - > .body { - flex: 1; - overflow: auto; - } - } - - > .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; - } - } -} -</style> |