mirror of
https://github.com/lana-k/sqliteviz.git
synced 2026-11-04 16:59:24 +08:00
change repo structure
This commit is contained in:
@@ -0,0 +1,115 @@
|
||||
<template>
|
||||
<div
|
||||
:class="[
|
||||
'checkbox-container',
|
||||
{ checked: checked },
|
||||
{ disabled: disabled }
|
||||
]"
|
||||
@click.stop="onClick"
|
||||
>
|
||||
<div v-show="!checked" class="unchecked" />
|
||||
<img
|
||||
v-show="checked && !disabled && theme === 'light'"
|
||||
class="checked-light"
|
||||
src="~@/assets/images/checkbox_checked_light.svg"
|
||||
/>
|
||||
<img
|
||||
v-show="checked && !disabled && theme !== 'light'"
|
||||
class="checked"
|
||||
src="~@/assets/images/checkbox_checked.svg"
|
||||
/>
|
||||
<img
|
||||
v-show="checked && disabled"
|
||||
class="checked-disabled"
|
||||
src="~@/assets/images/checkbox_checked_disabled.svg"
|
||||
/>
|
||||
<span v-if="label" class="label">{{ label }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'CheckBox',
|
||||
props: {
|
||||
theme: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: 'accent',
|
||||
validator: value => {
|
||||
return ['accent', 'light'].includes(value)
|
||||
}
|
||||
},
|
||||
init: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: ''
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
emits: ['click'],
|
||||
data() {
|
||||
return {
|
||||
checked: this.init
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onClick() {
|
||||
if (!this.disabled) {
|
||||
this.checked = !this.checked
|
||||
this.$emit('click', this.checked)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.checkbox-container {
|
||||
display: inline-flex;
|
||||
cursor: pointer;
|
||||
}
|
||||
.unchecked {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
background-color: white;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--border-radius-medium);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.unchecked:hover {
|
||||
background-color: var(--color-bg-light);
|
||||
}
|
||||
|
||||
img {
|
||||
display: block;
|
||||
}
|
||||
.label {
|
||||
margin-left: 6px;
|
||||
color: var(--color-text-base);
|
||||
}
|
||||
.checked .label {
|
||||
color: var(--color-text-active);
|
||||
}
|
||||
|
||||
.disabled.checkbox-container {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.disabled .label {
|
||||
color: var(--color-text-light-2);
|
||||
}
|
||||
|
||||
.disabled .unchecked,
|
||||
.disabled .unchecked:hover {
|
||||
background-color: var(--color-bg-light-2);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,124 @@
|
||||
<template>
|
||||
<button
|
||||
:class="['icon-btn', { active }]"
|
||||
:disabled="disabled"
|
||||
@click="onClick"
|
||||
@mouseenter="showTooltip($event, tooltipPosition)"
|
||||
@mouseleave="hideTooltip"
|
||||
>
|
||||
<div class="icon"><slot /></div>
|
||||
<div v-show="loading" class="icon-in-progress">
|
||||
<loading-indicator />
|
||||
</div>
|
||||
<span
|
||||
v-if="tooltip"
|
||||
ref="tooltip"
|
||||
class="icon-tooltip"
|
||||
:style="tooltipStyle"
|
||||
>
|
||||
{{ tooltip }}
|
||||
</span>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import tooltipMixin from '@/tooltipMixin'
|
||||
import LoadingIndicator from '@/components/Common/LoadingIndicator'
|
||||
|
||||
export default {
|
||||
name: 'SideBarButton',
|
||||
components: { LoadingIndicator },
|
||||
mixins: [tooltipMixin],
|
||||
props: {
|
||||
active: Boolean,
|
||||
disabled: Boolean,
|
||||
tooltip: String,
|
||||
tooltipPosition: String,
|
||||
loading: Boolean
|
||||
},
|
||||
emits: ['click'],
|
||||
methods: {
|
||||
onClick() {
|
||||
this.hideTooltip()
|
||||
this.$emit('click')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.icon-btn {
|
||||
box-sizing: border-box;
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
}
|
||||
.icon-btn:hover {
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--border-radius-medium-2);
|
||||
}
|
||||
|
||||
.icon-btn:hover .icon :deep(path),
|
||||
.icon-btn.active .icon :deep(path),
|
||||
.icon-btn:hover .icon :deep(circle),
|
||||
.icon-btn.active .icon :deep(circle) {
|
||||
fill: var(--color-accent);
|
||||
}
|
||||
|
||||
.icon-btn:disabled .icon :deep(path),
|
||||
.icon-btn:disabled .icon :deep(circle) {
|
||||
fill: var(--color-border);
|
||||
}
|
||||
|
||||
.icon-btn:disabled {
|
||||
cursor: default;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.disabled.icon-btn:hover .icon :deep(path) {
|
||||
fill: var(--color-border);
|
||||
}
|
||||
|
||||
.icon-in-progress {
|
||||
position: absolute;
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: var(--color-bg-light);
|
||||
will-change: opacity;
|
||||
/*
|
||||
We need to show loader in 1 sec after starting query execution. We can't do that with
|
||||
setTimeout because the main thread can be busy by getting a result set from the web worker.
|
||||
But we can use CSS animation for opacity. Opacity triggers changes only in the Composite Layer
|
||||
stage in rendering waterfall. Hence it can be processed only with Compositor Thread while
|
||||
the Main Thread processes a result set.
|
||||
https://www.viget.com/articles/animation-performance-101-browser-under-the-hood/
|
||||
*/
|
||||
animation: show-loader 1s linear 0s 1;
|
||||
}
|
||||
|
||||
@keyframes show-loader {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
99% {
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,125 @@
|
||||
<template>
|
||||
<modal
|
||||
v-model="show"
|
||||
class="dialog"
|
||||
:clickToClose="false"
|
||||
:contentTransition="{ name: 'loading-dialog' }"
|
||||
:overlayTransition="{ name: 'loading-dialog' }"
|
||||
@update:model-value="$emit('update:modelValue', $event)"
|
||||
>
|
||||
<div class="dialog-header">
|
||||
{{ title }}
|
||||
<close-icon :disabled="loading" @click="cancel" />
|
||||
</div>
|
||||
<div class="dialog-body">
|
||||
<div v-if="loading" class="loading-dialog-body">
|
||||
<loading-indicator :size="30" class="state-icon" />
|
||||
{{ loadingMsg }}
|
||||
</div>
|
||||
<div v-else class="loading-dialog-body">
|
||||
<img
|
||||
src="~@/assets/images/success.svg"
|
||||
class="success-icon state-icon"
|
||||
/>
|
||||
{{ successMsg }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="dialog-buttons-container">
|
||||
<button
|
||||
class="secondary"
|
||||
type="button"
|
||||
:disabled="loading"
|
||||
@click="cancel"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
class="primary"
|
||||
type="button"
|
||||
:disabled="loading"
|
||||
@click="$emit('action')"
|
||||
>
|
||||
{{ actionBtnName }}
|
||||
</button>
|
||||
</div>
|
||||
</modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import LoadingIndicator from '@/components/Common/LoadingIndicator'
|
||||
import CloseIcon from '@/components/svg/close'
|
||||
|
||||
export default {
|
||||
name: 'LoadingDialog',
|
||||
components: { LoadingIndicator, CloseIcon },
|
||||
props: {
|
||||
modelValue: Boolean,
|
||||
loadingMsg: String,
|
||||
successMsg: String,
|
||||
actionBtnName: String,
|
||||
title: String,
|
||||
loading: Boolean
|
||||
},
|
||||
emits: ['cancel', 'action', 'update:modelValue'],
|
||||
data() {
|
||||
return {
|
||||
show: this.modelValue
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
modelValue() {
|
||||
this.show = this.modelValue
|
||||
},
|
||||
loading() {
|
||||
if (this.loading) {
|
||||
this.$emit('update:modelValue', true)
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
cancel() {
|
||||
this.$emit('cancel')
|
||||
this.$emit('update:modelValue', false)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.loading-dialog-enter-active {
|
||||
animation: show-modal 1s linear 0s 1;
|
||||
}
|
||||
.loading-dialog-leave-active {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
@keyframes show-modal {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
99% {
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.loading-modal {
|
||||
width: 400px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style scoped>
|
||||
.loading-dialog-body {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.success-icon {
|
||||
width: 30px;
|
||||
}
|
||||
|
||||
.state-icon {
|
||||
margin-right: 8px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,129 @@
|
||||
<template>
|
||||
<svg
|
||||
:class="animationClass"
|
||||
:height="size"
|
||||
:width="size"
|
||||
:viewBox="`0 0 ${size} ${size}`"
|
||||
>
|
||||
<circle
|
||||
class="loader-svg bg"
|
||||
:style="{ strokeWidth }"
|
||||
:cx="size / 2"
|
||||
:cy="size / 2"
|
||||
:r="radius"
|
||||
/>
|
||||
<circle
|
||||
class="loader-svg front"
|
||||
:style="{
|
||||
strokeDasharray: circleProgress,
|
||||
strokeDashoffset: offset,
|
||||
strokeWidth
|
||||
}"
|
||||
:cx="size / 2"
|
||||
:cy="size / 2"
|
||||
:r="radius"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'LoadingIndicator',
|
||||
props: {
|
||||
progress: {
|
||||
type: Number,
|
||||
required: false
|
||||
},
|
||||
size: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: 20
|
||||
}
|
||||
},
|
||||
emits: [],
|
||||
computed: {
|
||||
circleProgress() {
|
||||
const circle = this.radius * 3.14 * 2
|
||||
const dash = this.progress
|
||||
? (circle * this.progress) / 100
|
||||
: (circle * 1) / 3
|
||||
const space = circle - dash
|
||||
return `${dash}px, ${space}px`
|
||||
},
|
||||
animationClass() {
|
||||
return this.progress === undefined ? 'loading' : 'progress'
|
||||
},
|
||||
radius() {
|
||||
return this.size / 2 - this.strokeWidth
|
||||
},
|
||||
offset() {
|
||||
return (this.radius * 3.14) / 2
|
||||
},
|
||||
strokeWidth() {
|
||||
return this.size / 10
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.loader-svg {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
fill: none;
|
||||
stroke-linecap: round;
|
||||
stroke: var(--color-accent);
|
||||
}
|
||||
|
||||
.loader-svg.bg {
|
||||
stroke: var(--color-gray-light-3);
|
||||
}
|
||||
|
||||
.loading .loader-svg.front {
|
||||
will-change: transform;
|
||||
animation: fill-animation-loading 1s cubic-bezier(1, 1, 1, 1) 0s infinite;
|
||||
transform-origin: center;
|
||||
}
|
||||
|
||||
/*
|
||||
We can't change anything in loading animation except transform, opacity and filter. Because in
|
||||
our case the Main Thread can be busy and animation will be frozen (e. g. getting a result set
|
||||
from the web-worker after query execution).
|
||||
But transform, opacity and filter trigger changes only in the Composite Layer stage in rendering
|
||||
waterfall. Hence they can be processed only with Compositor Thread while the Main Thread
|
||||
processes something else.
|
||||
https://www.viget.com/articles/animation-performance-101-browser-under-the-hood/
|
||||
*/
|
||||
@keyframes fill-animation-loading {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.progress .loader-svg.front {
|
||||
transition: stroke-dasharray 0.2s;
|
||||
}
|
||||
|
||||
.progress .loader-svg.bg {
|
||||
animation: bg-animation 1.5s cubic-bezier(1, 1, 1, 1) 0s infinite;
|
||||
}
|
||||
|
||||
@keyframes bg-animation {
|
||||
0% {
|
||||
r: 8;
|
||||
}
|
||||
50% {
|
||||
stroke: var(--color-gray-light-2);
|
||||
r: 9;
|
||||
}
|
||||
100% {
|
||||
r: 8;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,94 @@
|
||||
<template>
|
||||
<div ref="logsContainer" class="logs-container">
|
||||
<div v-for="(msg, index) in messages" :key="index" class="msg">
|
||||
<img v-if="msg.type === 'error'" src="~@/assets/images/error.svg" />
|
||||
<img
|
||||
v-if="msg.type === 'info'"
|
||||
src="~@/assets/images/info.svg"
|
||||
width="20px"
|
||||
/>
|
||||
<img v-if="msg.type === 'success'" src="~@/assets/images/success.svg" />
|
||||
<loading-indicator
|
||||
v-if="msg.type === 'loading'"
|
||||
:progress="msg.progress"
|
||||
/>
|
||||
<span class="msg-text">{{ serializeMessage(msg) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import LoadingIndicator from '@/components/Common/LoadingIndicator'
|
||||
|
||||
export default {
|
||||
name: 'Logs',
|
||||
components: { LoadingIndicator },
|
||||
props: { messages: Array },
|
||||
emits: [],
|
||||
watch: {
|
||||
'messages.length': 'scrollToBottom'
|
||||
},
|
||||
mounted() {
|
||||
this.scrollToBottom()
|
||||
},
|
||||
methods: {
|
||||
async scrollToBottom() {
|
||||
const container = this.$refs.logsContainer
|
||||
if (container) {
|
||||
await this.$nextTick()
|
||||
container.scrollTop = container.scrollHeight
|
||||
}
|
||||
},
|
||||
|
||||
serializeMessage(msg) {
|
||||
let result = ''
|
||||
if (msg.row !== null && msg.row !== undefined) {
|
||||
if (msg.type === 'error') {
|
||||
result += `Error in row ${msg.row}. `
|
||||
} else {
|
||||
result += `Information about row ${msg.row}. `
|
||||
}
|
||||
}
|
||||
|
||||
result += msg.message
|
||||
if (!/(\.|!|\?)$/.test(result)) {
|
||||
result += '.'
|
||||
}
|
||||
|
||||
if (msg.hint) {
|
||||
result += ` ${msg.hint}`
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.logs-container {
|
||||
background-color: var(--color-white);
|
||||
padding: 0 5px;
|
||||
border-radius: 5px;
|
||||
border: 1px solid var(--color-border-light);
|
||||
box-sizing: border-box;
|
||||
overflow-y: auto;
|
||||
color: var(--color-text-base);
|
||||
}
|
||||
.msg {
|
||||
padding: 12px 7px;
|
||||
border-bottom: 1px solid var(--color-border-light);
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
font-family: monospace;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.msg:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.msg-text {
|
||||
margin-left: 6px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,109 @@
|
||||
<template>
|
||||
<paginate
|
||||
v-model="page"
|
||||
:pageCount="pageCount"
|
||||
:pageRange="5"
|
||||
:marginPages="1"
|
||||
:prevText="chevron"
|
||||
:nextText="chevron"
|
||||
:noLiSurround="true"
|
||||
containerClass="paginator-continer"
|
||||
pageLinkClass="paginator-page-link"
|
||||
activeClass="paginator-active-page"
|
||||
breakViewLinkClass="paginator-break"
|
||||
nextLinkClass="paginator-next"
|
||||
prevLinkClass="paginator-prev"
|
||||
disabledClass="paginator-disabled"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Paginate from 'vuejs-paginate-next'
|
||||
|
||||
export default {
|
||||
name: 'Pager',
|
||||
components: { Paginate },
|
||||
props: {
|
||||
pageCount: Number,
|
||||
modelValue: Number
|
||||
},
|
||||
emits: ['update:modelValue'],
|
||||
data() {
|
||||
return {
|
||||
page: this.modelValue,
|
||||
chevron: `
|
||||
<svg width="9" height="9" viewBox="0 0 8 12" fill="none">
|
||||
<path
|
||||
d="M0.721924 9.93097L4.85292 5.79997L0.721924 1.66897L1.99992 0.399973L7.39992
|
||||
5.79997L1.99992 11.2L0.721924 9.93097Z" fill="#506784"
|
||||
/>
|
||||
</svg>
|
||||
`
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
page() {
|
||||
this.$emit('update:modelValue', this.page)
|
||||
},
|
||||
modelValue() {
|
||||
this.page = this.modelValue
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.paginator-continer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
line-height: 10px;
|
||||
}
|
||||
:deep(a) {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
:deep(.paginator-page-link) {
|
||||
padding: 2px 3px;
|
||||
margin: 0 5px;
|
||||
display: block;
|
||||
color: var(--color-text-base);
|
||||
font-size: 11px;
|
||||
}
|
||||
:deep(.paginator-page-link:hover) {
|
||||
color: var(--color-text-active);
|
||||
}
|
||||
:deep(.paginator-page-link:active),
|
||||
:deep(.paginator-page-link:visited),
|
||||
:deep(.paginator-page-link:focus),
|
||||
:deep(.paginator-next:active),
|
||||
:deep(.paginator-next:visited),
|
||||
:deep(.paginator-next:focus),
|
||||
:deep(.paginator-prev:active),
|
||||
:deep(.paginator-prev:visited),
|
||||
:deep(.paginator-prev:focus) {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
:deep(.paginator-active-page),
|
||||
:deep(.paginator-active-page:hover) {
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
:deep(.paginator-break:hover),
|
||||
:deep(.paginator-disabled:hover) {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
:deep(.paginator-prev svg) {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
:deep(.paginator-next:hover path),
|
||||
:deep(.paginator-prev:hover path) {
|
||||
fill: var(--color-text-active);
|
||||
}
|
||||
:deep(.paginator-disabled path),
|
||||
:deep(.paginator-disabled:hover path) {
|
||||
fill: var(--color-text-light-2);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,394 @@
|
||||
<template>
|
||||
<div
|
||||
ref="container"
|
||||
:class="[
|
||||
'splitpanes',
|
||||
`splitpanes-${horizontal ? 'horizontal' : 'vertical'}`,
|
||||
{ 'splitpanes-dragging': dragging }
|
||||
]"
|
||||
>
|
||||
<div
|
||||
ref="movableSplitter"
|
||||
class="movable-splitter"
|
||||
:style="movableSplitterStyle"
|
||||
/>
|
||||
<div
|
||||
ref="left"
|
||||
class="splitpanes-pane"
|
||||
:size="paneBefore.size"
|
||||
max-size="30"
|
||||
:style="styles.before"
|
||||
>
|
||||
<slot name="left-pane" />
|
||||
</div>
|
||||
<!-- Splitter start-->
|
||||
<div
|
||||
class="splitpanes-splitter"
|
||||
@mousedown="bindEvents"
|
||||
@touchstart="bindEvents"
|
||||
>
|
||||
<div
|
||||
:class="[
|
||||
'toggle-btns',
|
||||
{
|
||||
both:
|
||||
after.max === 100 &&
|
||||
before.max === 100 &&
|
||||
paneAfter.size > 0 &&
|
||||
paneBefore.size > 0
|
||||
}
|
||||
]"
|
||||
>
|
||||
<div
|
||||
v-if="after.max === 100 && paneAfter.size > 0"
|
||||
class="toggle-btn"
|
||||
@click="togglePane(paneBefore)"
|
||||
>
|
||||
<img
|
||||
class="direction-icon"
|
||||
src="~@/assets/images/chevron.svg"
|
||||
:style="directionBeforeIconStyle"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="before.max === 100 && paneBefore.size > 0"
|
||||
class="toggle-btn"
|
||||
@click="togglePane(paneAfter)"
|
||||
>
|
||||
<img
|
||||
class="direction-icon"
|
||||
src="~@/assets/images/chevron.svg"
|
||||
:style="directionAfterIconStyle"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- splitter end -->
|
||||
<div ref="right" class="splitpanes-pane" :style="styles.after">
|
||||
<slot name="right-pane" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import splitter from './splitter'
|
||||
|
||||
export default {
|
||||
name: 'Splitpanes',
|
||||
props: {
|
||||
horizontal: { type: Boolean, default: false },
|
||||
before: { type: Object },
|
||||
after: { type: Object },
|
||||
default: {
|
||||
type: Object,
|
||||
default: () => {
|
||||
return {
|
||||
before: 50,
|
||||
after: 50
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
emits: [],
|
||||
data() {
|
||||
return {
|
||||
container: null,
|
||||
paneBefore: this.before,
|
||||
paneAfter: this.after,
|
||||
beforeMinimising:
|
||||
!this.after.size || !this.before.size
|
||||
? this.default
|
||||
: {
|
||||
before: this.before.size,
|
||||
after: this.after.size
|
||||
},
|
||||
dragging: false,
|
||||
movableSplitter: {
|
||||
top: 0,
|
||||
left: 0,
|
||||
visibility: 'hidden'
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
styles() {
|
||||
return {
|
||||
before: {
|
||||
[this.horizontal ? 'height' : 'width']: `${this.paneBefore.size}%`
|
||||
},
|
||||
after: {
|
||||
[this.horizontal ? 'height' : 'width']: `${this.paneAfter.size}%`
|
||||
}
|
||||
}
|
||||
},
|
||||
movableSplitterStyle() {
|
||||
const style = { ...this.movableSplitter }
|
||||
style.top += '%'
|
||||
style.left += '%'
|
||||
return style
|
||||
},
|
||||
directionBeforeIconStyle() {
|
||||
const expanded = this.paneBefore.size !== 0
|
||||
const translation = 'translate(-50%, -50%) '
|
||||
let rotation = ''
|
||||
|
||||
if (this.horizontal) {
|
||||
rotation = expanded ? 'rotate(90deg)' : 'rotate(-90deg)'
|
||||
} else {
|
||||
rotation = expanded ? 'rotate(0deg)' : 'rotate(180deg)'
|
||||
}
|
||||
|
||||
return {
|
||||
transform: translation + rotation
|
||||
}
|
||||
},
|
||||
directionAfterIconStyle() {
|
||||
const expanded = this.paneAfter.size !== 0
|
||||
const translation = 'translate(-50%, -50%)'
|
||||
let rotation = ''
|
||||
|
||||
if (this.horizontal) {
|
||||
rotation = expanded ? 'rotate(-90deg)' : 'rotate(90deg)'
|
||||
} else {
|
||||
rotation = expanded ? 'rotate(180deg)' : 'rotate(0deg)'
|
||||
}
|
||||
|
||||
return {
|
||||
transform: translation + rotation
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.container = this.$refs.container
|
||||
},
|
||||
|
||||
methods: {
|
||||
bindEvents() {
|
||||
// Passive: false to prevent scrolling while touch dragging.
|
||||
document.addEventListener('mousemove', this.onMouseMove, {
|
||||
passive: false
|
||||
})
|
||||
document.addEventListener('mouseup', this.onMouseUp)
|
||||
|
||||
if ('ontouchstart' in window) {
|
||||
document.addEventListener('touchmove', this.onMouseMove, {
|
||||
passive: false
|
||||
})
|
||||
document.addEventListener('touchend', this.onMouseUp)
|
||||
}
|
||||
},
|
||||
|
||||
unbindEvents() {
|
||||
document.removeEventListener('mousemove', this.onMouseMove, {
|
||||
passive: false
|
||||
})
|
||||
document.removeEventListener('mouseup', this.onMouseUp)
|
||||
|
||||
if ('ontouchstart' in window) {
|
||||
document.removeEventListener('touchmove', this.onMouseMove, {
|
||||
passive: false
|
||||
})
|
||||
document.removeEventListener('touchend', this.onMouseUp)
|
||||
}
|
||||
},
|
||||
|
||||
onMouseMove(event) {
|
||||
event.preventDefault()
|
||||
this.dragging = true
|
||||
this.movableSplitter.visibility = 'visible'
|
||||
this.moveSplitter(event)
|
||||
},
|
||||
|
||||
onMouseUp() {
|
||||
if (this.dragging) {
|
||||
const dragPercentage = this.horizontal
|
||||
? this.movableSplitter.top
|
||||
: this.movableSplitter.left
|
||||
|
||||
this.paneBefore.size = dragPercentage
|
||||
this.paneAfter.size = 100 - dragPercentage
|
||||
|
||||
this.movableSplitter = {
|
||||
top: 0,
|
||||
left: 0,
|
||||
visibility: 'hidden'
|
||||
}
|
||||
|
||||
this.dragging = false
|
||||
}
|
||||
|
||||
this.unbindEvents()
|
||||
},
|
||||
|
||||
moveSplitter(event) {
|
||||
const splitterInfo = {
|
||||
container: this.container,
|
||||
paneBeforeMax: this.paneBefore.max,
|
||||
paneAfterMax: this.paneAfter.max,
|
||||
isHorisontal: this.horizontal
|
||||
}
|
||||
const offset = splitter.calculateOffset(event, splitterInfo)
|
||||
const dir = this.horizontal ? 'top' : 'left'
|
||||
this.movableSplitter[dir] = offset
|
||||
},
|
||||
|
||||
togglePane(pane) {
|
||||
if (pane.size > 0) {
|
||||
this.beforeMinimising.before = this.paneBefore.size
|
||||
this.beforeMinimising.after = this.paneAfter.size
|
||||
pane.size = 0
|
||||
const otherPane =
|
||||
pane === this.paneBefore ? this.paneAfter : this.paneBefore
|
||||
otherPane.size = 100 - pane.size
|
||||
} else {
|
||||
this.paneBefore.size = this.beforeMinimising.before
|
||||
this.paneAfter.size = this.beforeMinimising.after
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.splitpanes {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.splitpanes-vertical {
|
||||
flex-direction: row;
|
||||
}
|
||||
.splitpanes-horizontal {
|
||||
flex-direction: column;
|
||||
}
|
||||
.splitpanes-dragging * {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.splitpanes-pane {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
/* Splitter */
|
||||
|
||||
.splitpanes-vertical > .splitpanes-splitter,
|
||||
.splitpanes-vertical.splitpanes-dragging {
|
||||
cursor: col-resize;
|
||||
}
|
||||
.splitpanes-horizontal > .splitpanes-splitter,
|
||||
.splitpanes-horizontal.splitpanes-dragging {
|
||||
cursor: row-resize;
|
||||
}
|
||||
|
||||
.splitpanes-splitter {
|
||||
touch-action: none;
|
||||
background-color: var(--color-bg-light);
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.splitpanes-horizontal > .splitpanes-splitter {
|
||||
border-top: 1px solid var(--color-border-light);
|
||||
border-bottom: 1px solid var(--color-border-light);
|
||||
}
|
||||
|
||||
.splitpanes-vertical > .splitpanes-splitter {
|
||||
border-left: 1px solid var(--color-border-light);
|
||||
border-right: 1px solid var(--color-border-light);
|
||||
}
|
||||
|
||||
.movable-splitter {
|
||||
position: absolute;
|
||||
background-color: rgba(162, 177, 198, 0.5);
|
||||
}
|
||||
|
||||
.splitpanes-vertical > .splitpanes-splitter,
|
||||
.splitpanes-vertical > .movable-splitter {
|
||||
width: 8px;
|
||||
z-index: 5;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.splitpanes-horizontal > .splitpanes-splitter,
|
||||
.splitpanes-horizontal > .movable-splitter {
|
||||
height: 8px;
|
||||
width: 100%;
|
||||
z-index: 5;
|
||||
}
|
||||
/* Toggle buttons */
|
||||
.toggle-btns {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.splitpanes-vertical > .splitpanes-splitter .toggle-btns {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.splitpanes-horizontal > .splitpanes-splitter .toggle-btns {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.toggle-btn {
|
||||
background-color: var(--color-border-light);
|
||||
border-radius: var(--border-radius-small);
|
||||
border: 1px solid var(--color-border);
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.splitpanes-vertical > .splitpanes-splitter .toggle-btn {
|
||||
height: 49px;
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
.splitpanes-horizontal > .splitpanes-splitter .toggle-btn {
|
||||
width: 49px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
.toggle-btn .direction-icon {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
}
|
||||
|
||||
.splitpanes-horizontal
|
||||
> .splitpanes-splitter
|
||||
.toggle-btns.both
|
||||
.toggle-btn:first-child {
|
||||
border-radius: var(--border-radius-small) 0 0 var(--border-radius-small);
|
||||
}
|
||||
|
||||
.splitpanes-horizontal
|
||||
> .splitpanes-splitter
|
||||
.toggle-btns.both
|
||||
.toggle-btn:last-child {
|
||||
border-radius: 0 var(--border-radius-small) var(--border-radius-small) 0;
|
||||
margin-left: -1px;
|
||||
}
|
||||
|
||||
.splitpanes-vertical
|
||||
> .splitpanes-splitter
|
||||
.toggle-btns.both
|
||||
.toggle-btn:first-child {
|
||||
border-radius: var(--border-radius-small) var(--border-radius-small) 0 0;
|
||||
}
|
||||
|
||||
.splitpanes-vertical
|
||||
> .splitpanes-splitter
|
||||
.toggle-btns.both
|
||||
.toggle-btn:last-child {
|
||||
border-radius: 0 0 var(--border-radius-small) var(--border-radius-small);
|
||||
margin-top: -1px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,47 @@
|
||||
export default {
|
||||
// Get the cursor position relative to the splitpane container.
|
||||
getCurrentMouseDrag(event, container) {
|
||||
const rect = container.getBoundingClientRect()
|
||||
const { clientX, clientY } =
|
||||
'ontouchstart' in window && event.touches ? event.touches[0] : event
|
||||
return {
|
||||
x: clientX - rect.left,
|
||||
y: clientY - rect.top
|
||||
}
|
||||
},
|
||||
|
||||
// Returns the drag percentage of the splitter relative to the 2 panes it's inbetween.
|
||||
getCurrentDragPercentage(event, container, isHorisontal) {
|
||||
let drag = this.getCurrentMouseDrag(event, container)
|
||||
drag = drag[isHorisontal ? 'y' : 'x']
|
||||
const containerSize =
|
||||
container[isHorisontal ? 'clientHeight' : 'clientWidth']
|
||||
return (drag * 100) / containerSize
|
||||
},
|
||||
|
||||
// Returns the new position in percents.
|
||||
calculateOffset(
|
||||
event,
|
||||
{ container, isHorisontal, paneBeforeMax, paneAfterMax }
|
||||
) {
|
||||
const dragPercentage = this.getCurrentDragPercentage(
|
||||
event,
|
||||
container,
|
||||
isHorisontal
|
||||
)
|
||||
|
||||
const paneBeforeMaxReached =
|
||||
paneBeforeMax < 100 && dragPercentage >= paneBeforeMax
|
||||
const paneAfterMaxReached =
|
||||
paneAfterMax < 100 && dragPercentage <= 100 - paneAfterMax
|
||||
|
||||
// Prevent dragging beyond pane max.
|
||||
if (paneBeforeMaxReached || paneAfterMaxReached) {
|
||||
return paneBeforeMaxReached
|
||||
? paneBeforeMax
|
||||
: Math.max(100 - paneAfterMax, 0)
|
||||
} else {
|
||||
return Math.min(Math.max(dragPercentage, 0), paneBeforeMax)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
<template>
|
||||
<div>
|
||||
<div
|
||||
v-if="label"
|
||||
:class="['text-field-label', { error: errorMsg }, { disabled: disabled }]"
|
||||
>
|
||||
{{ label }}
|
||||
<hint-icon
|
||||
v-if="hint"
|
||||
class="hint"
|
||||
:hint="hint"
|
||||
:maxWidth="maxHintWidth || '149px'"
|
||||
/>
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
:placeholder="placeholder"
|
||||
:class="{ error: errorMsg }"
|
||||
:style="{ width: width }"
|
||||
:value="modelValue"
|
||||
:disabled="disabled"
|
||||
@input="$emit('update:modelValue', $event.target.value)"
|
||||
/>
|
||||
<div v-show="errorMsg" class="text-field-error">{{ errorMsg }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import HintIcon from '@/components/svg/hint'
|
||||
export default {
|
||||
name: 'TextField',
|
||||
components: { HintIcon },
|
||||
props: {
|
||||
placeholder: String,
|
||||
label: String,
|
||||
errorMsg: String,
|
||||
modelValue: String,
|
||||
width: String,
|
||||
hint: String,
|
||||
maxHintWidth: String,
|
||||
disabled: Boolean
|
||||
},
|
||||
emits: ['update:modelValue']
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
input {
|
||||
background: var(--color-white);
|
||||
border: 1px solid var(--color-border);
|
||||
color: var(--color-text-base);
|
||||
border-radius: var(--border-radius-medium-2);
|
||||
height: 36px;
|
||||
padding: 0 8px;
|
||||
font-size: 13px;
|
||||
box-sizing: border-box;
|
||||
display: block;
|
||||
}
|
||||
|
||||
input::placeholder {
|
||||
color: var(--color-text-light-2);
|
||||
}
|
||||
|
||||
input:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
input:disabled {
|
||||
background: var(--color-bg-light);
|
||||
color: var(--color-text-light-2);
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
input.error {
|
||||
border-color: var(--color-text-error);
|
||||
}
|
||||
|
||||
.text-field-label {
|
||||
font-size: 12px;
|
||||
color: var(--color-text-base);
|
||||
padding-left: 8px;
|
||||
margin-bottom: 2px;
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.text-field-label .hint {
|
||||
position: absolute;
|
||||
top: -2px;
|
||||
right: -22px;
|
||||
}
|
||||
|
||||
.text-field-label.error {
|
||||
color: var(--color-text-error);
|
||||
}
|
||||
|
||||
.text-field-label.disabled {
|
||||
color: var(--color-text-light-2);
|
||||
}
|
||||
|
||||
.text-field-error {
|
||||
color: var(--color-text-error);
|
||||
font-size: 12px;
|
||||
padding-left: 8px;
|
||||
margin-top: 2px;
|
||||
position: absolute;
|
||||
}
|
||||
.text-field-error:first-letter {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user