Co-authored-by: MeiMei <30769358+mei23@users.noreply.github.com>
Co-authored-by: Satsuki Yanagi <17376330+u1-liquid@users.noreply.github.com>
This commit is contained in:
syuilo
2020-01-30 04:37:25 +09:00
committed by GitHub
parent a5955c1123
commit f6154dc0af
871 changed files with 26140 additions and 71950 deletions

View File

@@ -0,0 +1,204 @@
<template>
<component class="bghgjjyj _button"
:is="link ? 'a' : 'button'"
:class="{ inline, primary }"
:type="type"
@click="$emit('click', $event)"
@mousedown="onMousedown"
>
<div ref="ripples" class="ripples"></div>
<div class="content">
<slot></slot>
</div>
</component>
</template>
<script lang="ts">
import Vue from 'vue';
export default Vue.extend({
props: {
type: {
type: String,
required: false
},
primary: {
type: Boolean,
required: false,
default: false
},
inline: {
type: Boolean,
required: false,
default: false
},
link: {
type: Boolean,
required: false,
default: false
},
autofocus: {
type: Boolean,
required: false,
default: false
},
wait: {
type: Boolean,
required: false,
default: false
},
},
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;
display: block;
min-width: 100px;
padding: 8px 14px;
text-align: center;
font-weight: normal;
font-size: 14px;
line-height: 24px;
box-shadow: none;
text-decoration: none;
background: var(--buttonBg);
border-radius: 6px;
overflow: hidden;
&:not(:disabled):hover {
background: var(--buttonHoverBg);
}
&:not(:disabled):active {
background: var(--buttonHoverBg);
}
&.primary {
color: #fff;
background: var(--accent);
&:not(:disabled):hover {
background: var(--jkhztclx);
}
&:not(:disabled):active {
background: var(--jkhztclx);
}
}
&:disabled {
opacity: 0.7;
}
&:focus {
&:after {
content: "";
pointer-events: none;
position: absolute;
top: -5px;
right: -5px;
bottom: -5px;
left: -5px;
border: 2px solid var(--accentAlpha03);
border-radius: 10px;
}
}
&.inline + .bghgjjyj {
margin-left: 12px;
}
&:not(.inline) + .bghgjjyj {
margin-top: 16px;
}
&.inline {
display: inline-block;
width: auto;
min-width: 100px;
}
&.primary {
font-weight: bold;
}
> .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>

View File

@@ -0,0 +1,104 @@
<template>
<div class="ukygtjoj _panel" :class="{ naked, hideHeader: !showHeader }">
<header v-if="showHeader">
<div class="title"><slot name="header"></slot></div>
<slot name="func"></slot>
<button class="_button" v-if="bodyTogglable" @click="() => showBody = !showBody">
<template v-if="showBody"><fa :icon="faAngleUp"/></template>
<template v-else><fa :icon="faAngleDown"/></template>
</button>
</header>
<div v-show="showBody">
<slot></slot>
</div>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import { faAngleUp, faAngleDown } from '@fortawesome/free-solid-svg-icons';
export default Vue.extend({
props: {
showHeader: {
type: Boolean,
required: false,
default: true
},
naked: {
type: Boolean,
required: false,
default: false
},
bodyTogglable: {
type: Boolean,
required: false,
default: false
},
expanded: {
type: Boolean,
required: false,
default: true
},
},
data() {
return {
showBody: this.expanded,
faAngleUp, faAngleDown
};
},
methods: {
toggleContent(show: boolean) {
if (!this.bodyTogglable) return;
this.showBody = show;
}
}
});
</script>
<style lang="scss" scoped>
.ukygtjoj {
position: relative;
overflow: hidden;
& + .ukygtjoj {
margin-top: var(--margin);
}
&.naked {
background: transparent !important;
box-shadow: none !important;
}
> header {
position: relative;
> .title {
margin: 0;
padding: 12px 16px;
@media (max-width: 500px) {
padding: 8px 10px;
}
> [data-icon] {
margin-right: 6px;
}
&:empty {
display: none;
}
}
> button {
position: absolute;
z-index: 2;
top: 0;
right: 0;
padding: 0;
width: 42px;
height: 100%;
}
}
}
</style>

View File

@@ -0,0 +1,15 @@
<template>
<div class="evrzpitu"></div>
</template>
<script lang="ts">
import Vue from 'vue';
export default Vue.extend({});
</script>
<style lang="scss" scoped>
.evrzpitu
margin 16px 0
border-bottom solid var(--lineWidth) var(--faceDivider)
</style>

View File

@@ -0,0 +1,55 @@
<template>
<div class="fpezltsf" :class="{ warn }">
<i v-if="warn"><fa :icon="faExclamationTriangle"/></i>
<i v-else><fa :icon="faInfoCircle"/></i>
<slot></slot>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import { faInfoCircle, faExclamationTriangle } from '@fortawesome/free-solid-svg-icons';
export default Vue.extend({
props: {
warn: {
type: Boolean,
required: false,
default: false
},
},
data() {
return {
faInfoCircle, faExclamationTriangle
};
}
});
</script>
<style lang="scss" scoped>
.fpezltsf {
margin: 16px 0;
padding: 16px;
font-size: 90%;
background: var(--infoBg);
color: var(--infoFg);
border-radius: 5px;
&.warn {
background: var(--infoWarnBg);
color: var(--infoWarnFg);
}
&:first-child {
margin-top: 0;
}
&:last-child {
margin-bottom: 0;
}
> i {
margin-right: 4px;
}
}
</style>

View File

@@ -0,0 +1,443 @@
<template>
<div class="juejbjww" :class="{ focused, filled, inline, disabled }">
<div class="icon" ref="icon"><slot name="icon"></slot></div>
<div class="input">
<span class="label" ref="label"><slot></slot></span>
<span class="title" ref="title">
<slot name="title"></slot>
<span class="warning" v-if="invalid"><fa :icon="faExclamationCircle"/>{{ $refs.input.validationMessage }}</span>
</span>
<div class="prefix" ref="prefix"><slot name="prefix"></slot></div>
<template v-if="type != 'file'">
<input v-if="debounce" ref="input"
v-debounce="500"
:type="type"
v-model.lazy="v"
:disabled="disabled"
:required="required"
:readonly="readonly"
:placeholder="placeholder"
:pattern="pattern"
:autocomplete="autocomplete"
:spellcheck="spellcheck"
@focus="focused = true"
@blur="focused = false"
@keydown="$emit('keydown', $event)"
@input="onInput"
:list="id"
>
<input v-else ref="input"
:type="type"
v-model="v"
:disabled="disabled"
:required="required"
:readonly="readonly"
:placeholder="placeholder"
:pattern="pattern"
:autocomplete="autocomplete"
:spellcheck="spellcheck"
@focus="focused = true"
@blur="focused = false"
@keydown="$emit('keydown', $event)"
@input="onInput"
:list="id"
>
<datalist :id="id" v-if="datalist">
<option v-for="data in datalist" :value="data"/>
</datalist>
</template>
<template v-else>
<input ref="input"
type="text"
:value="filePlaceholder"
readonly
@click="chooseFile"
>
<input ref="file"
type="file"
:value="value"
@change="onChangeFile"
>
</template>
<div class="suffix" ref="suffix"><slot name="suffix"></slot></div>
</div>
<button class="save _textButton" v-if="save && changed" @click="() => { changed = false; save(); }">{{ $t('save') }}</button>
<div class="desc"><slot name="desc"></slot></div>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import debounce from 'v-debounce';
import { faExclamationCircle } from '@fortawesome/free-solid-svg-icons';
export default Vue.extend({
directives: {
debounce
},
props: {
value: {
required: false
},
type: {
type: String,
required: false
},
required: {
type: Boolean,
required: false
},
readonly: {
type: Boolean,
required: false
},
disabled: {
type: Boolean,
required: false
},
pattern: {
type: String,
required: false
},
placeholder: {
type: String,
required: false
},
autofocus: {
type: Boolean,
required: false,
default: false
},
autocomplete: {
required: false
},
spellcheck: {
required: false
},
debounce: {
required: false
},
datalist: {
type: Array,
required: false,
},
inline: {
type: Boolean,
required: false,
default: false
},
save: {
type: Function,
required: false,
},
},
data() {
return {
v: this.value,
focused: false,
invalid: false,
changed: false,
id: Math.random().toString(),
faExclamationCircle
};
},
computed: {
filled(): boolean {
return this.v !== '' && this.v != null;
},
filePlaceholder(): string | null {
if (this.type != 'file') return null;
if (this.v == null) return null;
if (typeof this.v == 'string') return this.v;
if (Array.isArray(this.v)) {
return this.v.map(file => file.name).join(', ');
} else {
return this.v.name;
}
}
},
watch: {
value(v) {
this.v = v;
},
v(v) {
if (this.type === 'number') {
this.$emit('input', parseInt(v, 10));
} else {
this.$emit('input', v);
}
this.invalid = this.$refs.input.validity.badInput;
}
},
mounted() {
if (this.autofocus) {
this.$nextTick(() => {
this.$refs.input.focus();
});
}
this.$nextTick(() => {
// このコンポーネントが作成された時、非表示状態である場合がある
// 非表示状態だと要素の幅などは0になってしまうので、定期的に計算する
const clock = setInterval(() => {
if (this.$refs.prefix) {
this.$refs.label.style.left = (this.$refs.prefix.offsetLeft + this.$refs.prefix.offsetWidth) + 'px';
if (this.$refs.prefix.offsetWidth) {
this.$refs.input.style.paddingLeft = this.$refs.prefix.offsetWidth + 'px';
}
}
if (this.$refs.suffix) {
if (this.$refs.suffix.offsetWidth) {
this.$refs.input.style.paddingRight = this.$refs.suffix.offsetWidth + 'px';
}
}
}, 100);
this.$once('hook:beforeDestroy', () => {
clearInterval(clock);
});
});
this.$on('keydown', (e: KeyboardEvent) => {
if (e.code == 'Enter') {
this.$emit('enter');
}
});
},
methods: {
focus() {
this.$refs.input.focus();
},
togglePassword() {
if (this.type == 'password') {
this.type = 'text'
} else {
this.type = 'password'
}
},
chooseFile() {
this.$refs.file.click();
},
onChangeFile() {
this.v = Array.from((this.$refs.file as any).files);
this.$emit('input', this.v);
this.$emit('change', this.v);
},
onInput(ev) {
this.changed = true;
this.$emit('change', ev);
}
}
});
</script>
<style lang="scss" scoped>
.juejbjww {
position: relative;
margin: 32px 0;
> .icon {
position: absolute;
top: 0;
left: 0;
width: 24px;
text-align: center;
line-height: 32px;
&:not(:empty) + .input {
margin-left: 28px;
}
}
> .input {
position: relative;
&:before {
content: '';
display: block;
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 1px;
background: var(--inputBorder);
}
&:after {
content: '';
display: block;
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 2px;
background: var(--accent);
opacity: 0;
transform: scaleX(0.12);
transition: border 0.3s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1), transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
will-change: border opacity transform;
}
> .label {
position: absolute;
z-index: 1;
top: 0;
left: 0;
pointer-events: none;
transition: 0.4s cubic-bezier(0.25, 0.8, 0.25, 1);
transition-duration: 0.3s;
font-size: 16px;
line-height: 32px;
color: var(--inputLabel);
pointer-events: none;
//will-change transform
transform-origin: top left;
transform: scale(1);
}
> .title {
position: absolute;
z-index: 1;
top: -17px;
left: 0 !important;
pointer-events: none;
font-size: 16px;
line-height: 32px;
color: var(--inputLabel);
pointer-events: none;
//will-change transform
transform-origin: top left;
transform: scale(.75);
white-space: nowrap;
width: 133%;
overflow: hidden;
text-overflow: ellipsis;
> .warning {
margin-left: 0.5em;
color: var(--infoWarnFg);
> svg {
margin-right: 0.1em;
}
}
}
> input {
display: block;
width: 100%;
margin: 0;
padding: 0;
font: inherit;
font-weight: normal;
font-size: 16px;
line-height: 32px;
color: var(--inputText);
background: transparent;
border: none;
border-radius: 0;
outline: none;
box-shadow: none;
box-sizing: border-box;
&[type='file'] {
display: none;
}
}
> .prefix,
> .suffix {
display: block;
position: absolute;
z-index: 1;
top: 0;
font-size: 16px;
line-height: 32px;
color: var(--inputLabel);
pointer-events: none;
&:empty {
display: none;
}
> * {
display: inline-block;
min-width: 16px;
max-width: 150px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
}
> .prefix {
left: 0;
padding-right: 4px;
}
> .suffix {
right: 0;
padding-left: 4px;
}
}
> .save {
margin: 6px 0 0 0;
font-size: 13px;
}
> .desc {
margin: 6px 0 0 0;
font-size: 13px;
opacity: 0.7;
&:empty {
display: none;
}
* {
margin: 0;
}
}
&.focused {
> .input {
&:after {
opacity: 1;
transform: scaleX(1);
}
> .label {
color: var(--accent);
}
}
}
&.focused,
&.filled {
> .input {
> .label {
top: -17px;
left: 0 !important;
transform: scale(0.75);
}
}
}
&.inline {
display: inline-block;
margin: 0;
}
&.disabled {
opacity: 0.7;
&, * {
cursor: not-allowed !important;
}
}
}
</style>

View File

@@ -0,0 +1,59 @@
<template>
<sequential-entrance class="cxiknjgy" :class="{ autoMargin }">
<slot :items="items"></slot>
<div class="empty" v-if="empty" key="_empty_">
<slot name="empty"></slot>
</div>
<div class="more" v-if="more" key="_more_">
<mk-button :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }" @click="fetchMore()">
<template v-if="!moreFetching">{{ $t('loadMore') }}</template>
<template v-if="moreFetching"><fa :icon="faSpinner" pulse fixed-width/></template>
</mk-button>
</div>
</sequential-entrance>
</template>
<script lang="ts">
import Vue from 'vue';
import { faSpinner } from '@fortawesome/free-solid-svg-icons';
import MkButton from './button.vue';
import paging from '../../scripts/paging';
export default Vue.extend({
mixins: [
paging({}),
],
components: {
MkButton
},
props: {
pagination: {
required: true
},
autoMargin: {
required: false,
default: true
}
},
data() {
return {
faSpinner
};
},
});
</script>
<style lang="scss" scoped>
.cxiknjgy {
&.autoMargin > *:not(:last-child) {
margin-bottom: 16px;
@media (max-width: 500px) {
margin-bottom: 8px;
}
}
}
</style>

View File

@@ -0,0 +1,119 @@
<template>
<div
class="novjtctn"
:class="{ disabled, checked }"
:aria-checked="checked"
:aria-disabled="disabled"
@click="toggle"
>
<input type="radio"
:disabled="disabled"
>
<span class="button">
<span></span>
</span>
<span class="label"><slot></slot></span>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
export default Vue.extend({
model: {
prop: 'model',
event: 'change'
},
props: {
model: {
required: false
},
value: {
required: false
},
disabled: {
type: Boolean,
default: false
}
},
computed: {
checked(): boolean {
return this.model === this.value;
}
},
methods: {
toggle() {
this.$emit('change', this.value);
}
}
});
</script>
<style lang="scss" scoped>
.novjtctn {
display: inline-block;
margin: 0 32px 0 0;
cursor: pointer;
transition: all 0.3s;
> * {
user-select: none;
}
&.disabled {
opacity: 0.6;
cursor: not-allowed;
}
&.checked {
> .button {
border-color: var(--radioActive);
&:after {
background-color: var(--radioActive);
transform: scale(1);
opacity: 1;
}
}
}
> input {
position: absolute;
width: 0;
height: 0;
opacity: 0;
margin: 0;
}
> .button {
position: absolute;
width: 20px;
height: 20px;
background: none;
border: solid 2px var(--inputLabel);
border-radius: 100%;
transition: inherit;
&:after {
content: '';
display: block;
position: absolute;
top: 3px;
right: 3px;
bottom: 3px;
left: 3px;
border-radius: 100%;
opacity: 0;
transform: scale(0);
transition: 0.4s cubic-bezier(0.25, 0.8, 0.25, 1);
}
}
> .label {
margin-left: 28px;
display: block;
font-size: 16px;
line-height: 20px;
cursor: pointer;
}
}
</style>

View File

@@ -0,0 +1,220 @@
<template>
<div class="eiipwacr" :class="{ focused, disabled, filled, inline }">
<div class="icon" ref="icon"><slot name="icon"></slot></div>
<div class="input" @click="focus">
<span class="label" ref="label"><slot name="label"></slot></span>
<div class="prefix" ref="prefix"><slot name="prefix"></slot></div>
<select ref="input"
v-model="v"
:required="required"
:disabled="disabled"
@focus="focused = true"
@blur="focused = false"
>
<slot></slot>
</select>
<div class="suffix"><slot name="suffix"></slot></div>
</div>
<div class="text"><slot name="text"></slot></div>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
export default Vue.extend({
props: {
value: {
required: false
},
required: {
type: Boolean,
required: false
},
disabled: {
type: Boolean,
required: false
},
inline: {
type: Boolean,
required: false,
default: false
},
},
data() {
return {
focused: false
};
},
computed: {
v: {
get() {
return this.value;
},
set(v) {
this.$emit('input', v);
}
},
filled(): boolean {
return this.v != '' && this.v != null;
}
},
mounted() {
if (this.$refs.prefix) {
this.$refs.label.style.left = (this.$refs.prefix.offsetLeft + this.$refs.prefix.offsetWidth) + 'px';
}
},
methods: {
focus() {
this.$refs.input.focus();
}
}
});
</script>
<style lang="scss" scoped>
.eiipwacr {
position: relative;
margin: 32px 0;
> .icon {
position: absolute;
top: 0;
left: 0;
width: 24px;
text-align: center;
line-height: 32px;
&:not(:empty) + .input {
margin-left: 28px;
}
}
> .input {
display: flex;
&:before {
content: '';
display: block;
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 1px;
background: var(--inputBorder);
}
&:after {
content: '';
display: block;
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 2px;
background: var(--accent);
opacity: 0;
transform: scaleX(0.12);
transition: border 0.3s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1), transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
will-change: border opacity transform;
}
> .label {
position: absolute;
top: 0;
left: 0;
pointer-events: none;
transition: 0.4s cubic-bezier(0.25, 0.8, 0.25, 1);
transition-duration: 0.3s;
font-size: 16px;
line-height: 32px;
pointer-events: none;
//will-change transform
transform-origin: top left;
transform: scale(1);
}
> select {
display: block;
flex: 1;
width: 100%;
padding: 0;
font: inherit;
font-weight: normal;
font-size: 16px;
height: 32px;
background: var(--panel);
border: none;
border-radius: 0;
outline: none;
box-shadow: none;
color: var(--fg);
}
> .prefix,
> .suffix {
display: block;
align-self: center;
justify-self: center;
font-size: 16px;
line-height: 32px;
color: rgba(#000, 0.54);
pointer-events: none;
&:empty {
display: none;
}
> * {
display: block;
min-width: 16px;
}
}
> .prefix {
padding-right: 4px;
}
> .suffix {
padding-left: 4px;
}
}
> .text {
margin: 6px 0;
font-size: 13px;
&:empty {
display: none;
}
* {
margin: 0;
}
}
&.focused {
> .input {
&:after {
opacity: 1;
transform: scaleX(1);
}
> .label {
color: var(--accent);
}
}
}
&.focused,
&.filled {
> .input {
> .label {
top: -17px;
left: 0 !important;
transform: scale(0.75);
}
}
}
}
</style>

View File

@@ -0,0 +1,150 @@
<template>
<div
class="ziffeoms"
:class="{ disabled, checked }"
role="switch"
:aria-checked="checked"
:aria-disabled="disabled"
@click="toggle"
>
<input
type="checkbox"
ref="input"
:disabled="disabled"
@keydown.enter="toggle"
>
<span class="button">
<span></span>
</span>
<span class="label">
<span :aria-hidden="!checked"><slot></slot></span>
<p :aria-hidden="!checked">
<slot name="desc"></slot>
</p>
</span>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
export default Vue.extend({
model: {
prop: 'value',
event: 'change'
},
props: {
value: {
type: Boolean,
default: false
},
disabled: {
type: Boolean,
default: false
}
},
computed: {
checked(): boolean {
return this.value;
}
},
methods: {
toggle() {
if (this.disabled) return;
this.$emit('change', !this.checked);
}
}
});
</script>
<style lang="scss" scoped>
.ziffeoms {
position: relative;
display: flex;
margin: 32px 0;
cursor: pointer;
transition: all 0.3s;
&:first-child {
margin-top: 0;
}
&:last-child {
margin-bottom: 0;
}
> * {
user-select: none;
}
&.disabled {
opacity: 0.6;
cursor: not-allowed;
}
&.checked {
> .button {
background-color: var(--xxubwiul);
border-color: var(--xxubwiul);
> * {
background-color: var(--accent);
transform: translateX(14px);
}
}
}
> input {
position: absolute;
width: 0;
height: 0;
opacity: 0;
margin: 0;
}
> .button {
position: relative;
display: inline-block;
flex-shrink: 0;
margin: 3px 0 0 0;
width: 34px;
height: 14px;
background: var(--nhzhphzx);
outline: none;
border-radius: 14px;
transition: inherit;
> * {
position: absolute;
top: -3px;
left: 0;
border-radius: 100%;
transition: background-color 0.3s, transform 0.3s;
width: 20px;
height: 20px;
background-color: #fff;
box-shadow: 0 2px 1px -1px rgba(#000, 0.2), 0 1px 1px 0 rgba(#000, 0.14), 0 1px 3px 0 rgba(#000, 0.12);
}
}
> .label {
margin-left: 8px;
display: block;
font-size: 16px;
cursor: pointer;
transition: inherit;
color: var(--fg);
> span {
display: block;
line-height: 20px;
transition: inherit;
}
> p {
margin: 0;
opacity: 0.7;
font-size: 90%;
}
}
}
</style>

View File

@@ -0,0 +1,218 @@
<template>
<div class="adhpbeos" :class="{ focused, filled, tall, pre }">
<div class="input">
<span class="label" ref="label"><slot></slot></span>
<textarea ref="input"
:value="value"
:required="required"
:readonly="readonly"
:pattern="pattern"
:autocomplete="autocomplete"
@input="onInput"
@focus="focused = true"
@blur="focused = false"
></textarea>
</div>
<button class="save _textButton" v-if="save && changed" @click="() => { changed = false; save(); }">{{ $t('save') }}</button>
<div class="desc"><slot name="desc"></slot></div>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
export default Vue.extend({
props: {
value: {
required: false
},
required: {
type: Boolean,
required: false
},
readonly: {
type: Boolean,
required: false
},
pattern: {
type: String,
required: false
},
autocomplete: {
type: String,
required: false
},
tall: {
type: Boolean,
required: false,
default: false
},
pre: {
type: Boolean,
required: false,
default: false
},
save: {
type: Function,
required: false,
},
},
data() {
return {
focused: false,
changed: false,
}
},
computed: {
filled(): boolean {
return this.value != '' && this.value != null;
}
},
methods: {
focus() {
this.$refs.input.focus();
},
onInput(ev) {
this.changed = true;
this.$emit('input', ev.target.value);
}
}
});
</script>
<style lang="scss" scoped>
.adhpbeos {
margin: 42px 0 32px 0;
position: relative;
&:last-child {
margin-bottom: 0;
}
> .input {
position: relative;
&:before {
content: '';
display: block;
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
background: none;
border: solid 1px var(--inputBorder);
border-radius: 3px;
pointer-events: none;
}
&:after {
content: '';
display: block;
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
background: none;
border: solid 2px var(--accent);
border-radius: 3px;
opacity: 0;
transition: opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1);
pointer-events: none;
}
> .label {
position: absolute;
top: 6px;
left: 12px;
pointer-events: none;
transition: 0.4s cubic-bezier(0.25, 0.8, 0.25, 1);
transition-duration: 0.3s;
font-size: 16px;
line-height: 32px;
pointer-events: none;
//will-change transform
transform-origin: top left;
transform: scale(1);
}
> textarea {
display: block;
width: 100%;
min-width: 100%;
max-width: 100%;
min-height: 130px;
padding: 12px;
box-sizing: border-box;
font: inherit;
font-weight: normal;
font-size: 16px;
background: transparent;
border: none;
border-radius: 0;
outline: none;
box-shadow: none;
color: var(--fg);
}
}
> .save {
margin: 6px 0 0 0;
font-size: 13px;
}
> .desc {
margin: 6px 0 0 0;
font-size: 13px;
opacity: 0.7;
&:empty {
display: none;
}
* {
margin: 0;
}
}
&.focused {
> .input {
&:after {
opacity: 1;
}
> .label {
color: var(--accent);
}
}
}
&.focused,
&.filled {
> .input {
> .label {
top: -24px;
left: 0 !important;
transform: scale(0.75);
}
}
}
&.tall {
> .input {
> textarea {
min-height: 200px;
}
}
}
&.pre {
> .input {
> textarea {
white-space: pre;
}
}
}
}
</style>