diff options
Diffstat (limited to 'packages/frontend/src/components/grid/MkHeaderCell.vue')
| -rw-r--r-- | packages/frontend/src/components/grid/MkHeaderCell.vue | 216 |
1 files changed, 216 insertions, 0 deletions
diff --git a/packages/frontend/src/components/grid/MkHeaderCell.vue b/packages/frontend/src/components/grid/MkHeaderCell.vue new file mode 100644 index 0000000000..605d27c6d6 --- /dev/null +++ b/packages/frontend/src/components/grid/MkHeaderCell.vue @@ -0,0 +1,216 @@ +<!-- +SPDX-FileCopyrightText: syuilo and other misskey contributors +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<div + ref="rootEl" + class="mk_grid_th" + :class="$style.cell" + :style="[{ maxWidth: column.width, minWidth: column.width, width: column.width }]" + data-grid-cell + :data-grid-cell-row="-1" + :data-grid-cell-col="column.index" +> + <div :class="$style.root"> + <div :class="$style.left"/> + <div :class="$style.wrapper"> + <div ref="contentEl" :class="$style.contentArea"> + <span v-if="column.setting.icon" class="ti" :class="column.setting.icon" style="line-height: normal"/> + <span v-else>{{ text }}</span> + </div> + </div> + <div + :class="$style.right" + @mousedown="onHandleMouseDown" + @dblclick="onHandleDoubleClick" + /> + </div> +</div> +</template> + +<script setup lang="ts"> +import { computed, nextTick, onMounted, onUnmounted, ref, toRefs, watch } from 'vue'; +import { GridEventEmitter, Size } from '@/components/grid/grid.js'; +import { GridColumn } from '@/components/grid/column.js'; + +const emit = defineEmits<{ + (ev: 'operation:beginWidthChange', sender: GridColumn): void; + (ev: 'operation:endWidthChange', sender: GridColumn): void; + (ev: 'operation:widthLargest', sender: GridColumn): void; + (ev: 'change:width', sender: GridColumn, width: string): void; + (ev: 'change:contentSize', sender: GridColumn, newSize: Size): void; +}>(); +const props = defineProps<{ + column: GridColumn, + bus: GridEventEmitter, +}>(); + +const { column, bus } = toRefs(props); + +const rootEl = ref<InstanceType<typeof HTMLTableCellElement>>(); +const contentEl = ref<InstanceType<typeof HTMLDivElement>>(); + +const resizing = ref<boolean>(false); + +const text = computed(() => { + const result = column.value.setting.title ?? column.value.setting.bindTo; + return result.length > 0 ? result : ' '; +}); + +watch(column, () => { + // 中身がセットされた直後はサイズが分からないので、次のタイミングで更新する + nextTick(emitContentSizeChanged); +}, { immediate: true }); + +function onHandleDoubleClick(ev: MouseEvent) { + switch (ev.type) { + case 'dblclick': { + emit('operation:widthLargest', column.value); + break; + } + } +} + +function onHandleMouseDown(ev: MouseEvent) { + switch (ev.type) { + case 'mousedown': { + if (!resizing.value) { + registerHandleMouseUp(); + registerHandleMouseMove(); + resizing.value = true; + emit('operation:beginWidthChange', column.value); + } + break; + } + } +} + +function onHandleMouseMove(ev: MouseEvent) { + if (!rootEl.value) { + // 型ガード + return; + } + + switch (ev.type) { + case 'mousemove': { + if (resizing.value) { + const bounds = rootEl.value.getBoundingClientRect(); + const clientWidth = rootEl.value.clientWidth; + const clientRight = bounds.left + clientWidth; + const nextWidth = clientWidth + (ev.clientX - clientRight); + emit('change:width', column.value, `${nextWidth}px`); + } + break; + } + } +} + +function onHandleMouseUp(ev: MouseEvent) { + switch (ev.type) { + case 'mouseup': { + if (resizing.value) { + unregisterHandleMouseUp(); + unregisterHandleMouseMove(); + resizing.value = false; + emit('operation:endWidthChange', column.value); + } + break; + } + } +} + +function onForceRefreshContentSize() { + emitContentSizeChanged(); +} + +function registerHandleMouseMove() { + unregisterHandleMouseMove(); + addEventListener('mousemove', onHandleMouseMove); +} + +function unregisterHandleMouseMove() { + removeEventListener('mousemove', onHandleMouseMove); +} + +function registerHandleMouseUp() { + unregisterHandleMouseUp(); + addEventListener('mouseup', onHandleMouseUp); +} + +function unregisterHandleMouseUp() { + removeEventListener('mouseup', onHandleMouseUp); +} + +function emitContentSizeChanged() { + const clientWidth = contentEl.value?.clientWidth ?? 0; + const clientHeight = contentEl.value?.clientHeight ?? 0; + emit('change:contentSize', column.value, { + // バーの横幅も考慮したいので、+3px + width: clientWidth + 3 + 3, + height: clientHeight, + }); +} + +onMounted(() => { + bus.value.on('forceRefreshContentSize', onForceRefreshContentSize); +}); + +onUnmounted(() => { + bus.value.off('forceRefreshContentSize', onForceRefreshContentSize); +}); + +</script> + +<style module lang="scss"> +$handleWidth: 5px; +$cellHeight: 28px; + +.cell { + cursor: pointer; +} + +.root { + display: flex; + flex-direction: row; + height: $cellHeight; + max-height: $cellHeight; + min-height: $cellHeight; + + .wrapper { + flex: 1; + display: flex; + flex-direction: row; + overflow: hidden; + justify-content: center; + } + + .contentArea { + display: flex; + padding: 6px 4px; + box-sizing: border-box; + overflow: hidden; + white-space: nowrap; + text-align: center; + } + + .left { + // rightのぶんだけズレるのでそれを相殺するためのネガティブマージン + margin-left: -$handleWidth; + margin-right: auto; + width: $handleWidth; + min-width: $handleWidth; + } + + .right { + margin-left: auto; + // 判定を罫線の上に重ねたいのでネガティブマージンを使う + margin-right: -$handleWidth; + width: $handleWidth; + min-width: $handleWidth; + cursor: w-resize; + z-index: 1; + } +} +</style> |