From 60b3d73cc998b1ed8606ef19a7c992f729a731d8 Mon Sep 17 00:00:00 2001 From: syuilo <Syuilotan@yahoo.co.jp> Date: Wed, 21 Dec 2022 11:04:49 +0900 Subject: [PATCH] use sortablejs-vue3 instead of vuedraggable for more stability --- packages/client/package.json | 5 +- packages/client/src/components/MkPostForm.vue | 6 +- .../src/components/MkPostFormAttaches.vue | 208 +++++++++--------- packages/client/src/components/MkWidgets.vue | 26 ++- .../pages/page-editor/page-editor.blocks.vue | 20 +- .../src/pages/page-editor/page-editor.vue | 40 ++-- .../client/src/pages/settings/reaction.vue | 13 +- yarn.lock | 37 ++-- 8 files changed, 176 insertions(+), 179 deletions(-) diff --git a/packages/client/package.json b/packages/client/package.json index 87ef6e3638..c5fbf80317 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -46,6 +46,8 @@ "s-age": "1.1.2", "sass": "1.57.0", "seedrandom": "3.0.5", + "sortablejs": "^1.15.0", + "sortablejs-vue3": "^1.2.3", "strict-event-emitter-types": "2.0.0", "stringz": "2.1.0", "syuilo-password-strength": "0.0.1", @@ -61,8 +63,7 @@ "vanilla-tilt": "1.8.0", "vite": "4.0.2", "vue": "3.2.45", - "vue-prism-editor": "2.0.0-alpha.2", - "vuedraggable": "4.0.1" + "vue-prism-editor": "2.0.0-alpha.2" }, "devDependencies": { "@types/escape-regexp": "0.0.1", diff --git a/packages/client/src/components/MkPostForm.vue b/packages/client/src/components/MkPostForm.vue index a6215038f7..6911899799 100644 --- a/packages/client/src/components/MkPostForm.vue +++ b/packages/client/src/components/MkPostForm.vue @@ -43,7 +43,7 @@ <input v-show="useCw" ref="cwInputEl" v-model="cw" class="cw" :placeholder="i18n.ts.annotation" @keydown="onKeydown"> <textarea ref="textareaEl" v-model="text" class="text" :class="{ withCw: useCw }" :disabled="posting" :placeholder="placeholder" data-cy-post-form-text @keydown="onKeydown" @paste="onPaste" @compositionupdate="onCompositionUpdate" @compositionend="onCompositionEnd"/> <input v-show="withHashtags" ref="hashtagsInputEl" v-model="hashtags" class="hashtags" :placeholder="i18n.ts.hashtags" list="hashtags"> - <XPostFormAttaches class="attaches" :files="files" @updated="updateFiles" @detach="detachFile" @changeSensitive="updateFileSensitive" @changeName="updateFileName"/> + <XPostFormAttaches v-model="files" class="attaches" @detach="detachFile" @changeSensitive="updateFileSensitive" @changeName="updateFileName"/> <XPollEditor v-if="poll" v-model="poll" @destroyed="poll = null"/> <XNotePreview v-if="showPreview" class="preview" :text="text"/> <footer> @@ -370,10 +370,6 @@ function detachFile(id) { files = files.filter(x => x.id !== id); } -function updateFiles(_files) { - files = _files; -} - function updateFileSensitive(file, sensitive) { files[files.findIndex(x => x.id === file.id)].isSensitive = sensitive; } diff --git a/packages/client/src/components/MkPostFormAttaches.vue b/packages/client/src/components/MkPostFormAttaches.vue index 9eeffa55dd..44833db460 100644 --- a/packages/client/src/components/MkPostFormAttaches.vue +++ b/packages/client/src/components/MkPostFormAttaches.vue @@ -1,6 +1,6 @@ <template> -<div v-show="files.length != 0" class="skeikyzd"> - <XDraggable v-model="_files" class="files" item-key="id" animation="150" delay="100" delay-on-touch-only="true"> +<div v-show="props.modelValue.length != 0" class="skeikyzd"> + <Sortable :list="props.modelValue" class="files" item-key="id" :options="{ animation: 150, delay: 100, delayOnTouchOnly: true }" @end="onSorted"> <template #item="{element}"> <div class="file" @click="showFileMenu(element, $event)" @contextmenu.prevent="showFileMenu(element, $event)"> <MkDriveFileThumbnail :data-id="element.id" class="thumbnail" :file="element" fit="cover"/> @@ -9,128 +9,116 @@ </div> </div> </template> - </XDraggable> - <p class="remain">{{ 16 - files.length }}/16</p> + </Sortable> + <p class="remain">{{ 16 - props.modelValue.length }}/16</p> </div> </template> -<script lang="ts"> -import { defineComponent, defineAsyncComponent } from 'vue'; +<script lang="ts" setup> +import { defineAsyncComponent } from 'vue'; import MkDriveFileThumbnail from '@/components/MkDriveFileThumbnail.vue'; import * as os from '@/os'; +import { deepClone } from '@/scripts/clone'; +import { i18n } from '@/i18n'; -export default defineComponent({ - components: { - XDraggable: defineAsyncComponent(() => import('vuedraggable').then(x => x.default)), - MkDriveFileThumbnail, - }, +const Sortable = defineAsyncComponent(() => import('sortablejs-vue3').then(x => x.Sortable)); - props: { - files: { - type: Array, - required: true, +const props = defineProps<{ + modelValue: any[]; + detachMediaFn: () => void; +}>(); + +const emit = defineEmits<{ + (ev: 'update:modelValue', value: any[]): void; + (ev: 'detach'): void; + (ev: 'changeSensitive'): void; + (ev: 'changeName'): void; +}>(); + +let menuShowing = false; + +function onSorted(event) { + const items = deepClone(props.modelValue); + const item = items.splice(event.oldIndex, 1)[0]; + items.splice(event.newIndex, 0, item); + emit('update:modelValue', items); +} + +function detachMedia(id) { + if (props.detachMediaFn) { + props.detachMediaFn(id); + } else { + emit('detach', id); + } +} + +function toggleSensitive(file) { + os.api('drive/files/update', { + fileId: file.id, + isSensitive: !file.isSensitive, + }).then(() => { + emit('changeSensitive', file, !file.isSensitive); + }); +} +async function rename(file) { + const { canceled, result } = await os.inputText({ + title: i18n.ts.enterFileName, + default: file.name, + allowEmpty: false, + }); + if (canceled) return; + os.api('drive/files/update', { + fileId: file.id, + name: result, + }).then(() => { + emit('changeName', file, result); + file.name = result; + }); +} + +async function describe(file) { + os.popup(defineAsyncComponent(() => import('@/components/MkMediaCaption.vue')), { + title: i18n.ts.describeFile, + input: { + placeholder: i18n.ts.inputNewDescription, + default: file.comment !== null ? file.comment : '', }, - detachMediaFn: { - type: Function, - required: false, - }, - }, - - emits: ['updated', 'detach', 'changeSensitive', 'changeName'], - - data() { - return { - menu: null as Promise<null> | null, - }; - }, - - computed: { - _files: { - get() { - return this.files; - }, - set(value) { - this.$emit('updated', value); - }, - }, - }, - - methods: { - detachMedia(id) { - if (this.detachMediaFn) { - this.detachMediaFn(id); - } else { - this.$emit('detach', id); - } - }, - toggleSensitive(file) { + image: file, + }, { + done: result => { + if (!result || result.canceled) return; + let comment = result.result.length === 0 ? null : result.result; os.api('drive/files/update', { fileId: file.id, - isSensitive: !file.isSensitive, + comment: comment, }).then(() => { - this.$emit('changeSensitive', file, !file.isSensitive); - }); - }, - async rename(file) { - const { canceled, result } = await os.inputText({ - title: this.$ts.enterFileName, - default: file.name, - allowEmpty: false, - }); - if (canceled) return; - os.api('drive/files/update', { - fileId: file.id, - name: result, - }).then(() => { - this.$emit('changeName', file, result); - file.name = result; + file.comment = comment; }); }, + }, 'closed'); +} - async describe(file) { - os.popup(defineAsyncComponent(() => import('@/components/MkMediaCaption.vue')), { - title: this.$ts.describeFile, - input: { - placeholder: this.$ts.inputNewDescription, - default: file.comment !== null ? file.comment : '', - }, - image: file, - }, { - done: result => { - if (!result || result.canceled) return; - let comment = result.result.length === 0 ? null : result.result; - os.api('drive/files/update', { - fileId: file.id, - comment: comment, - }).then(() => { - file.comment = comment; - }); - }, - }, 'closed'); - }, - - showFileMenu(file, ev: MouseEvent) { - if (this.menu) return; - this.menu = os.popupMenu([{ - text: this.$ts.renameFile, - icon: 'ti ti-forms', - action: () => { this.rename(file); }, - }, { - text: file.isSensitive ? this.$ts.unmarkAsSensitive : this.$ts.markAsSensitive, - icon: file.isSensitive ? 'ti ti-eye-off' : 'ti ti-eye', - action: () => { this.toggleSensitive(file); }, - }, { - text: this.$ts.describeFile, - icon: 'ti ti-forms', - action: () => { this.describe(file); }, - }, { - text: this.$ts.attachCancel, - icon: 'ti ti-circle-x', - action: () => { this.detachMedia(file.id); }, - }], ev.currentTarget ?? ev.target).then(() => this.menu = null); - }, - }, -}); +function showFileMenu(file, ev: MouseEvent) { + if (menuShowing) return; + os.popupMenu([{ + text: i18n.ts.renameFile, + icon: 'ti ti-forms', + action: () => { rename(file); }, + }, { + text: file.isSensitive ? i18n.ts.unmarkAsSensitive : i18n.ts.markAsSensitive, + icon: file.isSensitive ? 'ti ti-eye-off' : 'ti ti-eye', + action: () => { toggleSensitive(file); }, + }, { + text: i18n.ts.describeFile, + icon: 'ti ti-forms', + action: () => { describe(file); }, + }, { + text: i18n.ts.attachCancel, + icon: 'ti ti-circle-x', + action: () => { detachMedia(file.id); }, + }], ev.currentTarget ?? ev.target).then(() => menuShowing = false); + menuShowing = true; +} </script> <style lang="scss" scoped> diff --git a/packages/client/src/components/MkWidgets.vue b/packages/client/src/components/MkWidgets.vue index dd2a2aa89a..17ffc8eb10 100644 --- a/packages/client/src/components/MkWidgets.vue +++ b/packages/client/src/components/MkWidgets.vue @@ -9,11 +9,11 @@ <MkButton inline primary class="mk-widget-add" @click="addWidget"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton> <MkButton inline @click="$emit('exit')">{{ i18n.ts.close }}</MkButton> </header> - <XDraggable - v-model="widgets_" + <Sortable + :list="props.widgets" item-key="id" - handle=".handle" - animation="150" + :options="{ handle: '.handle', animation: 150 }" + @end="onSorted" > <template #item="{element}"> <div class="customize-container"> @@ -24,7 +24,7 @@ </div> </div> </template> - </XDraggable> + </Sortable> </template> <component :is="`mkw-${widget.name}`" v-for="widget in widgets" v-else :key="widget.id" :ref="el => widgetRefs[widget.id] = el" class="widget" :widget="widget" @updateProps="updateWidget(widget.id, $event)" @contextmenu.stop="onContextmenu(widget, $event)"/> </div> @@ -38,8 +38,9 @@ import MkButton from '@/components/MkButton.vue'; import { widgets as widgetDefs } from '@/widgets'; import * as os from '@/os'; import { i18n } from '@/i18n'; +import { deepClone } from '@/scripts/clone'; -const XDraggable = defineAsyncComponent(() => import('vuedraggable')); +const Sortable = defineAsyncComponent(() => import('sortablejs-vue3').then(x => x.Sortable)); type Widget = { name: string; @@ -82,12 +83,13 @@ const removeWidget = (widget) => { const updateWidget = (id, data) => { emit('updateWidget', { id, data }); }; -const widgets_ = computed({ - get: () => props.widgets, - set: (value) => { - emit('updateWidgets', value); - }, -}); + +function onSorted(event) { + const items = deepClone(props.widgets); + const item = items.splice(event.oldIndex, 1)[0]; + items.splice(event.newIndex, 0, item); + emit('updateWidgets', items); +} function onContextmenu(widget: Widget, ev: MouseEvent) { const isLink = (el: HTMLElement) => { diff --git a/packages/client/src/pages/page-editor/page-editor.blocks.vue b/packages/client/src/pages/page-editor/page-editor.blocks.vue index dc363fe251..980e9d0e3c 100644 --- a/packages/client/src/pages/page-editor/page-editor.blocks.vue +++ b/packages/client/src/pages/page-editor/page-editor.blocks.vue @@ -1,9 +1,9 @@ <template> -<XDraggable v-model="blocks" tag="div" item-key="id" handle=".drag-handle" :group="{ name: 'blocks' }" animation="150" swap-threshold="0.5"> +<Sortable :list="blocks" tag="div" item-key="id" :options="{ handle: '.drag-handle', group: { name: 'blocks' }, animation: 150, swapThreshold: 0.5 }"> <template #item="{element}"> <component :is="'x-' + element.type" :value="element" :hpml="hpml" @update:value="updateItem" @remove="() => removeItem(element)"/> </template> -</XDraggable> +</Sortable> </template> <script lang="ts"> @@ -27,14 +27,14 @@ import * as os from '@/os'; export default defineComponent({ components: { - XDraggable: defineAsyncComponent(() => import('vuedraggable').then(x => x.default)), - XSection, XText, XImage, XButton, XTextarea, XTextInput, XTextareaInput, XNumberInput, XSwitch, XIf, XPost, XCounter, XRadioButton, XCanvas, XNote + Sortable: defineAsyncComponent(() => import('sortablejs-vue3').then(x => x.Sortable)), + XSection, XText, XImage, XButton, XTextarea, XTextInput, XTextareaInput, XNumberInput, XSwitch, XIf, XPost, XCounter, XRadioButton, XCanvas, XNote, }, props: { modelValue: { type: Array, - required: true + required: true, }, hpml: { required: true, @@ -50,8 +50,8 @@ export default defineComponent({ }, set(value) { this.$emit('update:modelValue', value); - } - } + }, + }, }, methods: { @@ -60,7 +60,7 @@ export default defineComponent({ const newValue = [ ...this.blocks.slice(0, i), v, - ...this.blocks.slice(i + 1) + ...this.blocks.slice(i + 1), ]; this.$emit('update:modelValue', newValue); }, @@ -69,10 +69,10 @@ export default defineComponent({ const i = this.blocks.findIndex(x => x.id === el.id); const newValue = [ ...this.blocks.slice(0, i), - ...this.blocks.slice(i + 1) + ...this.blocks.slice(i + 1), ]; this.$emit('update:modelValue', newValue); }, - } + }, }); </script> diff --git a/packages/client/src/pages/page-editor/page-editor.vue b/packages/client/src/pages/page-editor/page-editor.vue index 1230119c63..f8f5d9f6b0 100644 --- a/packages/client/src/pages/page-editor/page-editor.vue +++ b/packages/client/src/pages/page-editor/page-editor.vue @@ -54,7 +54,7 @@ <div v-else-if="tab === 'variables'"> <div class="qmuvgica"> - <XDraggable v-show="variables.length > 0" v-model="variables" tag="div" class="variables" item-key="name" handle=".drag-handle" :group="{ name: 'variables' }" animation="150" swap-threshold="0.5"> + <Sortable v-show="variables.length > 0" v-model="variables" tag="div" class="variables" item-key="name" handle=".drag-handle" :group="{ name: 'variables' }" animation="150" swap-threshold="0.5"> <template #item="{element}"> <XVariable :model-value="element" @@ -66,7 +66,7 @@ @remove="() => removeVariable(element)" /> </template> - </XDraggable> + </Sortable> <MkButton v-if="!readonly" class="add" @click="addVariable()"><i class="ti ti-plus"></i></MkButton> </div> @@ -107,7 +107,7 @@ import { mainRouter } from '@/router'; import { i18n } from '@/i18n'; import { definePageMetadata } from '@/scripts/page-metadata'; import { $i } from '@/account'; -const XDraggable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default)); +const Sortable = defineAsyncComponent(() => import('sortablejs-vue3').then(x => x.default)); const props = defineProps<{ initPageId?: string; @@ -186,24 +186,24 @@ function save() { if (pageId) { options.pageId = pageId; os.api('pages/update', options) - .then(page => { - currentName = name.trim(); - os.alert({ - type: 'success', - text: i18n.ts._pages.updated, - }); - }).catch(onError); + .then(page => { + currentName = name.trim(); + os.alert({ + type: 'success', + text: i18n.ts._pages.updated, + }); + }).catch(onError); } else { os.api('pages/create', options) - .then(created => { - pageId = created.id; - currentName = name.trim(); - os.alert({ - type: 'success', - text: i18n.ts._pages.created, - }); - mainRouter.push(`/pages/edit/${pageId}`); - }).catch(onError); + .then(created => { + pageId = created.id; + currentName = name.trim(); + os.alert({ + type: 'success', + text: i18n.ts._pages.created, + }); + mainRouter.push(`/pages/edit/${pageId}`); + }).catch(onError); } } @@ -438,7 +438,7 @@ definePageMetadata(computed(() => { return { title: title, icon: 'ti ti-pencil', - }; + }; })); </script> diff --git a/packages/client/src/pages/settings/reaction.vue b/packages/client/src/pages/settings/reaction.vue index 0dbe185504..2807684adb 100644 --- a/packages/client/src/pages/settings/reaction.vue +++ b/packages/client/src/pages/settings/reaction.vue @@ -3,7 +3,7 @@ <FromSlot class="_formBlock"> <template #label>{{ i18n.ts.reactionSettingDescription }}</template> <div v-panel style="border-radius: 6px;"> - <XDraggable v-model="reactions" class="zoaiodol" :item-key="item => item" animation="150" delay="100" delay-on-touch-only="true"> + <Sortable :list="reactions" class="zoaiodol" :item-key="item => item" :options="{ animation: 150, delay: 100, delayOnTouchOnly: true }" @end="onSorted"> <template #item="{element}"> <button class="_button item" @click="remove(element, $event)"> <MkEmoji :emoji="element" :normal="true"/> @@ -12,7 +12,9 @@ <template #footer> <button class="_button add" @click="chooseEmoji"><i class="ti ti-plus"></i></button> </template> - </XDraggable> + </Sortable> + <!-- TODO: https://github.com/MaxLeiter/sortablejs-vue3/issues/52 が実装されたら消す --> + <button class="_button add" @click="chooseEmoji"><i class="ti ti-plus"></i></button> </div> <template #caption>{{ i18n.ts.reactionSettingDescription2 }} <button class="_textButton" @click="preview">{{ i18n.ts.preview }}</button></template> </FromSlot> @@ -55,7 +57,7 @@ <script lang="ts" setup> import { defineAsyncComponent, watch } from 'vue'; -import XDraggable from 'vuedraggable'; +import { Sortable } from 'sortablejs-vue3'; import FormInput from '@/components/form/input.vue'; import FormRadios from '@/components/form/radios.vue'; import FromSlot from '@/components/form/slot.vue'; @@ -75,6 +77,11 @@ const reactionPickerWidth = $computed(defaultStore.makeGetterSetter('reactionPic const reactionPickerHeight = $computed(defaultStore.makeGetterSetter('reactionPickerHeight')); const reactionPickerUseDrawerForMobile = $computed(defaultStore.makeGetterSetter('reactionPickerUseDrawerForMobile')); +function onSorted(event) { + const item = reactions.splice(event.oldIndex, 1)[0]; + reactions.splice(event.newIndex, 0, item); +} + function save() { defaultStore.set('reactions', reactions); } diff --git a/yarn.lock b/yarn.lock index eb021a707c..ba1514ad04 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5193,6 +5193,8 @@ __metadata: s-age: 1.1.2 sass: 1.57.0 seedrandom: 3.0.5 + sortablejs: ^1.15.0 + sortablejs-vue3: ^1.2.3 start-server-and-test: 1.15.2 strict-event-emitter-types: 2.0.0 stringz: 2.1.0 @@ -5212,7 +5214,6 @@ __metadata: vue-eslint-parser: ^9.1.0 vue-prism-editor: 2.0.0-alpha.2 vue-tsc: ^1.0.14 - vuedraggable: 4.0.1 languageName: unknown linkType: soft @@ -15315,10 +15316,23 @@ __metadata: languageName: node linkType: hard -"sortablejs@npm:1.10.2": - version: 1.10.2 - resolution: "sortablejs@npm:1.10.2" - checksum: 37f8d47a9702b93c38077c5e0af90174dcf8e95cf96fe61a722033003eb293bdf3832e4a943f281eaedc433e24cd7d5a48a408706a71a21e75bc11ced0b358da +"sortablejs-vue3@npm:^1.2.3": + version: 1.2.3 + resolution: "sortablejs-vue3@npm:1.2.3" + dependencies: + sortablejs: ^1.15.0 + vue: ^3.2.37 + peerDependencies: + sortablejs: ^1.15.0 + vue: ^3.2.25 + checksum: 1cf069db4e950a9b447d98d6f4d233082f84b43ac88735e3aab07f2dcebee99026425ee5fc69b04893fe2bb9040fa8cda088a57205f7c46453043d32764b5b36 + languageName: node + linkType: hard + +"sortablejs@npm:^1.15.0": + version: 1.15.0 + resolution: "sortablejs@npm:1.15.0" + checksum: bb82223a663484640d317cad510ac987f26b7a443631040407224de1be069afcc6c39048b6d8527f10f269e33595e8128d7de2fac23517c8260470f77f932d55 languageName: node linkType: hard @@ -17163,7 +17177,7 @@ __metadata: languageName: node linkType: hard -"vue@npm:3.2.45": +"vue@npm:3.2.45, vue@npm:^3.2.37": version: 3.2.45 resolution: "vue@npm:3.2.45" dependencies: @@ -17176,17 +17190,6 @@ __metadata: languageName: node linkType: hard -"vuedraggable@npm:4.0.1": - version: 4.0.1 - resolution: "vuedraggable@npm:4.0.1" - dependencies: - sortablejs: 1.10.2 - peerDependencies: - vue: ^3.0.1 - checksum: 039e5d38560144299be3689270728639d041737b487cbb7dfec09b6c372b48804031785f9b40e6e14da0d213315b98ddc005713cc60a749cf98028e0c16fd866 - languageName: node - linkType: hard - "w3c-xmlserializer@npm:^4.0.0": version: 4.0.0 resolution: "w3c-xmlserializer@npm:4.0.0"