mirror of
https://github.com/misskey-dev/misskey.git
synced 2026-05-20 17:35:31 +02:00
enhance(frontend): remove vuedraggable (#17073)
* wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * Update page-editor.blocks.vue * Update MkDraggable.vue * refactor * refactor * ✌️ * refactor * Update MkDraggable.vue * ios * 🎨 * 🎨
This commit is contained in:
310
packages/frontend/src/components/MkDraggable.vue
Normal file
310
packages/frontend/src/components/MkDraggable.vue
Normal file
@@ -0,0 +1,310 @@
|
||||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<TransitionGroup
|
||||
tag="div"
|
||||
:enterActiveClass="$style.transition_items_enterActive"
|
||||
:leaveActiveClass="$style.transition_items_leaveActive"
|
||||
:enterFromClass="$style.transition_items_enterFrom"
|
||||
:leaveToClass="$style.transition_items_leaveTo"
|
||||
:moveClass="$style.transition_items_move"
|
||||
:class="[$style.items, { [$style.dragging]: dragging, [$style.horizontal]: direction === 'horizontal', [$style.vertical]: direction === 'vertical', [$style.withGaps]: withGaps, [$style.canNest]: canNest }]"
|
||||
>
|
||||
<slot name="header"></slot>
|
||||
<div
|
||||
v-if="modelValue.length === 0"
|
||||
:class="$style.emptyDropArea"
|
||||
@dragover.prevent.stop="() => {}"
|
||||
@dragleave="() => {}"
|
||||
@drop.prevent.stop="onEmptyDrop($event)"
|
||||
>
|
||||
</div>
|
||||
<div
|
||||
v-for="(item, i) in modelValue"
|
||||
:key="item.id"
|
||||
:class="$style.item"
|
||||
:draggable="!manualDragStart"
|
||||
@dragstart.stop="onDragstart($event, item)"
|
||||
>
|
||||
<div
|
||||
:class="[$style.forwardArea, { [$style.dropReady]: dropReadyArea[0] === item.id && dropReadyArea[1] === 'forward' }]"
|
||||
@dragover.prevent.stop="onDragover($event, item, false)"
|
||||
@dragleave="onDragleave($event, item)"
|
||||
@drop.prevent.stop="onDrop($event, item, false)"
|
||||
></div>
|
||||
<div style="position: relative; z-index: 0;">
|
||||
<slot :item="item" :index="i" :dragStart="(ev) => onDragstart(ev, item)"></slot>
|
||||
</div>
|
||||
<div
|
||||
:class="[$style.backwardArea, { [$style.dropReady]: dropReadyArea[0] === item.id && dropReadyArea[1] === 'backward' }]"
|
||||
@dragover.prevent.stop="onDragover($event, item, true)"
|
||||
@dragleave="onDragleave($event, item)"
|
||||
@drop.prevent.stop="onDrop($event, item, true)"
|
||||
></div>
|
||||
</div>
|
||||
<slot name="footer"></slot>
|
||||
</TransitionGroup>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { ref } from 'vue';
|
||||
|
||||
// 別々のコンポーネントインスタンス間でD&Dを融通するためにグローバルに状態を持っておく必要がある
|
||||
const dragging = ref(false);
|
||||
let dropCallback: ((targetInstanceId: string) => void) | null = null;
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup generic="T extends { id: string; }">
|
||||
import { nextTick } from 'vue';
|
||||
import { getDragData, setDragData } from '@/drag-and-drop.js';
|
||||
import { genId } from '@/utility/id.js';
|
||||
|
||||
const slots = defineSlots<{
|
||||
default(props: { item: T; index: number; dragStart: (ev: DragEvent) => void }): any;
|
||||
header(): any;
|
||||
footer(): any;
|
||||
}>();
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
modelValue: T[];
|
||||
direction: 'horizontal' | 'vertical';
|
||||
group?: string | null;
|
||||
manualDragStart?: boolean;
|
||||
withGaps?: boolean;
|
||||
canNest?: boolean;
|
||||
}>(), {
|
||||
group: null,
|
||||
manualDragStart: false,
|
||||
withGaps: false,
|
||||
canNest: false,
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'update:modelValue', value: T[]): void;
|
||||
}>();
|
||||
|
||||
const dropReadyArea = ref<[T['id'] | null, 'forward' | 'backward' | null]>([null, null]);
|
||||
const instanceId = genId();
|
||||
const group = props.group ?? instanceId;
|
||||
|
||||
function onDragstart(ev: DragEvent, item: T) {
|
||||
if (ev.dataTransfer == null) return;
|
||||
ev.dataTransfer.effectAllowed = 'move';
|
||||
setDragData(ev, 'MkDraggable', { item, instanceId, group });
|
||||
|
||||
const target = ev.target as HTMLElement;
|
||||
target.addEventListener('dragend', (ev) => {
|
||||
dragging.value = false;
|
||||
dropReadyArea.value = [null, null];
|
||||
}, { once: true });
|
||||
|
||||
dropCallback = (targetInstanceId) => {
|
||||
if (targetInstanceId === instanceId) return;
|
||||
const newValue = props.modelValue.filter(x => x.id !== item.id);
|
||||
emit('update:modelValue', newValue);
|
||||
};
|
||||
|
||||
// Chromeのバグで、Dragstartハンドラ内ですぐにDOMを変更する(=リアクティブなプロパティを変更する)とDragが終了してしまう
|
||||
// SEE: https://stackoverflow.com/questions/19639969/html5-dragend-event-firing-immediately
|
||||
window.setTimeout(() => {
|
||||
dragging.value = true;
|
||||
}, 10);
|
||||
}
|
||||
|
||||
function onDragover(ev: DragEvent, item: T, backward: boolean) {
|
||||
nextTick(() => {
|
||||
dropReadyArea.value = [item.id, backward ? 'backward' : 'forward'];
|
||||
});
|
||||
}
|
||||
|
||||
function onDragleave(ev: DragEvent, item: T) {
|
||||
dropReadyArea.value = [null, null];
|
||||
}
|
||||
|
||||
function onDrop(ev: DragEvent, item: T, backward: boolean) {
|
||||
const dragged = getDragData(ev, 'MkDraggable');
|
||||
dropReadyArea.value = [null, null];
|
||||
if (dragged == null || dragged.group !== group || dragged.item.id === item.id) return;
|
||||
dropCallback?.(instanceId);
|
||||
|
||||
const fromIndex = props.modelValue.findIndex(x => x.id === dragged.item.id);
|
||||
let toIndex = props.modelValue.findIndex(x => x.id === item.id);
|
||||
|
||||
const newValue = [...props.modelValue];
|
||||
if (fromIndex > -1) newValue.splice(fromIndex, 1);
|
||||
toIndex = newValue.findIndex(x => x.id === item.id);
|
||||
if (backward) toIndex += 1;
|
||||
newValue.splice(toIndex, 0, dragged.item as T);
|
||||
|
||||
emit('update:modelValue', newValue);
|
||||
}
|
||||
|
||||
function onEmptyDrop(ev: DragEvent) {
|
||||
const dragged = getDragData(ev, 'MkDraggable');
|
||||
if (dragged == null) return;
|
||||
dropCallback?.(instanceId);
|
||||
|
||||
emit('update:modelValue', [dragged.item as T]);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.transition_items_move,
|
||||
.transition_items_enterActive,
|
||||
.transition_items_leaveActive {
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
.transition_items_enterFrom,
|
||||
.transition_items_leaveTo {
|
||||
opacity: 0;
|
||||
}
|
||||
.transition_items_leaveActive {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.items {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: left;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.items.horizontal {
|
||||
flex-direction: row;
|
||||
}
|
||||
.items.vertical {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.item {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.items.vertical .item {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.items.horizontal.withGaps {
|
||||
row-gap: var(--MI-margin);
|
||||
}
|
||||
|
||||
.items.horizontal.withGaps .item {
|
||||
padding-left: calc(var(--MI-margin) / 2);
|
||||
padding-right: calc(var(--MI-margin) / 2);
|
||||
}
|
||||
|
||||
.items.vertical.withGaps .item {
|
||||
padding-top: calc(var(--MI-margin) / 2);
|
||||
padding-bottom: calc(var(--MI-margin) / 2);
|
||||
}
|
||||
|
||||
.forwardArea, .backwardArea {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.items.dragging {
|
||||
.forwardArea, .backwardArea {
|
||||
pointer-events: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.items.horizontal {
|
||||
.forwardArea {
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 50%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.backwardArea {
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 50%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.items.vertical {
|
||||
.forwardArea {
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 50%;
|
||||
}
|
||||
|
||||
.backwardArea {
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
.items.canNest.horizontal {
|
||||
.forwardArea, .backwardArea {
|
||||
width: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
.items.canNest.vertical {
|
||||
.forwardArea, .backwardArea {
|
||||
height: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
.dropReady::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
z-index: 99999;
|
||||
background: var(--MI_THEME-accent);
|
||||
border-radius: 999px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.items.horizontal {
|
||||
.forwardArea.dropReady::before {
|
||||
top: 0;
|
||||
left: -1px;
|
||||
width: 2px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.backwardArea.dropReady::before {
|
||||
top: 0;
|
||||
right: -1px;
|
||||
width: 2px;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.items.vertical {
|
||||
.forwardArea.dropReady::before {
|
||||
top: -1px;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
}
|
||||
|
||||
.backwardArea.dropReady::before {
|
||||
bottom: -1px;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.items.horizontal .emptyDropArea {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.items.vertical .emptyDropArea {
|
||||
width: 100%;
|
||||
height: 50px;
|
||||
}
|
||||
</style>
|
||||
@@ -5,23 +5,30 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
<template>
|
||||
<div v-show="props.modelValue.length != 0" :class="$style.root">
|
||||
<Sortable :modelValue="props.modelValue" :class="$style.files" itemKey="id" :animation="150" :delay="100" :delayOnTouchOnly="true" @update:modelValue="v => emit('update:modelValue', v)">
|
||||
<template #item="{ element }">
|
||||
<MkDraggable
|
||||
:modelValue="props.modelValue"
|
||||
:class="$style.files"
|
||||
direction="horizontal"
|
||||
withGaps
|
||||
@update:modelValue="v => emit('update:modelValue', v)"
|
||||
>
|
||||
<template #default="{ item }">
|
||||
<div
|
||||
:class="$style.file"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click="showFileMenu(element, $event)"
|
||||
@keydown.space.enter="showFileMenu(element, $event)"
|
||||
@contextmenu.prevent="showFileMenu(element, $event)"
|
||||
@click="showFileMenu(item, $event)"
|
||||
@keydown.space.enter="showFileMenu(item, $event)"
|
||||
@contextmenu.prevent="showFileMenu(item, $event)"
|
||||
>
|
||||
<MkDriveFileThumbnail :data-id="element.id" :class="$style.thumbnail" :file="element" fit="cover"/>
|
||||
<div v-if="element.isSensitive" :class="$style.sensitive">
|
||||
<!-- pointer-eventsをnoneにしておかないとiOSなどでドラッグしたときに画像の方に判定が持ってかれる -->
|
||||
<MkDriveFileThumbnail style="pointer-events: none;" :data-id="item.id" :class="$style.thumbnail" :file="item" fit="cover"/>
|
||||
<div v-if="item.isSensitive" :class="$style.sensitive" style="pointer-events: none;">
|
||||
<i class="ti ti-eye-exclamation" style="margin: auto;"></i>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Sortable>
|
||||
</MkDraggable>
|
||||
<p
|
||||
:class="[$style.remain, {
|
||||
[$style.exceeded]: props.modelValue.length > 16,
|
||||
@@ -33,11 +40,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { defineAsyncComponent, inject } from 'vue';
|
||||
import { inject } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import type { MenuItem } from '@/types/menu';
|
||||
import { copyToClipboard } from '@/utility/copy-to-clipboard';
|
||||
import MkDriveFileThumbnail from '@/components/MkDriveFileThumbnail.vue';
|
||||
import MkDraggable from '@/components/MkDraggable.vue';
|
||||
import * as os from '@/os.js';
|
||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
@@ -45,8 +53,6 @@ import { prefer } from '@/preferences.js';
|
||||
import { DI } from '@/di.js';
|
||||
import { globalEvents } from '@/events.js';
|
||||
|
||||
const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default));
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: Misskey.entities.DriveFile[];
|
||||
detachMediaFn?: (id: string) => void;
|
||||
@@ -221,7 +227,6 @@ function showFileMenu(file: Misskey.entities.DriveFile, ev: MouseEvent | Keyboar
|
||||
position: relative;
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
margin-right: 4px;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
cursor: move;
|
||||
|
||||
@@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div :class="$style.root">
|
||||
<div :class="$style.root" class="_gaps_s">
|
||||
<template v-if="edit">
|
||||
<header :class="$style.editHeader">
|
||||
<MkSelect v-model="widgetAdderSelected" :items="widgetAdderSelectedDef" style="margin-bottom: var(--MI-margin)" data-cy-widget-select>
|
||||
@@ -13,25 +13,21 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<MkButton inline primary data-cy-widget-add @click="addWidget"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton>
|
||||
<MkButton inline @click="emit('exit')">{{ i18n.ts.close }}</MkButton>
|
||||
</header>
|
||||
<Sortable
|
||||
<MkDraggable
|
||||
:modelValue="props.widgets"
|
||||
itemKey="id"
|
||||
handle=".handle"
|
||||
:animation="150"
|
||||
:group="{ name: 'SortableMkWidgets' }"
|
||||
:class="$style.editEditing"
|
||||
direction="vertical"
|
||||
withGaps
|
||||
group="MkWidgets"
|
||||
@update:modelValue="v => emit('updateWidgets', v)"
|
||||
>
|
||||
<template #item="{element}">
|
||||
<template #default="{ item }">
|
||||
<div :class="[$style.widget, $style.customizeContainer]" data-cy-customize-container>
|
||||
<button :class="$style.customizeContainerConfig" class="_button" @click.prevent.stop="configWidget(element.id)"><i class="ti ti-settings"></i></button>
|
||||
<button :class="$style.customizeContainerRemove" data-cy-customize-container-remove class="_button" @click.prevent.stop="removeWidget(element)"><i class="ti ti-x"></i></button>
|
||||
<div class="handle">
|
||||
<component :is="`widget-${element.name}`" :ref="el => widgetRefs[element.id] = el" class="widget" :class="$style.customizeContainerHandleWidget" :widget="element" @updateProps="updateWidget(element.id, $event)"/>
|
||||
</div>
|
||||
<button :class="$style.customizeContainerConfig" class="_button" @click.prevent.stop="configWidget(item.id)"><i class="ti ti-settings"></i></button>
|
||||
<button :class="$style.customizeContainerRemove" data-cy-customize-container-remove class="_button" @click.prevent.stop="removeWidget(item)"><i class="ti ti-x"></i></button>
|
||||
<component :is="`widget-${item.name}`" :ref="el => widgetRefs[item.id] = el" :class="$style.customizeContainerHandleWidget" :widget="item" @updateProps="updateWidget(item.id, $event)"/>
|
||||
</div>
|
||||
</template>
|
||||
</Sortable>
|
||||
</MkDraggable>
|
||||
</template>
|
||||
<component :is="`widget-${widget.name}`" v-for="widget in _widgets" v-else :key="widget.id" :ref="el => widgetRefs[widget.id] = el" :class="$style.widget" :widget="widget" @updateProps="updateWidget(widget.id, $event)" @contextmenu.stop="onContextmenu(widget, $event)"/>
|
||||
</div>
|
||||
@@ -49,19 +45,18 @@ export type DefaultStoredWidget = {
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { defineAsyncComponent, ref, computed } from 'vue';
|
||||
import { computed } from 'vue';
|
||||
import { isLink } from '@@/js/is-link.js';
|
||||
import { genId } from '@/utility/id.js';
|
||||
import MkSelect from '@/components/MkSelect.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkDraggable from '@/components/MkDraggable.vue';
|
||||
import { widgets as widgetDefs, federationWidgets } from '@/widgets/index.js';
|
||||
import * as os from '@/os.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { instance } from '@/instance.js';
|
||||
import { useMkSelect } from '@/composables/use-mkselect.js';
|
||||
|
||||
const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default));
|
||||
|
||||
const props = defineProps<{
|
||||
widgets: Widget[];
|
||||
edit: boolean;
|
||||
@@ -142,11 +137,6 @@ function onContextmenu(widget: Widget, ev: MouseEvent) {
|
||||
|
||||
.widget {
|
||||
contain: content;
|
||||
margin: var(--MI-margin) 0;
|
||||
|
||||
&:first-of-type {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.edit {
|
||||
@@ -158,10 +148,6 @@ function onContextmenu(widget: Widget, ev: MouseEvent) {
|
||||
padding: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
&Editing {
|
||||
min-height: 100px;
|
||||
}
|
||||
}
|
||||
|
||||
.customizeContainer {
|
||||
|
||||
Reference in New Issue
Block a user