282 lines
5.9 KiB
Vue
282 lines
5.9 KiB
Vue
<template>
|
|
<td
|
|
ref="rootEl"
|
|
:class="$style.cell"
|
|
:style="{ maxWidth: cellWidth, minWidth: cellWidth }"
|
|
:tabindex="-1"
|
|
@dblclick="onCellDoubleClick"
|
|
@keydown="onCellKeyDown"
|
|
>
|
|
<div
|
|
:class="[
|
|
$style.root,
|
|
[cell.selected ? $style.selected : {}],
|
|
[cell.ranged ? $style.ranged : {}],
|
|
[needsContentCentering ? $style.center : {}]
|
|
]"
|
|
>
|
|
<div v-if="!editing" ref="contentAreaEl">
|
|
<div :class="$style.content">
|
|
<div v-if="cellType === 'text'">
|
|
{{ cell.value }}
|
|
</div>
|
|
<div v-else-if="cellType === 'boolean'">
|
|
<span v-if="cell.value" class="ti ti-check"/>
|
|
<span v-else class="ti ti-x"/>
|
|
</div>
|
|
<div v-else-if="cellType === 'image'">
|
|
<img
|
|
:src="cell.value as string"
|
|
:alt="cell.value as string"
|
|
:class="$style.viewImage"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div v-else ref="inputAreaEl">
|
|
<input
|
|
v-if="cellType === 'text'"
|
|
type="text"
|
|
:class="$style.editingInput"
|
|
:value="editingValue"
|
|
@input="onInputText"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</td>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { computed, nextTick, ref, toRefs, watch } from 'vue';
|
|
import {
|
|
CellAddress,
|
|
CellValue,
|
|
equalCellAddress,
|
|
getCellAddress,
|
|
GridCell,
|
|
GridEventEmitter, Size,
|
|
} from '@/components/grid/types.js';
|
|
|
|
const emit = defineEmits<{
|
|
(ev: 'operation:beginEdit', sender: GridCell): void;
|
|
(ev: 'operation:endEdit', sender: GridCell): void;
|
|
(ev: 'operation:selectionMove', sender: GridCell, next: CellAddress): void;
|
|
(ev: 'change:value', sender: GridCell, newValue: CellValue): void;
|
|
(ev: 'change:contentSize', sender: GridCell, newSize: Size): void;
|
|
}>();
|
|
const props = defineProps<{
|
|
cell: GridCell,
|
|
bus: GridEventEmitter,
|
|
}>();
|
|
|
|
const { cell, bus } = toRefs(props);
|
|
|
|
const rootEl = ref<InstanceType<typeof HTMLTableCellElement>>();
|
|
const contentAreaEl = ref<InstanceType<typeof HTMLDivElement>>();
|
|
const inputAreaEl = ref<InstanceType<typeof HTMLDivElement>>();
|
|
|
|
const editing = ref<boolean>(false);
|
|
const editingValue = ref<CellValue>(undefined);
|
|
|
|
const cellWidth = computed(() => cell.value.column.width);
|
|
const cellType = computed(() => cell.value.column.setting.type);
|
|
const needsContentCentering = computed(() => {
|
|
switch (cellType.value) {
|
|
case 'boolean':
|
|
return true;
|
|
default:
|
|
return false;
|
|
}
|
|
});
|
|
|
|
watch(() => [cell, cell.value.value], () => {
|
|
// 中身がセットされた直後はサイズが分からないので、次のタイミングで更新する
|
|
nextTick(emitContentSizeChanged);
|
|
});
|
|
watch(() => cell.value.selected, () => {
|
|
if (cell.value.selected) {
|
|
rootEl.value?.focus();
|
|
}
|
|
});
|
|
|
|
function onCellDoubleClick(ev: MouseEvent) {
|
|
switch (ev.type) {
|
|
case 'dblclick': {
|
|
beginEditing();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
function onOutsideMouseDown(ev: MouseEvent) {
|
|
const isOutside = ev.target instanceof Node && !rootEl.value?.contains(ev.target);
|
|
if (isOutside || !equalCellAddress(cell.value.address, getCellAddress(ev.target as HTMLElement))) {
|
|
endEditing(true);
|
|
}
|
|
}
|
|
|
|
function onCellKeyDown(ev: KeyboardEvent) {
|
|
if (!editing.value) {
|
|
ev.preventDefault();
|
|
switch (ev.code) {
|
|
case 'Enter':
|
|
case 'F2': {
|
|
beginEditing();
|
|
break;
|
|
}
|
|
}
|
|
} else {
|
|
switch (ev.code) {
|
|
case 'Escape': {
|
|
endEditing(false);
|
|
break;
|
|
}
|
|
case 'Enter': {
|
|
if (!ev.isComposing) {
|
|
endEditing(true);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function onInputText(ev: Event) {
|
|
editingValue.value = (ev.target as HTMLInputElement).value;
|
|
}
|
|
|
|
function registerOutsideMouseDown() {
|
|
unregisterOutsideMouseDown();
|
|
addEventListener('mousedown', onOutsideMouseDown);
|
|
}
|
|
|
|
function unregisterOutsideMouseDown() {
|
|
removeEventListener('mousedown', onOutsideMouseDown);
|
|
}
|
|
|
|
function beginEditing() {
|
|
if (editing.value || !cell.value.column.setting.editable) {
|
|
return;
|
|
}
|
|
|
|
switch (cellType.value) {
|
|
case 'text': {
|
|
editingValue.value = cell.value.value;
|
|
editing.value = true;
|
|
registerOutsideMouseDown();
|
|
emit('operation:beginEdit', cell.value);
|
|
|
|
nextTick(() => {
|
|
// inputの展開後にフォーカスを当てたい
|
|
if (inputAreaEl.value) {
|
|
(inputAreaEl.value.querySelector('*') as HTMLElement).focus();
|
|
}
|
|
});
|
|
break;
|
|
}
|
|
case 'boolean': {
|
|
// とくに特殊なUIは設けず、トグルするだけ
|
|
emitValueChange(!cell.value.value);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
function endEditing(applyValue: boolean) {
|
|
if (!editing.value) {
|
|
return;
|
|
}
|
|
|
|
emit('operation:endEdit', cell.value);
|
|
unregisterOutsideMouseDown();
|
|
|
|
if (applyValue) {
|
|
emitValueChange(editingValue.value);
|
|
}
|
|
|
|
editingValue.value = undefined;
|
|
editing.value = false;
|
|
|
|
rootEl.value?.focus();
|
|
}
|
|
|
|
function emitValueChange(newValue: CellValue) {
|
|
emit('change:value', cell.value, newValue);
|
|
}
|
|
|
|
function emitContentSizeChanged() {
|
|
emit('change:contentSize', cell.value, {
|
|
width: contentAreaEl.value?.clientWidth ?? 0,
|
|
height: contentAreaEl.value?.clientHeight ?? 0,
|
|
});
|
|
}
|
|
|
|
</script>
|
|
|
|
<style module lang="scss">
|
|
$cellHeight: 28px;
|
|
|
|
.cell {
|
|
overflow: hidden;
|
|
white-space: nowrap;
|
|
height: $cellHeight;
|
|
max-height: $cellHeight;
|
|
min-height: $cellHeight;
|
|
border-left: solid 0.5px var(--divider);
|
|
border-top: solid 0.5px var(--divider);
|
|
cursor: cell;
|
|
|
|
&:focus {
|
|
outline: none;
|
|
}
|
|
}
|
|
|
|
.root {
|
|
display: flex;
|
|
flex-direction: row;
|
|
align-items: center;
|
|
box-sizing: border-box;
|
|
height: 100%;
|
|
|
|
// selected適用時に中身がズレてしまうので、透明の線をあらかじめ引いておきたい
|
|
border: solid 0.5px transparent;
|
|
|
|
&.selected {
|
|
border: solid 0.5px var(--accentLighten);
|
|
}
|
|
|
|
&.ranged {
|
|
background-color: var(--accentedBg);
|
|
}
|
|
|
|
&.center {
|
|
justify-content: center;
|
|
}
|
|
}
|
|
|
|
.content {
|
|
display: inline-block;
|
|
padding: 0 8px;
|
|
}
|
|
|
|
.viewImage {
|
|
width: auto;
|
|
max-height: $cellHeight;
|
|
height: $cellHeight;
|
|
object-fit: cover;
|
|
}
|
|
|
|
.editingInput {
|
|
padding: 0 8px;
|
|
width: 100%;
|
|
max-width: 100%;
|
|
box-sizing: border-box;
|
|
min-height: $cellHeight;
|
|
max-height: $cellHeight;
|
|
height: $cellHeight + 4px;
|
|
outline: none;
|
|
border: none;
|
|
font-family: 'Hiragino Maru Gothic Pro', "BIZ UDGothic", Roboto, HelveticaNeue, Arial, sans-serif;
|
|
}
|
|
|
|
</style>
|