refactor(frontend): Vue 드래그 정렬 라이브러리를 교체
This commit is contained in:
@@ -344,6 +344,7 @@ test('physical mobile touch reorders NPC priority across active and inactive lis
|
||||
const nationPanel = mobilePage.locator('.priority-panel').first();
|
||||
const activeList = nationPanel.locator('.priority-column').nth(1).locator('.priority-list');
|
||||
const activeRows = activeList.locator('.priority-item');
|
||||
await expect(activeRows.first()).toHaveCSS('touch-action', 'none');
|
||||
await touchDrag(
|
||||
mobilePage,
|
||||
activeRows.nth(0),
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
"mitt": "^3.0.1",
|
||||
"pinia": "^3.0.4",
|
||||
"vue": "^3.5.26",
|
||||
"vuedraggable-es": "4.1.1",
|
||||
"vue-draggable-plus": "0.6.1",
|
||||
"vue-router": "^4.6.4",
|
||||
"zod": "^4.3.5"
|
||||
},
|
||||
|
||||
@@ -1,5 +1,25 @@
|
||||
import { defineComponent, h, type PropType, type SlotsType, type VNode } from 'vue';
|
||||
import VueDraggable from 'vuedraggable-es';
|
||||
import { cloneVNode, computed, defineComponent, h, ref, type PropType, type SlotsType, type VNode } from 'vue';
|
||||
import { useDraggable, type DraggableEvent, type UseDraggableOptions } from 'vue-draggable-plus';
|
||||
|
||||
const SORTABLE_ITEM_ATTRIBUTE = 'data-sortable-string-list-item';
|
||||
|
||||
// SortableJS allows only one active drag, so this preserves the exact string across grouped lists.
|
||||
let activeStringDrag: { value: string } | null = null;
|
||||
|
||||
const restoreItem = (event: DraggableEvent<string>) => {
|
||||
if (event.oldIndex === undefined) return;
|
||||
event.item.remove();
|
||||
event.from.insertBefore(event.item, event.from.children[event.oldIndex] ?? null);
|
||||
};
|
||||
|
||||
const moveItem = (list: string[], from: number, to: number): string[] => {
|
||||
if (from === to) return list;
|
||||
const next = [...list];
|
||||
const [item] = next.splice(from, 1);
|
||||
if (item === undefined) return list;
|
||||
next.splice(to, 0, item);
|
||||
return next;
|
||||
};
|
||||
|
||||
export default defineComponent({
|
||||
name: 'SortableStringList',
|
||||
@@ -18,26 +38,79 @@ export default defineComponent({
|
||||
default: 'div',
|
||||
},
|
||||
},
|
||||
emits: {
|
||||
'update:list': (list: string[]) => Array.isArray(list),
|
||||
},
|
||||
slots: Object as SlotsType<{
|
||||
header?: () => VNode[];
|
||||
item: (props: { element: string; index: number }) => VNode[];
|
||||
}>,
|
||||
setup(props, { attrs, slots }) {
|
||||
return () =>
|
||||
h(
|
||||
VueDraggable,
|
||||
{
|
||||
...attrs,
|
||||
list: props.list,
|
||||
group: props.group,
|
||||
itemKey: (item: string) => item,
|
||||
tag: props.tag,
|
||||
},
|
||||
{
|
||||
header: () => slots.header?.(),
|
||||
item: ({ element, index }: { element: string; index: number }) =>
|
||||
slots.item({ element, index }),
|
||||
setup(props, { attrs, emit, slots }) {
|
||||
const root = ref<HTMLElement | null>(null);
|
||||
let initialChildren: ChildNode[] | null = null;
|
||||
|
||||
const options = computed<UseDraggableOptions<string>>(() => ({
|
||||
group: props.group,
|
||||
draggable: `[${SORTABLE_ITEM_ATTRIBUTE}]`,
|
||||
dataIdAttr: SORTABLE_ITEM_ATTRIBUTE,
|
||||
onStart: (event) => {
|
||||
initialChildren = Array.from(event.from.childNodes);
|
||||
if (event.oldDraggableIndex === undefined) {
|
||||
activeStringDrag = null;
|
||||
return;
|
||||
}
|
||||
const value = props.list[event.oldDraggableIndex];
|
||||
activeStringDrag = value === undefined ? null : { value };
|
||||
},
|
||||
onUpdate: (event) => {
|
||||
restoreItem(event);
|
||||
if (event.oldDraggableIndex === undefined || event.newDraggableIndex === undefined) return;
|
||||
emit('update:list', moveItem(props.list, event.oldDraggableIndex, event.newDraggableIndex));
|
||||
},
|
||||
onRemove: (event) => {
|
||||
restoreItem(event);
|
||||
if (event.pullMode === 'clone') {
|
||||
event.clone.remove();
|
||||
return;
|
||||
}
|
||||
if (event.oldDraggableIndex === undefined) return;
|
||||
const next = [...props.list];
|
||||
next.splice(event.oldDraggableIndex, 1);
|
||||
emit('update:list', next);
|
||||
},
|
||||
onAdd: (event) => {
|
||||
event.item.remove();
|
||||
if (event.newDraggableIndex === undefined) return;
|
||||
const value = activeStringDrag?.value ?? event.item.getAttribute(SORTABLE_ITEM_ATTRIBUTE);
|
||||
if (value === null) return;
|
||||
const next = [...props.list];
|
||||
next.splice(event.newDraggableIndex, 0, value);
|
||||
emit('update:list', next);
|
||||
},
|
||||
onEnd: (event) => {
|
||||
if (event.from === event.to && event.oldIndex === event.newIndex && initialChildren) {
|
||||
for (const child of initialChildren) event.from.append(child);
|
||||
}
|
||||
initialChildren = null;
|
||||
activeStringDrag = null;
|
||||
},
|
||||
}));
|
||||
|
||||
useDraggable(root, options);
|
||||
|
||||
return () => {
|
||||
const header = slots.header?.() ?? [];
|
||||
const items = props.list.flatMap((element, index) =>
|
||||
slots.item({ element, index }).map((node) =>
|
||||
cloneVNode(node, {
|
||||
key: element,
|
||||
[SORTABLE_ITEM_ATTRIBUTE]: element,
|
||||
// Prevent a long-list touch gesture from being cancelled as page scrolling.
|
||||
style: [node.props?.style, { touchAction: 'none' }],
|
||||
})
|
||||
)
|
||||
);
|
||||
return h(props.tag, { ...attrs, ref: root }, [...header, ...items]);
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
@@ -691,7 +691,7 @@ onMounted(() => {
|
||||
</div>
|
||||
<p>항목을 끌어 놓거나 위·아래 버튼으로 상대 순서를 바꿉니다.</p>
|
||||
<SortableStringList
|
||||
:list="mobileLayoutOrder"
|
||||
v-model:list="mobileLayoutOrder"
|
||||
tag="ol"
|
||||
class="mobile-layout-list"
|
||||
>
|
||||
|
||||
@@ -448,7 +448,7 @@ const submitPriority = async (section: PrioritySectionKey) => {
|
||||
<div class="priority-column">
|
||||
<div class="sub_bar legacy-bg2">비활성</div>
|
||||
<SortableStringList
|
||||
:list="panel.state.inactive"
|
||||
v-model:list="panel.state.inactive"
|
||||
:group="`npc-priority-${panel.key}`"
|
||||
tag="div"
|
||||
class="priority-list"
|
||||
@@ -477,7 +477,7 @@ const submitPriority = async (section: PrioritySectionKey) => {
|
||||
<div class="priority-column">
|
||||
<div class="sub_bar legacy-bg2">활성</div>
|
||||
<SortableStringList
|
||||
:list="panel.state.active"
|
||||
v-model:list="panel.state.active"
|
||||
:group="`npc-priority-${panel.key}`"
|
||||
tag="div"
|
||||
class="priority-list"
|
||||
|
||||
Reference in New Issue
Block a user