summaryrefslogtreecommitdiff
path: root/src/client/components
diff options
context:
space:
mode:
authorsyuilo <syuilotan@yahoo.co.jp>2021-02-19 21:42:47 +0900
committersyuilo <syuilotan@yahoo.co.jp>2021-02-19 21:42:47 +0900
commitd6c8b9b99470db45c201229b5c9235e7be3067de (patch)
tree01e5fccad6d84cf1e7f41e0a5e3aae955f3695e0 /src/client/components
parentMerge branch 'develop' (diff)
parent12.70.0 (diff)
downloadmisskey-d6c8b9b99470db45c201229b5c9235e7be3067de.tar.gz
misskey-d6c8b9b99470db45c201229b5c9235e7be3067de.tar.bz2
misskey-d6c8b9b99470db45c201229b5c9235e7be3067de.zip
Merge branch 'develop'
Diffstat (limited to 'src/client/components')
-rw-r--r--src/client/components/emoji-picker.vue5
-rw-r--r--src/client/components/form/input.vue120
-rw-r--r--src/client/components/form/textarea.vue101
-rw-r--r--src/client/components/global/avatar.vue4
-rw-r--r--src/client/components/note.vue4
-rw-r--r--src/client/components/notes.vue4
-rw-r--r--src/client/components/notifications.vue4
-rw-r--r--src/client/components/post-form.vue3
-rw-r--r--src/client/components/sample.vue2
-rw-r--r--src/client/components/sidebar.vue34
-rw-r--r--src/client/components/ui/modal.vue4
-rw-r--r--src/client/components/ui/tooltip.vue40
-rw-r--r--src/client/components/widgets.vue153
13 files changed, 344 insertions, 134 deletions
diff --git a/src/client/components/emoji-picker.vue b/src/client/components/emoji-picker.vue
index c2d1008e1b..9a261ef83f 100644
--- a/src/client/components/emoji-picker.vue
+++ b/src/client/components/emoji-picker.vue
@@ -99,7 +99,8 @@ import { faHeart, faFlag, faLaugh } from '@fortawesome/free-regular-svg-icons';
import MkModal from '@/components/ui/modal.vue';
import Particle from '@/components/particle.vue';
import * as os from '@/os';
-import { isDeviceTouch } from '../scripts/is-device-touch';
+import { isDeviceTouch } from '@/scripts/is-device-touch';
+import { isMobile } from '@/scripts/is-mobile';
import { emojiCategories } from '@/instance';
export default defineComponent({
@@ -322,7 +323,7 @@ export default defineComponent({
},
mounted() {
- if (!os.isMobile) {
+ if (!isMobile && !isDeviceTouch) {
this.$refs.search.focus({
preventScroll: true
});
diff --git a/src/client/components/form/input.vue b/src/client/components/form/input.vue
index c8c22e95c7..f0aa6b0534 100644
--- a/src/client/components/form/input.vue
+++ b/src/client/components/form/input.vue
@@ -1,63 +1,50 @@
<template>
-<div class="ztzhwixg _formItem" :class="{ inline, disabled }">
- <div class="_formLabel"><slot></slot></div>
- <div class="icon" ref="icon"><slot name="icon"></slot></div>
- <div class="input _formPanel">
- <div class="prefix" ref="prefixEl"><slot name="prefix"></slot></div>
- <input v-if="debounce" ref="inputEl"
- v-debounce="500"
- :type="type"
- v-model.lazy="v"
- :disabled="disabled"
- :required="required"
- :readonly="readonly"
- :placeholder="placeholder"
- :pattern="pattern"
- :autocomplete="autocomplete"
- :spellcheck="spellcheck"
- :step="step"
- @focus="focused = true"
- @blur="focused = false"
- @keydown="onKeydown($event)"
- @input="onInput"
- :list="id"
- >
- <input v-else ref="inputEl"
- :type="type"
- v-model="v"
- :disabled="disabled"
- :required="required"
- :readonly="readonly"
- :placeholder="placeholder"
- :pattern="pattern"
- :autocomplete="autocomplete"
- :spellcheck="spellcheck"
- :step="step"
- @focus="focused = true"
- @blur="focused = false"
- @keydown="onKeydown($event)"
- @input="onInput"
- :list="id"
- >
- <datalist :id="id" v-if="datalist">
- <option v-for="data in datalist" :value="data"/>
- </datalist>
- <div class="suffix" ref="suffixEl"><slot name="suffix"></slot></div>
+<FormGroup class="_formItem">
+ <template #label><slot></slot></template>
+ <div class="ztzhwixg _formItem" :class="{ inline, disabled }">
+ <div class="icon" ref="icon"><slot name="icon"></slot></div>
+ <div class="input _formPanel">
+ <div class="prefix" ref="prefixEl"><slot name="prefix"></slot></div>
+ <input ref="inputEl"
+ :type="type"
+ v-model="v"
+ :disabled="disabled"
+ :required="required"
+ :readonly="readonly"
+ :placeholder="placeholder"
+ :pattern="pattern"
+ :autocomplete="autocomplete"
+ :spellcheck="spellcheck"
+ :step="step"
+ @focus="focused = true"
+ @blur="focused = false"
+ @keydown="onKeydown($event)"
+ @input="onInput"
+ :list="id"
+ >
+ <datalist :id="id" v-if="datalist">
+ <option v-for="data in datalist" :value="data"/>
+ </datalist>
+ <div class="suffix" ref="suffixEl"><slot name="suffix"></slot></div>
+ </div>
</div>
- <button class="save _textButton" v-if="save && changed" @click="() => { changed = false; save(); }">{{ $ts.save }}</button>
- <div class="_formCaption"><slot name="desc"></slot></div>
-</div>
+ <template #caption><slot name="desc"></slot></template>
+
+ <FormButton v-if="manualSave && changed" @click="updated" primary><Fa :icon="faSave"/> {{ $ts.save }}</FormButton>
+</FormGroup>
</template>
<script lang="ts">
import { defineComponent, onMounted, onUnmounted, nextTick, ref, watch, computed, toRefs } from 'vue';
-import debounce from 'v-debounce';
-import { faExclamationCircle } from '@fortawesome/free-solid-svg-icons';
+import { faExclamationCircle, faSave } from '@fortawesome/free-solid-svg-icons';
import './form.scss';
+import FormButton from './button.vue';
+import FormGroup from './group.vue';
export default defineComponent({
- directives: {
- debounce
+ components: {
+ FormGroup,
+ FormButton,
},
props: {
value: {
@@ -101,9 +88,6 @@ export default defineComponent({
step: {
required: false
},
- debounce: {
- required: false
- },
datalist: {
type: Array,
required: false,
@@ -113,9 +97,10 @@ export default defineComponent({
required: false,
default: false
},
- save: {
- type: Function,
+ manualSave: {
+ type: Boolean,
required: false,
+ default: false
},
},
emits: ['change', 'keydown', 'enter'],
@@ -144,15 +129,22 @@ export default defineComponent({
}
};
+ const updated = () => {
+ changed.value = false;
+ if (type?.value === 'number') {
+ context.emit('update:value', parseFloat(v.value));
+ } else {
+ context.emit('update:value', v.value);
+ }
+ };
+
watch(value, newValue => {
v.value = newValue;
});
watch(v, newValue => {
- if (type?.value === 'number') {
- context.emit('update:value', parseFloat(newValue));
- } else {
- context.emit('update:value', newValue);
+ if (!props.manualSave) {
+ updated();
}
invalid.value = inputEl.value.validity.badInput;
@@ -198,7 +190,8 @@ export default defineComponent({
focus,
onInput,
onKeydown,
- faExclamationCircle,
+ updated,
+ faExclamationCircle, faSave,
};
},
});
@@ -285,11 +278,6 @@ export default defineComponent({
}
}
- > .save {
- margin: 6px 0 0 0;
- font-size: 0.8em;
- }
-
&.inline {
display: inline-block;
margin: 0;
diff --git a/src/client/components/form/textarea.vue b/src/client/components/form/textarea.vue
index 711cd50124..135e16c259 100644
--- a/src/client/components/form/textarea.vue
+++ b/src/client/components/form/textarea.vue
@@ -1,29 +1,39 @@
<template>
-<div class="rivhosbp _formItem" :class="{ tall, pre }">
- <div class="_formLabel"><slot></slot></div>
- <div class="input _formPanel">
- <textarea ref="input" :class="{ code, _monospace: code }"
- :value="value"
- :required="required"
- :readonly="readonly"
- :pattern="pattern"
- :autocomplete="autocomplete"
- :spellcheck="!code"
- @input="onInput"
- @focus="focused = true"
- @blur="focused = false"
- ></textarea>
+<FormGroup class="_formItem">
+ <template #label><slot></slot></template>
+ <div class="rivhosbp _formItem" :class="{ tall, pre }">
+ <div class="input _formPanel">
+ <textarea ref="input" :class="{ code, _monospace: code }"
+ v-model="v"
+ :required="required"
+ :readonly="readonly"
+ :pattern="pattern"
+ :autocomplete="autocomplete"
+ :spellcheck="!code"
+ @input="onInput"
+ @focus="focused = true"
+ @blur="focused = false"
+ ></textarea>
+ </div>
</div>
- <button class="save _textButton" v-if="save && changed" @click="() => { changed = false; save(); }">{{ $ts.save }}</button>
- <div class="_formCaption"><slot name="desc"></slot></div>
-</div>
+ <template #caption><slot name="desc"></slot></template>
+
+ <FormButton v-if="manualSave && changed" @click="updated" primary><Fa :icon="faSave"/> {{ $ts.save }}</FormButton>
+</FormGroup>
</template>
<script lang="ts">
-import { defineComponent } from 'vue';
+import { defineComponent, ref, toRefs, watch } from 'vue';
+import { faSave } from '@fortawesome/free-solid-svg-icons';
import './form.scss';
+import FormButton from './button.vue';
+import FormGroup from './group.vue';
export default defineComponent({
+ components: {
+ FormGroup,
+ FormButton,
+ },
props: {
value: {
required: false
@@ -58,24 +68,46 @@ export default defineComponent({
required: false,
default: false
},
- save: {
- type: Function,
+ manualSave: {
+ type: Boolean,
required: false,
+ default: false
},
},
- data() {
+ setup(props, context) {
+ const { value } = toRefs(props);
+ const v = ref(value.value);
+ const changed = ref(false);
+ const inputEl = ref(null);
+ const focus = () => inputEl.value.focus();
+ const onInput = (ev) => {
+ changed.value = true;
+ context.emit('change', ev);
+ };
+
+ const updated = () => {
+ changed.value = false;
+ context.emit('update:value', v.value);
+ };
+
+ watch(value, newValue => {
+ v.value = newValue;
+ });
+
+ watch(v, newValue => {
+ if (!props.manualSave) {
+ updated();
+ }
+ });
+
return {
- changed: false,
- }
- },
- methods: {
- focus() {
- this.$refs.input.focus();
- },
- onInput(ev) {
- this.changed = true;
- this.$emit('update:value', ev.target.value);
- }
+ v,
+ updated,
+ changed,
+ focus,
+ onInput,
+ faSave,
+ };
}
});
</script>
@@ -112,11 +144,6 @@ export default defineComponent({
}
}
- > .save {
- margin: 6px 0 0 0;
- font-size: 0.8em;
- }
-
&.tall {
> .input {
> textarea {
diff --git a/src/client/components/global/avatar.vue b/src/client/components/global/avatar.vue
index 9f8b0eeca1..d2f25fa41e 100644
--- a/src/client/components/global/avatar.vue
+++ b/src/client/components/global/avatar.vue
@@ -1,8 +1,8 @@
<template>
-<span class="eiwwqkts" :class="{ cat }" :title="acct(user)" v-if="disableLink" v-user-preview="disablePreview ? undefined : user.id" @click="onClick">
+<span class="eiwwqkts _noSelect" :class="{ cat }" :title="acct(user)" v-if="disableLink" v-user-preview="disablePreview ? undefined : user.id" @click="onClick">
<img class="inner" :src="url" decoding="async"/>
</span>
-<MkA class="eiwwqkts" :class="{ cat }" :to="userPage(user)" :title="acct(user)" :target="target" v-else v-user-preview="disablePreview ? undefined : user.id">
+<MkA class="eiwwqkts _noSelect" :class="{ cat }" :to="userPage(user)" :title="acct(user)" :target="target" v-else v-user-preview="disablePreview ? undefined : user.id">
<img class="inner" :src="url" decoding="async"/>
</MkA>
</template>
diff --git a/src/client/components/note.vue b/src/client/components/note.vue
index b839ab3e8f..d532289857 100644
--- a/src/client/components/note.vue
+++ b/src/client/components/note.vue
@@ -1,6 +1,6 @@
<template>
<div
- class="note _panel"
+ class="tkcbzcuz _panel"
v-if="!muted"
v-show="!isDeleted"
:tabindex="!isDeleted ? '-1' : null"
@@ -858,7 +858,7 @@ export default defineComponent({
</script>
<style lang="scss" scoped>
-.note {
+.tkcbzcuz {
position: relative;
transition: box-shadow 0.1s ease;
overflow: hidden;
diff --git a/src/client/components/notes.vue b/src/client/components/notes.vue
index 9973809192..bd6d5bb4f5 100644
--- a/src/client/components/notes.vue
+++ b/src/client/components/notes.vue
@@ -8,7 +8,7 @@
<MkError v-if="error" @retry="init()"/>
<div v-show="more && reversed" style="margin-bottom: var(--margin);">
- <button class="_loadMore" @click="fetchMore" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }">
+ <button class="_buttonPrimary" @click="fetchMoreFeature" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }">
<template v-if="!moreFetching">{{ $ts.loadMore }}</template>
<template v-if="moreFetching"><MkLoading inline/></template>
</button>
@@ -19,7 +19,7 @@
</XList>
<div v-show="more && !reversed" style="margin-top: var(--margin);">
- <button class="_loadMore" v-appear="$store.state.enableInfiniteScroll ? fetchMore : null" @click="fetchMore" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }">
+ <button class="_buttonPrimary" v-appear="$store.state.enableInfiniteScroll ? fetchMore : null" @click="fetchMore" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }">
<template v-if="!moreFetching">{{ $ts.loadMore }}</template>
<template v-if="moreFetching"><MkLoading inline/></template>
</button>
diff --git a/src/client/components/notifications.vue b/src/client/components/notifications.vue
index 9759cc2395..56dbfd5bdf 100644
--- a/src/client/components/notifications.vue
+++ b/src/client/components/notifications.vue
@@ -1,11 +1,11 @@
<template>
-<div class="mfcuwfyp">
+<div class="mfcuwfyp _noGap_">
<XList class="notifications" :items="items" v-slot="{ item: notification }">
<XNote v-if="['reply', 'quote', 'mention'].includes(notification.type)" :note="notification.note" @update:note="noteUpdated(notification.note, $event)" :key="notification.id"/>
<XNotification v-else :notification="notification" :with-time="true" :full="true" class="_panel notification" :key="notification.id"/>
</XList>
- <button class="_loadMore" v-appear="$store.state.enableInfiniteScroll ? fetchMore : null" @click="fetchMore" v-show="more" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }">
+ <button class="_buttonPrimary" v-appear="$store.state.enableInfiniteScroll ? fetchMore : null" @click="fetchMore" v-show="more" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }">
<template v-if="!moreFetching">{{ $ts.loadMore }}</template>
<template v-if="moreFetching"><MkLoading inline/></template>
</button>
diff --git a/src/client/components/post-form.vue b/src/client/components/post-form.vue
index d2c0cffa12..fa9aeff8af 100644
--- a/src/client/components/post-form.vue
+++ b/src/client/components/post-form.vue
@@ -69,6 +69,7 @@ import { noteVisibilities } from '../../types';
import * as os from '@/os';
import { selectFile } from '@/scripts/select-file';
import { notePostInterruptors, postFormActions } from '@/store';
+import { isMobile } from '@/scripts/is-mobile';
export default defineComponent({
components: {
@@ -554,7 +555,7 @@ export default defineComponent({
localOnly: this.localOnly,
visibility: this.visibility,
visibleUserIds: this.visibility == 'specified' ? this.visibleUsers.map(u => u.id) : undefined,
- viaMobile: os.isMobile
+ viaMobile: isMobile
};
// plugin
diff --git a/src/client/components/sample.vue b/src/client/components/sample.vue
index 8fd79ceec9..0f29fc69bb 100644
--- a/src/client/components/sample.vue
+++ b/src/client/components/sample.vue
@@ -51,7 +51,7 @@ export default defineComponent({
text: '',
flag: true,
radio: 'misskey',
- mfm: `Hello world! This is an @example mention. BTW you are @${this.$i.username}.\nAlso, here is ${config.url} and [example link](${config.url}). for more details, see https://example.com.\nAs you know #misskey is open-source software.`
+ mfm: `Hello world! This is an @example mention. BTW you are @${this.$i ? this.$i.username : 'guest'}.\nAlso, here is ${config.url} and [example link](${config.url}). for more details, see https://example.com.\nAs you know #misskey is open-source software.`
}
},
diff --git a/src/client/components/sidebar.vue b/src/client/components/sidebar.vue
index 251f68527a..f7c50a2ba5 100644
--- a/src/client/components/sidebar.vue
+++ b/src/client/components/sidebar.vue
@@ -55,6 +55,14 @@ import { sidebarDef } from '@/sidebar';
import { getAccounts, addAccount, login } from '@/account';
export default defineComponent({
+ props: {
+ defaultHidden: {
+ type: Boolean,
+ required: false,
+ default: false,
+ }
+ },
+
data() {
return {
host: host,
@@ -63,7 +71,7 @@ export default defineComponent({
connection: null,
menuDef: sidebarDef,
iconOnly: false,
- hidden: false,
+ hidden: this.defaultHidden,
faGripVertical, faChevronLeft, faComments, faHashtag, faBroadcastTower, faFireAlt, faEllipsisH, faPencilAlt, faBars, faTimes, faBell, faSearch, faUserCog, faCog, faUser, faHome, faStar, faCircle, faAt, faEnvelope, faListUl, faPlus, faUserClock, faLaugh, faUsers, faTachometerAlt, faExchangeAlt, faGlobe, faChartBar, faCloud, faServer, faProjectDiagram
};
},
@@ -112,7 +120,9 @@ export default defineComponent({
methods: {
calcViewState() {
this.iconOnly = (window.innerWidth <= 1279) || (this.$store.state.sidebarDisplay === 'icon');
- this.hidden = (window.innerWidth <= 650);
+ if (!this.defaultHidden) {
+ this.hidden = (window.innerWidth <= 650);
+ }
},
show() {
@@ -128,13 +138,19 @@ export default defineComponent({
},
async openAccountMenu(ev) {
- const storedAccounts = getAccounts();
- const accounts = (await os.api('users/show', { userIds: storedAccounts.map(x => x.id) })).filter(x => x.id !== this.$i.id);
+ const storedAccounts = getAccounts().filter(x => x.id !== this.$i.id);
+ const accountsPromise = os.api('users/show', { userIds: storedAccounts.map(x => x.id) });
- const accountItems = accounts.map(account => ({
- type: 'user',
- user: account,
- action: () => { this.switchAccount(account); }
+ const accountItemPromises = storedAccounts.map(a => new Promise(res => {
+ accountsPromise.then(accounts => {
+ const account = accounts.find(x => x.id === a.id);
+ if (account == null) return res(null);
+ res({
+ type: 'user',
+ user: account,
+ action: () => { this.switchAccount(account); }
+ });
+ });
}));
os.modalMenu([...[{
@@ -142,7 +158,7 @@ export default defineComponent({
text: this.$ts.profile,
to: `/@${ this.$i.username }`,
avatar: this.$i,
- }, null, ...accountItems, {
+ }, null, ...accountItemPromises, {
icon: faPlus,
text: this.$ts.addAcount,
action: () => {
diff --git a/src/client/components/ui/modal.vue b/src/client/components/ui/modal.vue
index 0d1038dce9..69a83e002c 100644
--- a/src/client/components/ui/modal.vue
+++ b/src/client/components/ui/modal.vue
@@ -98,11 +98,11 @@ export default defineComponent({
}
} else {
if (left + width - window.pageXOffset > window.innerWidth) {
- left = window.innerWidth - width + window.pageXOffset;
+ left = window.innerWidth - width + window.pageXOffset - 1;
}
if (top + height - window.pageYOffset > window.innerHeight) {
- top = window.innerHeight - height + window.pageYOffset;
+ top = window.innerHeight - height + window.pageYOffset - 1;
}
}
diff --git a/src/client/components/ui/tooltip.vue b/src/client/components/ui/tooltip.vue
index 6ea344c54d..b220fe5d8c 100644
--- a/src/client/components/ui/tooltip.vue
+++ b/src/client/components/ui/tooltip.vue
@@ -1,6 +1,6 @@
<template>
-<transition name="zoom-in-top" appear @after-leave="$emit('closed')">
- <div class="buebdbiu _acrylic _shadow" v-if="showing">
+<transition name="tooltip" appear @after-leave="$emit('closed')">
+ <div class="buebdbiu _acrylic _shadow" v-show="showing" ref="content">
<slot>{{ text }}</slot>
</div>
</transition>
@@ -35,19 +35,43 @@ export default defineComponent({
const rect = this.source.getBoundingClientRect();
- let x = rect.left + window.pageXOffset + (this.source.offsetWidth / 2);
- let y = rect.top + window.pageYOffset + this.source.offsetHeight;
+ const contentWidth = this.$refs.content.offsetWidth;
+ const contentHeight = this.$refs.content.offsetHeight;
- x -= (this.$el.offsetWidth / 2);
+ let left = rect.left + window.pageXOffset + (this.source.offsetWidth / 2);
+ let top = rect.top + window.pageYOffset + this.source.offsetHeight;
- this.$el.style.left = x + 'px';
- this.$el.style.top = y + 'px';
+ left -= (this.$el.offsetWidth / 2);
+
+ if (left + contentWidth - window.pageXOffset > window.innerWidth) {
+ left = window.innerWidth - contentWidth + window.pageXOffset - 1;
+ }
+
+ if (top + contentHeight - window.pageYOffset > window.innerHeight) {
+ top = rect.top + window.pageYOffset - contentHeight;
+ this.$refs.content.style.transformOrigin = 'center bottom';
+ }
+
+ 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;
@@ -57,6 +81,6 @@ export default defineComponent({
text-align: center;
border-radius: 4px;
pointer-events: none;
- transform-origin: center -16px;
+ transform-origin: center top;
}
</style>
diff --git a/src/client/components/widgets.vue b/src/client/components/widgets.vue
new file mode 100644
index 0000000000..23fce7d714
--- /dev/null
+++ b/src/client/components/widgets.vue
@@ -0,0 +1,153 @@
+<template>
+<div class="vjoppmmu">
+ <template v-if="edit">
+ <header>
+ <MkSelect v-model:value="widgetAdderSelected" style="margin-bottom: var(--margin)">
+ <template #label>{{ $ts.selectWidget }}</template>
+ <option v-for="widget in widgetDefs" :value="widget" :key="widget">{{ $t(`_widgets.${widget}`) }}</option>
+ </MkSelect>
+ <MkButton inline @click="addWidget" primary><Fa :icon="faPlus"/> {{ $ts.add }}</MkButton>
+ <MkButton inline @click="$emit('exit')">{{ $ts.close }}</MkButton>
+ </header>
+ <XDraggable
+ v-model="_widgets"
+ item-key="id"
+ animation="150"
+ >
+ <template #item="{element}">
+ <div class="customize-container">
+ <button class="config _button" @click.prevent.stop="configWidget(element.id)"><Fa :icon="faCog"/></button>
+ <button class="remove _button" @click.prevent.stop="removeWidget(element)"><Fa :icon="faTimes"/></button>
+ <component :is="`mkw-${element.name}`" :widget="element" :setting-callback="setting => settings[element.id] = setting" :column="column" @updateProps="updateWidget(element.id, $event)"/>
+ </div>
+ </template>
+ </XDraggable>
+ </template>
+ <component v-else class="widget" v-for="widget in widgets" :is="`mkw-${widget.name}`" :key="widget.id" :widget="widget" :column="column" @updateProps="updateWidget(widget.id, $event)"/>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent, defineAsyncComponent } from 'vue';
+import { v4 as uuid } from 'uuid';
+import { faTimes, faCog, faPlus } from '@fortawesome/free-solid-svg-icons';
+import MkSelect from '@/components/ui/select.vue';
+import MkButton from '@/components/ui/button.vue';
+import { widgets as widgetDefs } from '@/widgets';
+
+export default defineComponent({
+ components: {
+ XDraggable: defineAsyncComponent(() => import('vuedraggable').then(x => x.default)),
+ MkSelect,
+ MkButton,
+ },
+
+ props: {
+ widgets: {
+ required: true,
+ },
+ edit: {
+ type: Boolean,
+ required: true,
+ },
+ },
+
+ emits: ['updateWidgets', 'addWidget', 'removeWidget', 'updateWidget', 'exit'],
+
+ data() {
+ return {
+ widgetAdderSelected: null,
+ widgetDefs,
+ settings: {},
+ faTimes, faPlus, faCog
+ };
+ },
+
+ computed: {
+ _widgets: {
+ get() {
+ return this.widgets;
+ },
+ set(value) {
+ this.$emit('updateWidgets', value);
+ }
+ }
+ },
+
+ methods: {
+ configWidget(id) {
+ this.settings[id]();
+ },
+
+ addWidget() {
+ if (this.widgetAdderSelected == null) return;
+
+ this.$emit('addWidget', {
+ name: this.widgetAdderSelected,
+ id: uuid(),
+ data: {}
+ });
+
+ this.widgetAdderSelected = null;
+ },
+
+ removeWidget(widget) {
+ this.$emit('removeWidget', widget);
+ },
+
+ updateWidget(id, data) {
+ this.$emit('updateWidget', { id, data });
+ },
+ }
+});
+</script>
+
+<style lang="scss" scoped>
+.vjoppmmu {
+ > header {
+ margin: 16px 0;
+
+ > * {
+ width: 100%;
+ padding: 4px;
+ }
+ }
+
+ > .widget, .customize-container {
+ margin: var(--margin) 0;
+
+ &:first-of-type {
+ margin-top: 0;
+ }
+ }
+
+ .customize-container {
+ position: relative;
+ cursor: move;
+
+ > *:not(.remove):not(.config) {
+ pointer-events: none;
+ }
+
+ > .config,
+ > .remove {
+ position: absolute;
+ z-index: 10000;
+ top: 8px;
+ width: 32px;
+ height: 32px;
+ color: #fff;
+ background: rgba(#000, 0.7);
+ border-radius: 4px;
+ }
+
+ > .config {
+ right: 8px + 8px + 32px;
+ }
+
+ > .remove {
+ right: 8px;
+ }
+ }
+}
+</style>