sharkey/src/client/app/desktop/views/components/window.vue

649 lines
15 KiB
Vue
Raw Normal View History

2018-02-11 18:00:08 +09:00
<template>
2018-02-11 18:38:12 +09:00
<div class="mk-window" :data-flexible="isFlexible" @dragover="onDragover">
2018-02-11 18:00:08 +09:00
<div class="bg" ref="bg" v-show="isModal" @click="onBgClick"></div>
2018-02-11 18:50:30 +09:00
<div class="main" ref="main" tabindex="-1" :data-is-modal="isModal" @mousedown="onBodyMousedown" @keydown="onKeydown" :style="{ width, height }">
2018-02-11 18:00:08 +09:00
<div class="body">
2018-03-06 14:33:01 +09:00
<header ref="header"
2018-05-27 13:49:09 +09:00
:class="{ withGradient: $store.state.settings.gradientWindowHeader }"
2018-03-06 14:33:01 +09:00
@contextmenu.prevent="() => {}" @mousedown.prevent="onHeaderMousedown"
>
2018-02-12 00:17:51 +09:00
<h1><slot name="header"></slot></h1>
2018-02-11 18:00:08 +09:00
<div>
<button class="popout" v-if="popoutUrl" @mousedown.stop="() => {}" @click="popout" title="%i18n:@popout%">%fa:R window-restore%</button>
<button class="close" v-if="canClose" @mousedown.stop="() => {}" @click="close" title="%i18n:@close%">%fa:times%</button>
2018-02-11 18:00:08 +09:00
</div>
</header>
2018-02-16 17:01:36 +09:00
<div class="content">
<slot></slot>
</div>
2018-02-11 18:00:08 +09:00
</div>
2018-04-29 08:51:17 +09:00
<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>
2018-02-11 18:00:08 +09:00
</div>
</div>
</template>
2018-02-11 18:38:12 +09:00
<script lang="ts">
import Vue from 'vue';
2018-02-13 09:27:57 +09:00
import * as anime from 'animejs';
2018-02-11 23:26:35 +09:00
import contains from '../../../common/scripts/contains';
2018-02-11 18:38:12 +09:00
const minHeight = 40;
const minWidth = 200;
2018-02-11 22:04:08 +09:00
function dragListen(fn) {
window.addEventListener('mousemove', fn);
window.addEventListener('mouseleave', dragClear.bind(null, fn));
window.addEventListener('mouseup', dragClear.bind(null, fn));
}
function dragClear(fn) {
window.removeEventListener('mousemove', fn);
window.removeEventListener('mouseleave', dragClear);
window.removeEventListener('mouseup', dragClear);
}
2018-02-11 18:38:12 +09:00
export default Vue.extend({
props: {
isModal: {
type: Boolean,
default: false
},
canClose: {
type: Boolean,
default: true
},
2018-02-11 18:50:30 +09:00
width: {
type: String,
default: '530px'
},
2018-02-11 18:38:12 +09:00
height: {
2018-02-11 18:50:30 +09:00
type: String,
default: 'auto'
2018-02-11 18:38:12 +09:00
},
popoutUrl: {
2018-03-15 19:53:46 +09:00
type: [String, Function],
default: null
},
name: {
type: String,
default: null
2018-09-18 14:50:13 +09:00
},
animation: {
type: Boolean,
required: false,
default: true
2018-02-11 18:38:12 +09:00
}
},
2018-02-11 22:04:08 +09:00
2018-03-14 16:12:36 +09:00
data() {
return {
preventMount: false
};
},
2018-02-11 18:38:12 +09:00
computed: {
isFlexible(): boolean {
2018-04-29 08:51:17 +09:00
return this.height == 'auto';
2018-02-11 18:38:12 +09:00
},
canResize(): boolean {
return !this.isFlexible;
}
2018-02-11 18:50:30 +09:00
},
2018-02-11 22:04:08 +09:00
created() {
2018-05-27 13:49:09 +09:00
if (this.$store.state.device.autoPopout && this.popoutUrl) {
2018-03-14 16:12:36 +09:00
this.popout();
this.preventMount = true;
} else {
// ウィンドウをウィンドウシステムに登録
(this as any).os.windows.add(this);
}
2018-02-11 22:04:08 +09:00
},
2018-02-11 18:50:30 +09:00
mounted() {
2018-03-14 16:12:36 +09:00
if (this.preventMount) {
2018-09-15 21:53:04 +09:00
this.destroyDom();
2018-03-14 16:12:36 +09:00
return;
}
2018-02-18 18:40:24 +09:00
this.$nextTick(() => {
2018-02-14 19:33:39 +09:00
const main = this.$refs.main as any;
main.style.top = '15%';
main.style.left = (window.innerWidth / 2) - (main.offsetWidth / 2) + 'px';
2018-02-11 18:50:30 +09:00
2018-02-14 19:33:39 +09:00
window.addEventListener('resize', this.onBrowserResize);
2018-02-11 18:38:12 +09:00
2018-02-14 19:33:39 +09:00
this.open();
});
2018-02-11 22:04:08 +09:00
},
2018-02-11 18:38:12 +09:00
2018-02-11 22:04:08 +09:00
destroyed() {
// ウィンドウをウィンドウシステムから削除
2018-02-18 12:35:18 +09:00
(this as any).os.windows.remove(this);
2018-02-11 18:38:12 +09:00
2018-02-11 22:04:08 +09:00
window.removeEventListener('resize', this.onBrowserResize);
},
2018-02-11 18:38:12 +09:00
2018-02-11 22:04:08 +09:00
methods: {
open() {
this.$emit('opening');
2018-02-11 18:38:12 +09:00
2018-02-11 22:04:08 +09:00
this.top();
2018-02-11 18:38:12 +09:00
2018-02-11 22:04:08 +09:00
const bg = this.$refs.bg as any;
const main = this.$refs.main as any;
2018-02-11 18:38:12 +09:00
2018-02-11 22:04:08 +09:00
if (this.isModal) {
bg.style.pointerEvents = 'auto';
anime({
targets: bg,
opacity: 1,
2018-09-18 14:50:13 +09:00
duration: this.animation ? 100 : 0,
2018-02-11 22:04:08 +09:00
easing: 'linear'
});
}
2018-02-11 18:38:12 +09:00
2018-02-11 22:04:08 +09:00
main.style.pointerEvents = 'auto';
anime({
targets: main,
opacity: 1,
scale: [1.1, 1],
2018-09-18 14:50:13 +09:00
duration: this.animation ? 200 : 0,
2018-02-11 22:04:08 +09:00
easing: 'easeOutQuad'
});
2018-02-11 18:38:12 +09:00
2018-02-11 22:04:08 +09:00
if (focus) main.focus();
2018-02-11 18:38:12 +09:00
2018-02-11 22:04:08 +09:00
setTimeout(() => {
this.$emit('opened');
2018-09-18 14:50:13 +09:00
}, this.animation ? 300 : 0);
2018-02-11 22:04:08 +09:00
},
2018-02-11 18:38:12 +09:00
2018-02-11 22:04:08 +09:00
close() {
2018-02-13 19:00:54 +09:00
this.$emit('before-close');
2018-02-11 22:04:08 +09:00
const bg = this.$refs.bg as any;
const main = this.$refs.main as any;
if (this.isModal) {
bg.style.pointerEvents = 'none';
anime({
targets: bg,
opacity: 0,
2018-09-18 14:50:13 +09:00
duration: this.animation ? 300 : 0,
2018-02-11 22:04:08 +09:00
easing: 'linear'
});
2018-02-11 18:38:12 +09:00
}
2018-02-11 22:04:08 +09:00
main.style.pointerEvents = 'none';
2018-02-11 18:38:12 +09:00
2018-02-11 22:04:08 +09:00
anime({
targets: main,
opacity: 0,
scale: 0.8,
2018-09-18 14:50:13 +09:00
duration: this.animation ? 300 : 0,
2018-02-11 22:04:08 +09:00
easing: [0.5, -0.5, 1, 0.5]
});
2018-02-11 18:38:12 +09:00
2018-02-11 22:04:08 +09:00
setTimeout(() => {
this.$emit('closed');
this.destroyDom();
2018-09-18 14:50:13 +09:00
}, this.animation ? 300 : 0);
2018-02-11 22:04:08 +09:00
},
popout() {
2018-03-14 16:12:36 +09:00
const url = typeof this.popoutUrl == 'function' ? this.popoutUrl() : this.popoutUrl;
2018-02-11 22:04:08 +09:00
const main = this.$refs.main as any;
2018-03-14 16:12:36 +09:00
if (main) {
const position = main.getBoundingClientRect();
2018-02-11 22:04:08 +09:00
2018-03-14 16:12:36 +09:00
const width = parseInt(getComputedStyle(main, '').width, 10);
const height = parseInt(getComputedStyle(main, '').height, 10);
const x = window.screenX + position.left;
const y = window.screenY + position.top;
2018-02-11 22:04:08 +09:00
2018-03-14 16:12:36 +09:00
window.open(url, url,
`width=${width}, height=${height}, top=${y}, left=${x}`);
2018-02-11 22:04:08 +09:00
2018-03-14 16:12:36 +09:00
this.close();
} else {
const x = window.top.outerHeight / 2 + window.top.screenY - (parseInt(this.height, 10) / 2);
const y = window.top.outerWidth / 2 + window.top.screenX - (parseInt(this.width, 10) / 2);
window.open(url, url,
`width=${this.width}, height=${this.height}, top=${x}, left=${y}`);
}
2018-02-11 22:04:08 +09:00
},
// 最前面へ移動
top() {
let z = 0;
2018-02-18 12:35:18 +09:00
(this as any).os.windows.getAll().forEach(w => {
2018-02-11 22:04:08 +09:00
if (w == this) return;
const m = w.$refs.main;
const mz = Number(document.defaultView.getComputedStyle(m, null).zIndex);
if (mz > z) z = mz;
});
if (z > 0) {
(this.$refs.main as any).style.zIndex = z + 1;
if (this.isModal) (this.$refs.bg as any).style.zIndex = z + 1;
}
},
onBgClick() {
if (this.canClose) this.close();
},
onBodyMousedown() {
this.top();
},
onHeaderMousedown(e) {
const main = this.$refs.main as any;
if (!contains(main, document.activeElement)) main.focus();
const position = main.getBoundingClientRect();
const clickX = e.clientX;
const clickY = 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 => {
let moveLeft = me.clientX - moveBaseX;
let moveTop = me.clientY - moveBaseY;
// 上はみ出し
if (moveTop < 0) moveTop = 0;
// 左はみ出し
if (moveLeft < 0) moveLeft = 0;
// 下はみ出し
if (moveTop + windowHeight > browserHeight) moveTop = browserHeight - windowHeight;
// 右はみ出し
if (moveLeft + windowWidth > browserWidth) moveLeft = browserWidth - windowWidth;
main.style.left = moveLeft + 'px';
main.style.top = moveTop + 'px';
});
},
// 上ハンドル掴み時
onTopHandleMousedown(e) {
const main = this.$refs.main 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.$refs.main 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.$refs.main 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.$refs.main 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) {
(this.$refs.main as any).style.height = height + 'px';
},
// 幅を適用
applyTransformWidth(width) {
(this.$refs.main as any).style.width = width + 'px';
},
// Y座標を適用
applyTransformTop(top) {
(this.$refs.main as any).style.top = top + 'px';
},
// X座標を適用
applyTransformLeft(left) {
(this.$refs.main as any).style.left = left + 'px';
},
onDragover(e) {
e.dataTransfer.dropEffect = 'none';
},
onKeydown(e) {
if (e.which == 27) { // Esc
if (this.canClose) {
e.preventDefault();
e.stopPropagation();
this.close();
}
}
},
onBrowserResize() {
const main = this.$refs.main 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 < 0) main.style.top = 0;
if (position.left + windowWidth > browserWidth) main.style.left = browserWidth - windowWidth + 'px';
if (position.top + windowHeight > browserHeight) main.style.top = browserHeight - windowHeight + 'px';
2018-02-11 18:38:12 +09:00
}
}
2018-02-11 22:04:08 +09:00
});
2018-02-11 18:38:12 +09:00
</script>
<style lang="stylus" scoped>
2018-09-26 20:19:35 +09:00
2018-03-03 13:47:55 +09:00
2018-04-20 03:41:24 +09:00
root(isDark)
2018-02-11 18:38:12 +09:00
display block
> .bg
display block
position fixed
2018-03-03 09:49:47 +09:00
z-index 2000
2018-02-11 18:38:12 +09:00
top 0
left 0
width 100%
height 100%
2018-04-29 08:51:17 +09:00
background rgba(#000, 0.7)
2018-02-11 18:38:12 +09:00
opacity 0
pointer-events none
> .main
display block
position fixed
2018-03-03 09:49:47 +09:00
z-index 2000
2018-02-11 18:38:12 +09:00
top 15%
left 0
margin 0
opacity 0
pointer-events none
&:focus
&:not([data-is-modal])
> .body
2018-05-27 18:18:18 +09:00
if isDark
2018-09-26 20:19:35 +09:00
box-shadow 0 0 0px 1px var(--primaryAlpha05), 0 2px 12px 0 rgba(#000, 0.5)
2018-05-27 18:18:18 +09:00
else
2018-09-26 20:19:35 +09:00
box-shadow 0 0 0px 1px var(--primaryAlpha05), 0 2px 6px 0 rgba(#000, 0.2)
2018-02-11 18:38:12 +09:00
> .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
> .body
height 100%
overflow hidden
2018-04-20 03:41:24 +09:00
background isDark ? #282C37 : #fff
2018-02-11 18:38:12 +09:00
border-radius 6px
2018-05-27 18:18:18 +09:00
if isDark
box-shadow 0 2px 12px 0 rgba(#000, 0.5)
else
box-shadow 0 2px 6px 0 rgba(#000, 0.2)
2018-02-11 18:38:12 +09:00
> header
$header-height = 40px
2018-03-03 09:49:47 +09:00
z-index 1001
2018-02-11 18:38:12 +09:00
height $header-height
overflow hidden
white-space nowrap
cursor move
2018-04-20 03:41:24 +09:00
background isDark ? #313543 : #fff
2018-02-11 18:38:12 +09:00
border-radius 6px 6px 0 0
box-shadow 0 1px 0 rgba(#000, 0.1)
2018-03-06 14:33:01 +09:00
&.withGradient
2018-04-20 03:41:24 +09:00
background isDark ? linear-gradient(to bottom, #313543, #1d2027) : linear-gradient(to bottom, #fff, #ececec)
2018-03-06 14:33:01 +09:00
box-shadow 0 1px 0 rgba(#000, 0.15)
2018-02-11 18:38:12 +09:00
&, *
user-select none
> h1
pointer-events none
display block
margin 0 auto
overflow hidden
height $header-height
text-overflow ellipsis
text-align center
font-size 1em
line-height $header-height
font-weight normal
2018-04-20 03:41:24 +09:00
color isDark ? #e3e5e8 : #666
2018-02-11 18:38:12 +09:00
> div:last-child
position absolute
top 0
right 0
display block
z-index 1
> *
display inline-block
margin 0
padding 0
cursor pointer
2018-02-20 23:37:35 +09:00
font-size 1em
2018-04-20 03:41:24 +09:00
color isDark ? #9baec8 : rgba(#000, 0.4)
2018-02-11 18:38:12 +09:00
border none
outline none
background transparent
&:hover
2018-04-20 03:41:24 +09:00
color isDark ? #b2c1d5 : rgba(#000, 0.6)
2018-02-11 18:38:12 +09:00
&:active
2018-04-20 03:41:24 +09:00
color isDark ? #b2c1d5 : darken(#000, 30%)
2018-02-11 18:38:12 +09:00
> [data-fa]
padding 0
width $header-height
line-height $header-height
text-align center
> .content
height 100%
&:not([flexible])
> .main > .body > .content
height calc(100% - 40px)
2018-04-20 03:41:24 +09:00
.mk-window[data-darkmode]
root(true)
.mk-window:not([data-darkmode])
root(false)
2018-02-11 18:38:12 +09:00
</style>