merge: 최신 main을 모바일 터치 드래그 브랜치에 통합
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
import type { CommandMapData, CommandOption } from './types';
|
||||
|
||||
export const commandCityOptions = (
|
||||
commandKey: string,
|
||||
options: readonly CommandOption[],
|
||||
mapData?: CommandMapData | null
|
||||
): CommandOption[] => {
|
||||
if (commandKey !== 'che_발령' || typeof mapData?.myNation !== 'number') return [...options];
|
||||
|
||||
const nationByCityId = new Map(mapData.cityList.map(([cityId, , , nationId]) => [cityId, nationId]));
|
||||
return options
|
||||
.map((option, index) => ({ option, index }))
|
||||
.sort((left, right) => {
|
||||
const leftOwned =
|
||||
typeof left.option.value === 'number' && nationByCityId.get(left.option.value) === mapData.myNation;
|
||||
const rightOwned =
|
||||
typeof right.option.value === 'number' && nationByCityId.get(right.option.value) === mapData.myNation;
|
||||
return Number(rightOwned) - Number(leftOwned) || left.index - right.index;
|
||||
})
|
||||
.map(({ option }) => option);
|
||||
};
|
||||
@@ -2,6 +2,7 @@
|
||||
import { computed, reactive, watch, type CSSProperties } from 'vue';
|
||||
import MapViewer from './MapViewer.vue';
|
||||
import { commandArgumentPresentation } from '../command/commandArgumentPresentation';
|
||||
import { commandCityOptions } from '../command/commandArgumentOptions';
|
||||
import {
|
||||
commandArgumentFieldContract,
|
||||
shouldPreserveCommandArgumentValue,
|
||||
@@ -64,6 +65,9 @@ const optionsFor = (field: CommandInputField): CommandOption[] => {
|
||||
if (field.optionSource === 'nations') {
|
||||
return props.options.nationTargets?.[props.commandKey] ?? props.options.nations;
|
||||
}
|
||||
if (field.optionSource === 'cities') {
|
||||
return commandCityOptions(props.commandKey, props.options.cities, props.mapData);
|
||||
}
|
||||
if (field.optionSource === 'items') {
|
||||
return props.options.items[String(values.itemType ?? '')] ?? [];
|
||||
}
|
||||
@@ -104,12 +108,7 @@ const synchronizeValues = () => {
|
||||
for (const field of props.fields) {
|
||||
const preserve =
|
||||
!commandChanged &&
|
||||
shouldPreserveCommandArgumentValue(
|
||||
field,
|
||||
previousFieldContracts.get(field.key),
|
||||
values,
|
||||
optionsFor(field)
|
||||
);
|
||||
shouldPreserveCommandArgumentValue(field, previousFieldContracts.get(field.key), values, optionsFor(field));
|
||||
if (!preserve) values[field.key] = defaultValue(field);
|
||||
}
|
||||
const itemCodeField = props.fields.find((field) => field.key === 'itemCode');
|
||||
|
||||
@@ -28,6 +28,8 @@ const props = defineProps<{
|
||||
const emit = defineEmits<{
|
||||
(event: 'hover', cityId: number): void;
|
||||
(event: 'leave'): void;
|
||||
(event: 'touch', cityId: number, touchEvent: TouchEvent): void;
|
||||
(event: 'touchleave'): void;
|
||||
(event: 'select', cityId: number): void;
|
||||
}>();
|
||||
|
||||
@@ -37,6 +39,25 @@ const stateOffset = computed(() => -6 * props.mapScale);
|
||||
const selectCity = () => {
|
||||
if (!props.readonly) emit('select', props.city.id);
|
||||
};
|
||||
|
||||
let touchOnTrack = false;
|
||||
|
||||
const touchstart = () => {
|
||||
touchOnTrack = true;
|
||||
};
|
||||
|
||||
const touchmove = () => {
|
||||
touchOnTrack = false;
|
||||
};
|
||||
|
||||
const touchend = (event: TouchEvent) => {
|
||||
if (touchOnTrack) {
|
||||
event.stopPropagation();
|
||||
emit('touch', props.city.id, event);
|
||||
return;
|
||||
}
|
||||
emit('touchleave');
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -59,6 +80,9 @@ const selectCity = () => {
|
||||
:style="{ left: `${props.city.x}px`, top: `${props.city.y}px` }"
|
||||
@mouseenter="emit('hover', props.city.id)"
|
||||
@mouseleave="emit('leave')"
|
||||
@touchstart="touchstart"
|
||||
@touchmove="touchmove"
|
||||
@touchend="touchend"
|
||||
@click.stop="selectCity"
|
||||
>
|
||||
<div class="city-dot" :style="{ backgroundColor: props.city.color, width: `${size}px`, height: `${size}px` }">
|
||||
|
||||
@@ -54,6 +54,8 @@ const props = defineProps<{
|
||||
const emit = defineEmits<{
|
||||
(event: 'hover', cityId: number): void;
|
||||
(event: 'leave'): void;
|
||||
(event: 'touch', cityId: number, touchEvent: TouchEvent): void;
|
||||
(event: 'touchleave'): void;
|
||||
(event: 'select', cityId: number): void;
|
||||
}>();
|
||||
|
||||
@@ -148,6 +150,25 @@ const selectCity = () => {
|
||||
if (!props.readonly) emit('select', props.city.id);
|
||||
};
|
||||
|
||||
let touchOnTrack = false;
|
||||
|
||||
const touchstart = () => {
|
||||
touchOnTrack = true;
|
||||
};
|
||||
|
||||
const touchmove = () => {
|
||||
touchOnTrack = false;
|
||||
};
|
||||
|
||||
const touchend = (event: TouchEvent) => {
|
||||
if (touchOnTrack) {
|
||||
event.stopPropagation();
|
||||
emit('touch', props.city.id, event);
|
||||
return;
|
||||
}
|
||||
emit('touchleave');
|
||||
};
|
||||
|
||||
const cityStateStyle = computed(() => ({
|
||||
width: `${12 * props.mapScale}px`,
|
||||
height: `${12 * props.mapScale}px`,
|
||||
@@ -174,6 +195,9 @@ const cityStateStyle = computed(() => ({
|
||||
:style="cityBaseStyle"
|
||||
@mouseenter="emit('hover', props.city.id)"
|
||||
@mouseleave="emit('leave')"
|
||||
@touchstart="touchstart"
|
||||
@touchmove="touchmove"
|
||||
@touchend="touchend"
|
||||
@click.stop="selectCity"
|
||||
>
|
||||
<div v-if="cityBgStyle" class="city-bg" :style="[cityBgWrapperStyle, cityBgStyle]" />
|
||||
|
||||
@@ -94,9 +94,11 @@ const mapStore = useMapViewerStore();
|
||||
const {
|
||||
showCityName,
|
||||
detailMode: storeDetailMode,
|
||||
singleTapNavigation,
|
||||
hoveredCityId,
|
||||
selectedCityId: storeSelectedCityId,
|
||||
} = storeToRefs(mapStore);
|
||||
const hasTouchInput = useMediaQuery('(any-pointer: coarse)');
|
||||
|
||||
const mapArea = ref<HTMLElement | null>(null);
|
||||
const mapBody = ref<HTMLElement | null>(null);
|
||||
@@ -404,6 +406,28 @@ const setHoveredCity = (cityId: number | null) => {
|
||||
mapStore.setHoveredCity(cityId);
|
||||
};
|
||||
|
||||
const touchPreviewCityId = ref<number | null>(null);
|
||||
|
||||
const clearTouchPreview = () => {
|
||||
touchPreviewCityId.value = null;
|
||||
setHoveredCity(null);
|
||||
};
|
||||
|
||||
const touchCity = (cityId: number, event: TouchEvent) => {
|
||||
if (touchPreviewCityId.value !== cityId) {
|
||||
touchPreviewCityId.value = cityId;
|
||||
setHoveredCity(cityId);
|
||||
if (!singleTapNavigation.value) {
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const toggleSingleTapNavigation = () => {
|
||||
clearTouchPreview();
|
||||
mapStore.toggleSingleTapNavigation();
|
||||
};
|
||||
|
||||
const selectCity = (cityId: number) => {
|
||||
if (props.readonly) return;
|
||||
emit('select-city', cityId);
|
||||
@@ -433,6 +457,7 @@ const selectCity = (cityId: number) => {
|
||||
class="map-area"
|
||||
:class="[mapThemeClass, mapSeasonClass]"
|
||||
:style="{ width: mapWidth, height: mapHeight }"
|
||||
@click="clearTouchPreview"
|
||||
>
|
||||
<div class="map-layer map-bglayer1" :style="mapBackgroundStyle" />
|
||||
<div class="map-layer map-bglayer2" />
|
||||
@@ -449,6 +474,8 @@ const selectCity = (cityId: number) => {
|
||||
v-bind="detailProps"
|
||||
@hover="setHoveredCity"
|
||||
@leave="setHoveredCity(null)"
|
||||
@touch="touchCity"
|
||||
@touchleave="clearTouchPreview"
|
||||
@select="selectCity"
|
||||
/>
|
||||
<div
|
||||
@@ -466,9 +493,18 @@ const selectCity = (cityId: number) => {
|
||||
<div class="tooltip-body">{{ hoveredCity.nationId > 0 ? hoveredCity.nationName : '' }}</div>
|
||||
</div>
|
||||
<div class="map-controls">
|
||||
<button class="map-toggle" :class="{ active: showCityName }" @click="mapStore.toggleCityName">
|
||||
<button class="map-toggle" :class="{ active: showCityName }" @click.stop="mapStore.toggleCityName">
|
||||
도시명 표기 {{ showCityName ? '끄기' : '켜기' }}
|
||||
</button>
|
||||
<button
|
||||
v-if="hasTouchInput"
|
||||
class="map-toggle map-toggle-single-tap"
|
||||
:class="{ active: singleTapNavigation }"
|
||||
:aria-pressed="singleTapNavigation"
|
||||
@click.stop="toggleSingleTapNavigation"
|
||||
>
|
||||
두번 탭 해 도시 이동 {{ singleTapNavigation ? '켜기' : '끄기' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -555,6 +591,8 @@ const selectCity = (cityId: number) => {
|
||||
right: 4px;
|
||||
bottom: 4px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.map-toggle {
|
||||
|
||||
Reference in New Issue
Block a user