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:
parent
e18b92823f
commit
8c5572dd3b
|
|
@ -75,7 +75,6 @@
|
||||||
"v-code-diff": "1.13.1",
|
"v-code-diff": "1.13.1",
|
||||||
"vite": "7.3.0",
|
"vite": "7.3.0",
|
||||||
"vue": "3.5.26",
|
"vue": "3.5.26",
|
||||||
"vuedraggable": "next",
|
|
||||||
"wanakana": "5.3.1"
|
"wanakana": "5.3.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
|
||||||
|
|
@ -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>
|
<template>
|
||||||
<div v-show="props.modelValue.length != 0" :class="$style.root">
|
<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)">
|
<MkDraggable
|
||||||
<template #item="{ element }">
|
:modelValue="props.modelValue"
|
||||||
|
:class="$style.files"
|
||||||
|
direction="horizontal"
|
||||||
|
withGaps
|
||||||
|
@update:modelValue="v => emit('update:modelValue', v)"
|
||||||
|
>
|
||||||
|
<template #default="{ item }">
|
||||||
<div
|
<div
|
||||||
:class="$style.file"
|
:class="$style.file"
|
||||||
role="button"
|
role="button"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
@click="showFileMenu(element, $event)"
|
@click="showFileMenu(item, $event)"
|
||||||
@keydown.space.enter="showFileMenu(element, $event)"
|
@keydown.space.enter="showFileMenu(item, $event)"
|
||||||
@contextmenu.prevent="showFileMenu(element, $event)"
|
@contextmenu.prevent="showFileMenu(item, $event)"
|
||||||
>
|
>
|
||||||
<MkDriveFileThumbnail :data-id="element.id" :class="$style.thumbnail" :file="element" fit="cover"/>
|
<!-- pointer-eventsをnoneにしておかないとiOSなどでドラッグしたときに画像の方に判定が持ってかれる -->
|
||||||
<div v-if="element.isSensitive" :class="$style.sensitive">
|
<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>
|
<i class="ti ti-eye-exclamation" style="margin: auto;"></i>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</Sortable>
|
</MkDraggable>
|
||||||
<p
|
<p
|
||||||
:class="[$style.remain, {
|
:class="[$style.remain, {
|
||||||
[$style.exceeded]: props.modelValue.length > 16,
|
[$style.exceeded]: props.modelValue.length > 16,
|
||||||
|
|
@ -33,11 +40,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { defineAsyncComponent, inject } from 'vue';
|
import { inject } from 'vue';
|
||||||
import * as Misskey from 'misskey-js';
|
import * as Misskey from 'misskey-js';
|
||||||
import type { MenuItem } from '@/types/menu';
|
import type { MenuItem } from '@/types/menu';
|
||||||
import { copyToClipboard } from '@/utility/copy-to-clipboard';
|
import { copyToClipboard } from '@/utility/copy-to-clipboard';
|
||||||
import MkDriveFileThumbnail from '@/components/MkDriveFileThumbnail.vue';
|
import MkDriveFileThumbnail from '@/components/MkDriveFileThumbnail.vue';
|
||||||
|
import MkDraggable from '@/components/MkDraggable.vue';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
|
|
@ -45,8 +53,6 @@ import { prefer } from '@/preferences.js';
|
||||||
import { DI } from '@/di.js';
|
import { DI } from '@/di.js';
|
||||||
import { globalEvents } from '@/events.js';
|
import { globalEvents } from '@/events.js';
|
||||||
|
|
||||||
const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default));
|
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
modelValue: Misskey.entities.DriveFile[];
|
modelValue: Misskey.entities.DriveFile[];
|
||||||
detachMediaFn?: (id: string) => void;
|
detachMediaFn?: (id: string) => void;
|
||||||
|
|
@ -221,7 +227,6 @@ function showFileMenu(file: Misskey.entities.DriveFile, ev: MouseEvent | Keyboar
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 64px;
|
width: 64px;
|
||||||
height: 64px;
|
height: 64px;
|
||||||
margin-right: 4px;
|
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
cursor: move;
|
cursor: move;
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div :class="$style.root">
|
<div :class="$style.root" class="_gaps_s">
|
||||||
<template v-if="edit">
|
<template v-if="edit">
|
||||||
<header :class="$style.editHeader">
|
<header :class="$style.editHeader">
|
||||||
<MkSelect v-model="widgetAdderSelected" :items="widgetAdderSelectedDef" style="margin-bottom: var(--MI-margin)" data-cy-widget-select>
|
<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 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>
|
<MkButton inline @click="emit('exit')">{{ i18n.ts.close }}</MkButton>
|
||||||
</header>
|
</header>
|
||||||
<Sortable
|
<MkDraggable
|
||||||
:modelValue="props.widgets"
|
:modelValue="props.widgets"
|
||||||
itemKey="id"
|
direction="vertical"
|
||||||
handle=".handle"
|
withGaps
|
||||||
:animation="150"
|
group="MkWidgets"
|
||||||
:group="{ name: 'SortableMkWidgets' }"
|
|
||||||
:class="$style.editEditing"
|
|
||||||
@update:modelValue="v => emit('updateWidgets', v)"
|
@update:modelValue="v => emit('updateWidgets', v)"
|
||||||
>
|
>
|
||||||
<template #item="{element}">
|
<template #default="{ item }">
|
||||||
<div :class="[$style.widget, $style.customizeContainer]" data-cy-customize-container>
|
<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.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(element)"><i class="ti ti-x"></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>
|
||||||
<div class="handle">
|
<component :is="`widget-${item.name}`" :ref="el => widgetRefs[item.id] = el" :class="$style.customizeContainerHandleWidget" :widget="item" @updateProps="updateWidget(item.id, $event)"/>
|
||||||
<component :is="`widget-${element.name}`" :ref="el => widgetRefs[element.id] = el" class="widget" :class="$style.customizeContainerHandleWidget" :widget="element" @updateProps="updateWidget(element.id, $event)"/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</Sortable>
|
</MkDraggable>
|
||||||
</template>
|
</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)"/>
|
<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>
|
</div>
|
||||||
|
|
@ -49,19 +45,18 @@ export type DefaultStoredWidget = {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { defineAsyncComponent, ref, computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
import { isLink } from '@@/js/is-link.js';
|
import { isLink } from '@@/js/is-link.js';
|
||||||
import { genId } from '@/utility/id.js';
|
import { genId } from '@/utility/id.js';
|
||||||
import MkSelect from '@/components/MkSelect.vue';
|
import MkSelect from '@/components/MkSelect.vue';
|
||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from '@/components/MkButton.vue';
|
||||||
|
import MkDraggable from '@/components/MkDraggable.vue';
|
||||||
import { widgets as widgetDefs, federationWidgets } from '@/widgets/index.js';
|
import { widgets as widgetDefs, federationWidgets } from '@/widgets/index.js';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { instance } from '@/instance.js';
|
import { instance } from '@/instance.js';
|
||||||
import { useMkSelect } from '@/composables/use-mkselect.js';
|
import { useMkSelect } from '@/composables/use-mkselect.js';
|
||||||
|
|
||||||
const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default));
|
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
widgets: Widget[];
|
widgets: Widget[];
|
||||||
edit: boolean;
|
edit: boolean;
|
||||||
|
|
@ -142,11 +137,6 @@ function onContextmenu(widget: Widget, ev: MouseEvent) {
|
||||||
|
|
||||||
.widget {
|
.widget {
|
||||||
contain: content;
|
contain: content;
|
||||||
margin: var(--MI-margin) 0;
|
|
||||||
|
|
||||||
&:first-of-type {
|
|
||||||
margin-top: 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.edit {
|
.edit {
|
||||||
|
|
@ -158,10 +148,6 @@ function onContextmenu(widget: Widget, ev: MouseEvent) {
|
||||||
padding: 4px;
|
padding: 4px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&Editing {
|
|
||||||
min-height: 100px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.customizeContainer {
|
.customizeContainer {
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ type DragDataMap = {
|
||||||
driveFiles: Misskey.entities.DriveFile[];
|
driveFiles: Misskey.entities.DriveFile[];
|
||||||
driveFolders: Misskey.entities.DriveFolder[];
|
driveFolders: Misskey.entities.DriveFolder[];
|
||||||
deckColumn: string;
|
deckColumn: string;
|
||||||
|
MkDraggable: { item: { id: string }; instanceId: string; group: string; };
|
||||||
};
|
};
|
||||||
|
|
||||||
// NOTE: dataTransfer の format は大文字小文字区別されないっぽいので toLowerCase が必要
|
// NOTE: dataTransfer の format は大文字小文字区別されないっぽいので toLowerCase が必要
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<div :class="$style.header">
|
<div :class="$style.header">
|
||||||
<MkSelect v-model="type" :items="typeDef" :class="$style.typeSelect">
|
<MkSelect v-model="type" :items="typeDef" :class="$style.typeSelect">
|
||||||
</MkSelect>
|
</MkSelect>
|
||||||
<button v-if="draggable" class="drag-handle _button" :class="$style.dragHandle">
|
<button v-if="draggable" class="_button" :class="$style.dragHandle" :draggable="true" @dragstart.stop="dragStartCallback">
|
||||||
<i class="ti ti-menu-2"></i>
|
<i class="ti ti-menu-2"></i>
|
||||||
</button>
|
</button>
|
||||||
<button v-if="draggable" class="_button" :class="$style.remove" @click="removeSelf">
|
<button v-if="draggable" class="_button" :class="$style.remove" @click="removeSelf">
|
||||||
|
|
@ -17,14 +17,27 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="type === 'and' || type === 'or'" class="_gaps">
|
<div v-if="type === 'and' || type === 'or'" class="_gaps">
|
||||||
<Sortable v-model="v.values" tag="div" class="_gaps" itemKey="id" handle=".drag-handle" :group="{ name: 'roleFormula' }" :animation="150" :swapThreshold="0.5">
|
<MkDraggable
|
||||||
<template #item="{element}">
|
v-model="v.values"
|
||||||
|
direction="vertical"
|
||||||
|
withGaps
|
||||||
|
canNest
|
||||||
|
manualDragStart
|
||||||
|
group="roleFormula"
|
||||||
|
>
|
||||||
|
<template #default="{ item, dragStart }">
|
||||||
<div :class="$style.item">
|
<div :class="$style.item">
|
||||||
<!-- divが無いとエラーになる https://github.com/SortableJS/vue.draggable.next/issues/189 -->
|
<!-- divが無いとエラーになる https://github.com/SortableJS/vue.draggable.next/issues/189 -->
|
||||||
<RolesEditorFormula :modelValue="element" draggable @update:modelValue="updated => valuesItemUpdated(updated)" @remove="removeItem(element)"/>
|
<RolesEditorFormula
|
||||||
|
:modelValue="item"
|
||||||
|
:dragStartCallback="dragStart"
|
||||||
|
draggable
|
||||||
|
@update:modelValue="updated => valuesItemUpdated(updated)"
|
||||||
|
@remove="removeItem(item.id)"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</Sortable>
|
</MkDraggable>
|
||||||
<MkButton rounded style="margin: 0 auto;" @click="addValue"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton>
|
<MkButton rounded style="margin: 0 auto;" @click="addValue"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -45,18 +58,17 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, defineAsyncComponent, ref, watch } from 'vue';
|
import { computed, ref, watch } from 'vue';
|
||||||
|
import type { GetMkSelectValueTypesFromDef, MkSelectItem } from '@/components/MkSelect.vue';
|
||||||
import { genId } from '@/utility/id.js';
|
import { genId } from '@/utility/id.js';
|
||||||
import MkInput from '@/components/MkInput.vue';
|
import MkInput from '@/components/MkInput.vue';
|
||||||
import MkSelect from '@/components/MkSelect.vue';
|
import MkSelect from '@/components/MkSelect.vue';
|
||||||
import type { GetMkSelectValueTypesFromDef, MkSelectItem } from '@/components/MkSelect.vue';
|
|
||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from '@/components/MkButton.vue';
|
||||||
|
import MkDraggable from '@/components/MkDraggable.vue';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { deepClone } from '@/utility/clone.js';
|
import { deepClone } from '@/utility/clone.js';
|
||||||
import { rolesCache } from '@/cache.js';
|
import { rolesCache } from '@/cache.js';
|
||||||
|
|
||||||
const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default));
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(ev: 'update:modelValue', value: any): void;
|
(ev: 'update:modelValue', value: any): void;
|
||||||
(ev: 'remove'): void;
|
(ev: 'remove'): void;
|
||||||
|
|
@ -65,6 +77,7 @@ const emit = defineEmits<{
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
modelValue: any;
|
modelValue: any;
|
||||||
draggable?: boolean;
|
draggable?: boolean;
|
||||||
|
dragStartCallback?: (ev: DragEvent) => void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const v = ref(deepClone(props.modelValue));
|
const v = ref(deepClone(props.modelValue));
|
||||||
|
|
@ -132,8 +145,8 @@ function valuesItemUpdated(item) {
|
||||||
v.value.values[i] = item;
|
v.value.values[i] = item;
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeItem(item) {
|
function removeItem(itemId) {
|
||||||
v.value.values = v.value.values.filter(_item => _item.id !== item.id);
|
v.value.values = v.value.values.filter(_item => _item.id !== itemId);
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeSelf() {
|
function removeSelf() {
|
||||||
|
|
|
||||||
|
|
@ -12,28 +12,25 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<div class="_gaps_m">
|
<div class="_gaps_m">
|
||||||
<div><SearchText>{{ i18n.ts._serverRules.description }}</SearchText></div>
|
<div><SearchText>{{ i18n.ts._serverRules.description }}</SearchText></div>
|
||||||
|
|
||||||
<Sortable
|
<MkDraggable
|
||||||
v-model="serverRules"
|
v-model="serverRules"
|
||||||
class="_gaps_m"
|
direction="vertical"
|
||||||
:itemKey="(_, i) => i"
|
withGaps
|
||||||
:animation="150"
|
manualDragStart
|
||||||
:handle="'.' + $style.itemHandle"
|
|
||||||
@start="e => e.item.classList.add('active')"
|
|
||||||
@end="e => e.item.classList.remove('active')"
|
|
||||||
>
|
>
|
||||||
<template #item="{element,index}">
|
<template #default="{ item, index, dragStart }">
|
||||||
<div :class="$style.item">
|
<div :class="$style.item">
|
||||||
<div :class="$style.itemHeader">
|
<div :class="$style.itemHeader">
|
||||||
<div :class="$style.itemNumber" v-text="String(index + 1)"/>
|
<div :class="$style.itemNumber" v-text="String(index + 1)"/>
|
||||||
<span :class="$style.itemHandle"><i class="ti ti-menu"/></span>
|
<span :class="$style.itemHandle" :draggable="true" @dragstart.stop="dragStart"><i class="ti ti-menu"/></span>
|
||||||
<button class="_button" :class="$style.itemRemove" @click="remove(index)"><i class="ti ti-x"></i></button>
|
<button class="_button" :class="$style.itemRemove" @click="remove(item.id)"><i class="ti ti-x"></i></button>
|
||||||
</div>
|
</div>
|
||||||
<MkInput v-model="serverRules[index]"/>
|
<MkInput :modelValue="item.text" @update:modelValue="serverRules[index].text = $event"/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</Sortable>
|
</MkDraggable>
|
||||||
<div :class="$style.commands">
|
<div :class="$style.commands">
|
||||||
<MkButton rounded @click="serverRules.push('')"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton>
|
<MkButton rounded @click="add"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton>
|
||||||
<MkButton primary rounded @click="save"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton>
|
<MkButton primary rounded @click="save"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -42,28 +39,31 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { defineAsyncComponent, ref, computed } from 'vue';
|
import { ref } from 'vue';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
import { fetchInstance, instance } from '@/instance.js';
|
import { fetchInstance, instance } from '@/instance.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from '@/components/MkButton.vue';
|
||||||
import MkInput from '@/components/MkInput.vue';
|
import MkInput from '@/components/MkInput.vue';
|
||||||
import MkFolder from '@/components/MkFolder.vue';
|
import MkFolder from '@/components/MkFolder.vue';
|
||||||
|
import MkDraggable from '@/components/MkDraggable.vue';
|
||||||
|
|
||||||
const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default));
|
const serverRules = ref<{ text: string; id: string; }[]>(instance.serverRules.map(text => ({ text, id: Math.random().toString() })));
|
||||||
|
|
||||||
const serverRules = ref<string[]>(instance.serverRules);
|
async function save() {
|
||||||
|
|
||||||
const save = async () => {
|
|
||||||
await os.apiWithDialog('admin/update-meta', {
|
await os.apiWithDialog('admin/update-meta', {
|
||||||
serverRules: serverRules.value,
|
serverRules: serverRules.value.map(r => r.text),
|
||||||
});
|
});
|
||||||
fetchInstance(true);
|
fetchInstance(true);
|
||||||
};
|
}
|
||||||
|
|
||||||
const remove = (index: number): void => {
|
function add(): void {
|
||||||
serverRules.value.splice(index, 1);
|
serverRules.value.push({ text: '', id: Math.random().toString() });
|
||||||
};
|
}
|
||||||
|
|
||||||
|
function remove(id: string): void {
|
||||||
|
serverRules.value = serverRules.value.filter(r => r.id !== id);
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" module>
|
<style lang="scss" module>
|
||||||
|
|
|
||||||
|
|
@ -41,20 +41,19 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<div class="_gaps">
|
<div class="_gaps">
|
||||||
<MkButton primary rounded @click="addPinnedNote()"><i class="ti ti-plus"></i></MkButton>
|
<MkButton primary rounded @click="addPinnedNote()"><i class="ti ti-plus"></i></MkButton>
|
||||||
|
|
||||||
<Sortable
|
<MkDraggable
|
||||||
v-model="pinnedNotes"
|
:modelValue="pinnedNoteIds.map(id => ({ id }))"
|
||||||
itemKey="id"
|
direction="vertical"
|
||||||
:handle="'.' + $style.pinnedNoteHandle"
|
@update:modelValue="v => pinnedNoteIds = v.map(x => x.id)"
|
||||||
:animation="150"
|
|
||||||
>
|
>
|
||||||
<template #item="{element,index}">
|
<template #default="{ item }">
|
||||||
<div :class="$style.pinnedNote">
|
<div :class="$style.pinnedNote">
|
||||||
<button class="_button" :class="$style.pinnedNoteHandle"><i class="ti ti-menu"></i></button>
|
<button class="_button" :class="$style.pinnedNoteHandle"><i class="ti ti-menu"></i></button>
|
||||||
{{ element.id }}
|
{{ item.id }}
|
||||||
<button class="_button" :class="$style.pinnedNoteRemove" @click="removePinnedNote(index)"><i class="ti ti-x"></i></button>
|
<button class="_button" :class="$style.pinnedNoteRemove" @click="removePinnedNote(item.id)"><i class="ti ti-x"></i></button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</Sortable>
|
</MkDraggable>
|
||||||
</div>
|
</div>
|
||||||
</MkFolder>
|
</MkFolder>
|
||||||
|
|
||||||
|
|
@ -68,7 +67,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, ref, watch, defineAsyncComponent } from 'vue';
|
import { computed, ref, watch } from 'vue';
|
||||||
import * as Misskey from 'misskey-js';
|
import * as Misskey from 'misskey-js';
|
||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from '@/components/MkButton.vue';
|
||||||
import MkInput from '@/components/MkInput.vue';
|
import MkInput from '@/components/MkInput.vue';
|
||||||
|
|
@ -81,10 +80,9 @@ import { i18n } from '@/i18n.js';
|
||||||
import MkFolder from '@/components/MkFolder.vue';
|
import MkFolder from '@/components/MkFolder.vue';
|
||||||
import MkSwitch from '@/components/MkSwitch.vue';
|
import MkSwitch from '@/components/MkSwitch.vue';
|
||||||
import MkTextarea from '@/components/MkTextarea.vue';
|
import MkTextarea from '@/components/MkTextarea.vue';
|
||||||
|
import MkDraggable from '@/components/MkDraggable.vue';
|
||||||
import { useRouter } from '@/router.js';
|
import { useRouter } from '@/router.js';
|
||||||
|
|
||||||
const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default));
|
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
|
|
@ -99,7 +97,7 @@ const bannerId = ref<string | null>(null);
|
||||||
const color = ref('#000');
|
const color = ref('#000');
|
||||||
const isSensitive = ref(false);
|
const isSensitive = ref(false);
|
||||||
const allowRenoteToExternal = ref(true);
|
const allowRenoteToExternal = ref(true);
|
||||||
const pinnedNotes = ref<{ id: Misskey.entities.Note['id'] }[]>([]);
|
const pinnedNoteIds = ref<Misskey.entities.Note['id'][]>([]);
|
||||||
|
|
||||||
watch(() => bannerId.value, async () => {
|
watch(() => bannerId.value, async () => {
|
||||||
if (bannerId.value == null) {
|
if (bannerId.value == null) {
|
||||||
|
|
@ -123,9 +121,7 @@ async function fetchChannel() {
|
||||||
bannerId.value = result.bannerId;
|
bannerId.value = result.bannerId;
|
||||||
bannerUrl.value = result.bannerUrl;
|
bannerUrl.value = result.bannerUrl;
|
||||||
isSensitive.value = result.isSensitive;
|
isSensitive.value = result.isSensitive;
|
||||||
pinnedNotes.value = result.pinnedNoteIds.map(id => ({
|
pinnedNoteIds.value = result.pinnedNoteIds;
|
||||||
id,
|
|
||||||
}));
|
|
||||||
color.value = result.color;
|
color.value = result.color;
|
||||||
allowRenoteToExternal.value = result.allowRenoteToExternal;
|
allowRenoteToExternal.value = result.allowRenoteToExternal;
|
||||||
|
|
||||||
|
|
@ -143,13 +139,11 @@ async function addPinnedNote() {
|
||||||
const note = await os.apiWithDialog('notes/show', {
|
const note = await os.apiWithDialog('notes/show', {
|
||||||
noteId: fromUrl ?? value,
|
noteId: fromUrl ?? value,
|
||||||
});
|
});
|
||||||
pinnedNotes.value = [{
|
pinnedNoteIds.value.unshift(note.id);
|
||||||
id: note.id,
|
|
||||||
}, ...pinnedNotes.value];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function removePinnedNote(index: number) {
|
function removePinnedNote(id: string) {
|
||||||
pinnedNotes.value.splice(index, 1);
|
pinnedNoteIds.value = pinnedNoteIds.value.filter(x => x !== id);
|
||||||
}
|
}
|
||||||
|
|
||||||
function save() {
|
function save() {
|
||||||
|
|
@ -166,7 +160,7 @@ function save() {
|
||||||
os.apiWithDialog('channels/update', {
|
os.apiWithDialog('channels/update', {
|
||||||
...params,
|
...params,
|
||||||
channelId: props.channelId,
|
channelId: props.channelId,
|
||||||
pinnedNoteIds: pinnedNotes.value.map(x => x.id),
|
pinnedNoteIds: pinnedNoteIds.value,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
os.apiWithDialog('channels/create', params).then(created => {
|
os.apiWithDialog('channels/create', params).then(created => {
|
||||||
|
|
|
||||||
|
|
@ -4,36 +4,41 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Sortable :modelValue="modelValue" tag="div" itemKey="id" handle=".drag-handle" :group="{ name: 'blocks' }" :animation="150" :swapThreshold="0.5" @update:modelValue="v => emit('update:modelValue', v)">
|
<MkDraggable
|
||||||
<template #item="{element}">
|
:modelValue="modelValue"
|
||||||
<div :class="$style.item">
|
direction="vertical"
|
||||||
|
withGaps
|
||||||
|
canNest
|
||||||
|
group="pageBlocks"
|
||||||
|
@update:modelValue="v => emit('update:modelValue', v)"
|
||||||
|
>
|
||||||
|
<template #default="{ item }">
|
||||||
|
<div>
|
||||||
<!-- divが無いとエラーになる https://github.com/SortableJS/vue.draggable.next/issues/189 -->
|
<!-- divが無いとエラーになる https://github.com/SortableJS/vue.draggable.next/issues/189 -->
|
||||||
<component :is="getComponent(element.type)" :modelValue="element" @update:modelValue="updateItem" @remove="() => removeItem(element)"/>
|
<component :is="getComponent(item.type) as any" :modelValue="item" @update:modelValue="updateItem" @remove="() => removeItem(item)"/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</Sortable>
|
</MkDraggable>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { defineAsyncComponent } from 'vue';
|
|
||||||
import * as Misskey from 'misskey-js';
|
import * as Misskey from 'misskey-js';
|
||||||
import XSection from './els/page-editor.el.section.vue';
|
import XSection from './els/page-editor.el.section.vue';
|
||||||
import XText from './els/page-editor.el.text.vue';
|
import XText from './els/page-editor.el.text.vue';
|
||||||
import XImage from './els/page-editor.el.image.vue';
|
import XImage from './els/page-editor.el.image.vue';
|
||||||
import XNote from './els/page-editor.el.note.vue';
|
import XNote from './els/page-editor.el.note.vue';
|
||||||
|
import MkDraggable from '@/components/MkDraggable.vue';
|
||||||
|
|
||||||
function getComponent(type: string) {
|
function getComponent(type: Misskey.entities.Page['content'][number]['type']) {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'section': return XSection;
|
case 'section': return XSection;
|
||||||
case 'text': return XText;
|
case 'text': return XText;
|
||||||
case 'image': return XImage;
|
case 'image': return XImage;
|
||||||
case 'note': return XNote;
|
case 'note': return XNote;
|
||||||
default: return null;
|
default: return XText;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default));
|
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
modelValue: Misskey.entities.Page['content'];
|
modelValue: Misskey.entities.Page['content'];
|
||||||
}>();
|
}>();
|
||||||
|
|
@ -61,11 +66,3 @@ function removeItem(el) {
|
||||||
emit('update:modelValue', newValue);
|
emit('update:modelValue', newValue);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" module>
|
|
||||||
.item {
|
|
||||||
& + .item {
|
|
||||||
margin-top: 16px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
|
||||||
|
|
@ -18,19 +18,18 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div v-panel style="border-radius: 6px;">
|
<div v-panel style="border-radius: 6px;">
|
||||||
<Sortable
|
<MkDraggable
|
||||||
v-model="emojis"
|
:modelValue="emojis.map(emoji => ({ id: emoji, emoji }))"
|
||||||
|
direction="horizontal"
|
||||||
:class="$style.emojis"
|
:class="$style.emojis"
|
||||||
:itemKey="item => item"
|
group="emojiPalettes"
|
||||||
:animation="150"
|
@update:modelValue="v => emojis = v.map(x => x.emoji)"
|
||||||
:delay="100"
|
|
||||||
:delayOnTouchOnly="true"
|
|
||||||
:group="{ name: 'SortableEmojiPalettes' }"
|
|
||||||
>
|
>
|
||||||
<template #item="{element}">
|
<template #default="{ item }">
|
||||||
<button class="_button" :class="$style.emojisItem" @click="remove(element, $event)">
|
<button class="_button" :class="$style.emojisItem" @click="remove(item.emoji, $event)">
|
||||||
<MkCustomEmoji v-if="element[0] === ':'" :name="element" :normal="true" :fallbackToImage="true"/>
|
<!-- pointer-eventsをnoneにしておかないとiOSなどでドラッグしたときに画像の方に判定が持ってかれる -->
|
||||||
<MkEmoji v-else :emoji="element" :normal="true"/>
|
<MkCustomEmoji v-if="item.emoji[0] === ':'" style="pointer-events: none;" :name="item.emoji" :normal="true" :fallbackToImage="true"/>
|
||||||
|
<MkEmoji v-else style="pointer-events: none;" :emoji="item.emoji" :normal="true"/>
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
|
|
@ -38,7 +37,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<i class="ti ti-plus"></i>
|
<i class="ti ti-plus"></i>
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
</Sortable>
|
</MkDraggable>
|
||||||
</div>
|
</div>
|
||||||
<div :class="$style.editorCaption">{{ i18n.ts.reactionSettingDescription2 }}</div>
|
<div :class="$style.editorCaption">{{ i18n.ts.reactionSettingDescription2 }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -47,7 +46,6 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref, watch } from 'vue';
|
import { ref, watch } from 'vue';
|
||||||
import Sortable from 'vuedraggable';
|
|
||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from '@/components/MkButton.vue';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
|
|
@ -55,6 +53,7 @@ import { deepClone } from '@/utility/clone.js';
|
||||||
import MkCustomEmoji from '@/components/global/MkCustomEmoji.vue';
|
import MkCustomEmoji from '@/components/global/MkCustomEmoji.vue';
|
||||||
import MkEmoji from '@/components/global/MkEmoji.vue';
|
import MkEmoji from '@/components/global/MkEmoji.vue';
|
||||||
import MkFolder from '@/components/MkFolder.vue';
|
import MkFolder from '@/components/MkFolder.vue';
|
||||||
|
import MkDraggable from '@/components/MkDraggable.vue';
|
||||||
import { copyToClipboard } from '@/utility/copy-to-clipboard.js';
|
import { copyToClipboard } from '@/utility/copy-to-clipboard.js';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
|
|
|
||||||
|
|
@ -9,25 +9,21 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<FormSlot>
|
<FormSlot>
|
||||||
<template #label>{{ i18n.ts.navbar }}</template>
|
<template #label>{{ i18n.ts.navbar }}</template>
|
||||||
<MkContainer :showHeader="false">
|
<MkContainer :showHeader="false">
|
||||||
<Sortable
|
<MkDraggable
|
||||||
v-model="items"
|
v-model="items"
|
||||||
itemKey="id"
|
direction="vertical"
|
||||||
:animation="150"
|
|
||||||
:handle="'.' + $style.itemHandle"
|
|
||||||
@start="e => e.item.classList.add('active')"
|
|
||||||
@end="e => e.item.classList.remove('active')"
|
|
||||||
>
|
>
|
||||||
<template #item="{element,index}">
|
<template #default="{ item }">
|
||||||
<div
|
<div
|
||||||
v-if="element.type === '-' || navbarItemDef[element.type]"
|
v-if="item.type === '-' || navbarItemDef[item.type]"
|
||||||
:class="$style.item"
|
:class="$style.item"
|
||||||
>
|
>
|
||||||
<button class="_button" :class="$style.itemHandle"><i class="ti ti-menu"></i></button>
|
<button class="_button" :class="$style.itemHandle"><i class="ti ti-menu"></i></button>
|
||||||
<i class="ti-fw" :class="[$style.itemIcon, navbarItemDef[element.type]?.icon]"></i><span :class="$style.itemText">{{ navbarItemDef[element.type]?.title ?? i18n.ts.divider }}</span>
|
<i class="ti-fw" :class="[$style.itemIcon, navbarItemDef[item.type]?.icon]"></i><span :class="$style.itemText">{{ navbarItemDef[item.type]?.title ?? i18n.ts.divider }}</span>
|
||||||
<button class="_button" :class="$style.itemRemove" @click="removeItem(index)"><i class="ti ti-x"></i></button>
|
<button class="_button" :class="$style.itemRemove" @click="removeItem(item.id)"><i class="ti ti-x"></i></button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</Sortable>
|
</MkDraggable>
|
||||||
</MkContainer>
|
</MkContainer>
|
||||||
</FormSlot>
|
</FormSlot>
|
||||||
<div class="_buttons">
|
<div class="_buttons">
|
||||||
|
|
@ -54,13 +50,14 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, defineAsyncComponent, ref, watch } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
import MkRadios from '@/components/MkRadios.vue';
|
import MkRadios from '@/components/MkRadios.vue';
|
||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from '@/components/MkButton.vue';
|
||||||
import FormSlot from '@/components/form/slot.vue';
|
import FormSlot from '@/components/form/slot.vue';
|
||||||
import MkContainer from '@/components/MkContainer.vue';
|
import MkContainer from '@/components/MkContainer.vue';
|
||||||
import MkSwitch from '@/components/MkSwitch.vue';
|
import MkSwitch from '@/components/MkSwitch.vue';
|
||||||
import MkPreferenceContainer from '@/components/MkPreferenceContainer.vue';
|
import MkPreferenceContainer from '@/components/MkPreferenceContainer.vue';
|
||||||
|
import MkDraggable from '@/components/MkDraggable.vue';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
import { navbarItemDef } from '@/navbar.js';
|
import { navbarItemDef } from '@/navbar.js';
|
||||||
import { store } from '@/store.js';
|
import { store } from '@/store.js';
|
||||||
|
|
@ -70,8 +67,6 @@ import { prefer } from '@/preferences.js';
|
||||||
import { getInitialPrefValue } from '@/preferences/manager.js';
|
import { getInitialPrefValue } from '@/preferences/manager.js';
|
||||||
import { genId } from '@/utility/id.js';
|
import { genId } from '@/utility/id.js';
|
||||||
|
|
||||||
const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default));
|
|
||||||
|
|
||||||
const items = ref(prefer.s.menu.map(x => ({
|
const items = ref(prefer.s.menu.map(x => ({
|
||||||
id: genId(),
|
id: genId(),
|
||||||
type: x,
|
type: x,
|
||||||
|
|
@ -98,8 +93,8 @@ async function addItem() {
|
||||||
}];
|
}];
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeItem(index: number) {
|
function removeItem(itemId: string) {
|
||||||
items.value.splice(index, 1);
|
items.value = items.value.filter(i => i.id !== itemId);
|
||||||
}
|
}
|
||||||
|
|
||||||
function save() {
|
function save() {
|
||||||
|
|
|
||||||
|
|
@ -75,30 +75,27 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<div :class="$style.metadataRoot" class="_gaps_s">
|
<div :class="$style.metadataRoot" class="_gaps_s">
|
||||||
<MkInfo>{{ i18n.ts._profile.verifiedLinkDescription }}</MkInfo>
|
<MkInfo>{{ i18n.ts._profile.verifiedLinkDescription }}</MkInfo>
|
||||||
|
|
||||||
<Sortable
|
<MkDraggable
|
||||||
v-model="fields"
|
v-model="fields"
|
||||||
class="_gaps_s"
|
direction="vertical"
|
||||||
itemKey="id"
|
withGaps
|
||||||
:animation="150"
|
manualDragStart
|
||||||
:handle="'.' + $style.dragItemHandle"
|
|
||||||
@start="e => e.item.classList.add('active')"
|
|
||||||
@end="e => e.item.classList.remove('active')"
|
|
||||||
>
|
>
|
||||||
<template #item="{element, index}">
|
<template #default="{ item, dragStart }">
|
||||||
<div v-panel :class="$style.fieldDragItem">
|
<div v-panel :class="$style.fieldDragItem">
|
||||||
<button v-if="!fieldEditMode" class="_button" :class="$style.dragItemHandle" tabindex="-1"><i class="ti ti-menu"></i></button>
|
<button v-if="!fieldEditMode" class="_button" :class="$style.dragItemHandle" tabindex="-1" :draggable="true" @dragstart.stop="dragStart"><i class="ti ti-menu"></i></button>
|
||||||
<button v-if="fieldEditMode" :disabled="fields.length <= 1" class="_button" :class="$style.dragItemRemove" @click="deleteField(index)"><i class="ti ti-x"></i></button>
|
<button v-if="fieldEditMode" :disabled="fields.length <= 1" class="_button" :class="$style.dragItemRemove" @click="deleteField(item.id)"><i class="ti ti-x"></i></button>
|
||||||
<div :class="$style.dragItemForm">
|
<div :class="$style.dragItemForm">
|
||||||
<FormSplit :minWidth="200">
|
<FormSplit :minWidth="200">
|
||||||
<MkInput v-model="element.name" small :placeholder="i18n.ts._profile.metadataLabel">
|
<MkInput v-model="item.name" small :placeholder="i18n.ts._profile.metadataLabel">
|
||||||
</MkInput>
|
</MkInput>
|
||||||
<MkInput v-model="element.value" small :placeholder="i18n.ts._profile.metadataContent">
|
<MkInput v-model="item.value" small :placeholder="i18n.ts._profile.metadataContent">
|
||||||
</MkInput>
|
</MkInput>
|
||||||
</FormSplit>
|
</FormSplit>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</Sortable>
|
</MkDraggable>
|
||||||
</div>
|
</div>
|
||||||
</MkFolder>
|
</MkFolder>
|
||||||
<template #caption>{{ i18n.ts._profile.metadataDescription }}</template>
|
<template #caption>{{ i18n.ts._profile.metadataDescription }}</template>
|
||||||
|
|
@ -165,7 +162,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, reactive, ref, watch, defineAsyncComponent } from 'vue';
|
import { computed, reactive, ref, watch } from 'vue';
|
||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from '@/components/MkButton.vue';
|
||||||
import MkInput from '@/components/MkInput.vue';
|
import MkInput from '@/components/MkInput.vue';
|
||||||
import MkSwitch from '@/components/MkSwitch.vue';
|
import MkSwitch from '@/components/MkSwitch.vue';
|
||||||
|
|
@ -174,6 +171,7 @@ import FormSplit from '@/components/form/split.vue';
|
||||||
import MkFolder from '@/components/MkFolder.vue';
|
import MkFolder from '@/components/MkFolder.vue';
|
||||||
import FormSlot from '@/components/form/slot.vue';
|
import FormSlot from '@/components/form/slot.vue';
|
||||||
import FormLink from '@/components/form/link.vue';
|
import FormLink from '@/components/form/link.vue';
|
||||||
|
import MkDraggable from '@/components/MkDraggable.vue';
|
||||||
import { chooseDriveFile } from '@/utility/drive.js';
|
import { chooseDriveFile } from '@/utility/drive.js';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
|
|
@ -188,8 +186,6 @@ import { genId } from '@/utility/id.js';
|
||||||
|
|
||||||
const $i = ensureSignin();
|
const $i = ensureSignin();
|
||||||
|
|
||||||
const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default));
|
|
||||||
|
|
||||||
const reactionAcceptance = store.model('reactionAcceptance');
|
const reactionAcceptance = store.model('reactionAcceptance');
|
||||||
|
|
||||||
function assertVaildLang(lang: string | null): lang is keyof typeof langmap {
|
function assertVaildLang(lang: string | null): lang is keyof typeof langmap {
|
||||||
|
|
@ -228,8 +224,8 @@ while (fields.value.length < 4) {
|
||||||
addField();
|
addField();
|
||||||
}
|
}
|
||||||
|
|
||||||
function deleteField(index: number) {
|
function deleteField(itemId: string) {
|
||||||
fields.value.splice(index, 1);
|
fields.value = fields.value.filter(f => f.id !== itemId);
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveFields() {
|
function saveFields() {
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<XWidgets :edit="editMode" :widgets="widgets" @addWidget="addWidget" @removeWidget="removeWidget" @updateWidget="updateWidget" @updateWidgets="updateWidgets" @exit="editMode = false"/>
|
<XWidgets :edit="editMode" :widgets="widgets" @addWidget="addWidget" @removeWidget="removeWidget" @updateWidget="updateWidget" @updateWidgets="updateWidgets" @exit="editMode = false"/>
|
||||||
|
|
||||||
<button v-if="editMode" class="_textButton" style="font-size: 0.9em;" @click="editMode = false"><i class="ti ti-check"></i> {{ i18n.ts.editWidgetsExit }}</button>
|
<button v-if="editMode" class="_textButton" style="font-size: 0.9em;" @click="editMode = false"><i class="ti ti-check"></i> {{ i18n.ts.editWidgetsExit }}</button>
|
||||||
<button v-else class="_textButton" data-cy-widget-edit :class="$style.edit" style="font-size: 0.9em;" @click="editMode = true"><i class="ti ti-pencil"></i> {{ i18n.ts.editWidgets }}</button>
|
<button v-else class="_textButton" data-cy-widget-edit :class="$style.edit" style="font-size: 0.9em; margin-top: 16px;" @click="editMode = true"><i class="ti ti-pencil"></i> {{ i18n.ts.editWidgets }}</button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -833,9 +833,6 @@ importers:
|
||||||
vue:
|
vue:
|
||||||
specifier: 3.5.26
|
specifier: 3.5.26
|
||||||
version: 3.5.26(typescript@5.9.3)
|
version: 3.5.26(typescript@5.9.3)
|
||||||
vuedraggable:
|
|
||||||
specifier: next
|
|
||||||
version: 4.1.0(vue@3.5.26(typescript@5.9.3))
|
|
||||||
wanakana:
|
wanakana:
|
||||||
specifier: 5.3.1
|
specifier: 5.3.1
|
||||||
version: 5.3.1
|
version: 5.3.1
|
||||||
|
|
@ -4933,10 +4930,6 @@ packages:
|
||||||
eslint: ^8.57.0 || ^9.0.0
|
eslint: ^8.57.0 || ^9.0.0
|
||||||
typescript: '>=4.8.4 <6.0.0'
|
typescript: '>=4.8.4 <6.0.0'
|
||||||
|
|
||||||
'@typescript-eslint/types@8.49.0':
|
|
||||||
resolution: {integrity: sha512-e9k/fneezorUo6WShlQpMxXh8/8wfyc+biu6tnAqA81oWrEic0k21RHzP9uqqpyBBeBKu4T+Bsjy9/b8u7obXQ==}
|
|
||||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
|
||||||
|
|
||||||
'@typescript-eslint/types@8.50.1':
|
'@typescript-eslint/types@8.50.1':
|
||||||
resolution: {integrity: sha512-v5lFIS2feTkNyMhd7AucE/9j/4V9v5iIbpVRncjk/K0sQ6Sb+Np9fgYS/63n6nwqahHQvbmujeBL7mp07Q9mlA==}
|
resolution: {integrity: sha512-v5lFIS2feTkNyMhd7AucE/9j/4V9v5iIbpVRncjk/K0sQ6Sb+Np9fgYS/63n6nwqahHQvbmujeBL7mp07Q9mlA==}
|
||||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
|
|
@ -10107,9 +10100,6 @@ packages:
|
||||||
resolution: {integrity: sha512-vzn8aSqKgytVik0iwdBEi+zevbTYZogewTUM6dtpmGwEcdzbub/TX4bCzRhebDCRC3QzXgJsLRKB2V/Oof7HXg==}
|
resolution: {integrity: sha512-vzn8aSqKgytVik0iwdBEi+zevbTYZogewTUM6dtpmGwEcdzbub/TX4bCzRhebDCRC3QzXgJsLRKB2V/Oof7HXg==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
sortablejs@1.14.0:
|
|
||||||
resolution: {integrity: sha512-pBXvQCs5/33fdN1/39pPL0NZF20LeRbLQ5jtnheIPN9JQAaufGjKdWduZn4U7wCtVuzKhmRkI0DFYHYRbB2H1w==}
|
|
||||||
|
|
||||||
source-map-js@1.2.1:
|
source-map-js@1.2.1:
|
||||||
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
|
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
@ -11114,11 +11104,6 @@ packages:
|
||||||
typescript:
|
typescript:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
vuedraggable@4.1.0:
|
|
||||||
resolution: {integrity: sha512-FU5HCWBmsf20GpP3eudURW3WdWTKIbEIQxh9/8GE806hydR9qZqRRxRE3RjqX7PkuLuMQG/A7n3cfj9rCEchww==}
|
|
||||||
peerDependencies:
|
|
||||||
vue: ^3.0.1
|
|
||||||
|
|
||||||
w3c-xmlserializer@4.0.0:
|
w3c-xmlserializer@4.0.0:
|
||||||
resolution: {integrity: sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==}
|
resolution: {integrity: sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==}
|
||||||
engines: {node: '>=14'}
|
engines: {node: '>=14'}
|
||||||
|
|
@ -15278,7 +15263,7 @@ snapshots:
|
||||||
'@stylistic/eslint-plugin@5.5.0(eslint@9.39.2)':
|
'@stylistic/eslint-plugin@5.5.0(eslint@9.39.2)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@eslint-community/eslint-utils': 4.9.0(eslint@9.39.2)
|
'@eslint-community/eslint-utils': 4.9.0(eslint@9.39.2)
|
||||||
'@typescript-eslint/types': 8.49.0
|
'@typescript-eslint/types': 8.50.1
|
||||||
eslint: 9.39.2
|
eslint: 9.39.2
|
||||||
eslint-visitor-keys: 4.2.1
|
eslint-visitor-keys: 4.2.1
|
||||||
espree: 10.4.0
|
espree: 10.4.0
|
||||||
|
|
@ -15972,8 +15957,6 @@ snapshots:
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
'@typescript-eslint/types@8.49.0': {}
|
|
||||||
|
|
||||||
'@typescript-eslint/types@8.50.1': {}
|
'@typescript-eslint/types@8.50.1': {}
|
||||||
|
|
||||||
'@typescript-eslint/typescript-estree@8.50.1(typescript@5.9.3)':
|
'@typescript-eslint/typescript-estree@8.50.1(typescript@5.9.3)':
|
||||||
|
|
@ -22285,8 +22268,6 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
is-plain-obj: 1.1.0
|
is-plain-obj: 1.1.0
|
||||||
|
|
||||||
sortablejs@1.14.0: {}
|
|
||||||
|
|
||||||
source-map-js@1.2.1: {}
|
source-map-js@1.2.1: {}
|
||||||
|
|
||||||
source-map-support@0.5.13:
|
source-map-support@0.5.13:
|
||||||
|
|
@ -23270,11 +23251,6 @@ snapshots:
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
typescript: 5.9.3
|
typescript: 5.9.3
|
||||||
|
|
||||||
vuedraggable@4.1.0(vue@3.5.26(typescript@5.9.3)):
|
|
||||||
dependencies:
|
|
||||||
sortablejs: 1.14.0
|
|
||||||
vue: 3.5.26(typescript@5.9.3)
|
|
||||||
|
|
||||||
w3c-xmlserializer@4.0.0:
|
w3c-xmlserializer@4.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
xml-name-validator: 4.0.0
|
xml-name-validator: 4.0.0
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue