diff options
| author | syuilo <syuilotan@yahoo.co.jp> | 2021-02-19 21:42:47 +0900 |
|---|---|---|
| committer | syuilo <syuilotan@yahoo.co.jp> | 2021-02-19 21:42:47 +0900 |
| commit | d6c8b9b99470db45c201229b5c9235e7be3067de (patch) | |
| tree | 01e5fccad6d84cf1e7f41e0a5e3aae955f3695e0 /src/client/components | |
| parent | Merge branch 'develop' (diff) | |
| parent | 12.70.0 (diff) | |
| download | misskey-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.vue | 5 | ||||
| -rw-r--r-- | src/client/components/form/input.vue | 120 | ||||
| -rw-r--r-- | src/client/components/form/textarea.vue | 101 | ||||
| -rw-r--r-- | src/client/components/global/avatar.vue | 4 | ||||
| -rw-r--r-- | src/client/components/note.vue | 4 | ||||
| -rw-r--r-- | src/client/components/notes.vue | 4 | ||||
| -rw-r--r-- | src/client/components/notifications.vue | 4 | ||||
| -rw-r--r-- | src/client/components/post-form.vue | 3 | ||||
| -rw-r--r-- | src/client/components/sample.vue | 2 | ||||
| -rw-r--r-- | src/client/components/sidebar.vue | 34 | ||||
| -rw-r--r-- | src/client/components/ui/modal.vue | 4 | ||||
| -rw-r--r-- | src/client/components/ui/tooltip.vue | 40 | ||||
| -rw-r--r-- | src/client/components/widgets.vue | 153 |
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> |