wip
This commit is contained in:
parent
07efd85ffd
commit
e39ba6286f
|
@ -27,7 +27,7 @@
|
|||
"@syuilo/aiscript": "0.17.0",
|
||||
"@tabler/icons-webfont": "2.44.0",
|
||||
"@twemoji/parser": "15.0.0",
|
||||
"@vitejs/plugin-vue": "5.0.2",
|
||||
"@vitejs/plugin-vue": "5.0.3",
|
||||
"@vue/compiler-sfc": "3.4.3",
|
||||
"aiscript-vscode": "github:aiscript-dev/aiscript-vscode#v0.0.6",
|
||||
"astring": "1.8.6",
|
||||
|
|
|
@ -20,7 +20,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
>
|
||||
<template v-for="media in mediaList.filter(media => previewable(media))">
|
||||
<XVideo v-if="media.type.startsWith('video')" :key="`video:${media.id}`" :class="$style.media" :video="media"/>
|
||||
<XImage v-else-if="media.type.startsWith('image')" :key="`image:${media.id}`" :class="$style.media" class="image" :data-id="media.id" :image="media" :raw="raw"/>
|
||||
<XImage v-else-if="media.type.startsWith('image')" :key="`image:${media.id}`" :class="$style.media" class="viewImage" :data-id="media.id" :image="media" :raw="raw"/>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -90,7 +90,7 @@ const exampleCWNote = reactive<Misskey.entities.Note>({
|
|||
background: var(--divider);
|
||||
}
|
||||
|
||||
.image {
|
||||
.viewImage {
|
||||
max-width: 300px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
|
|
@ -100,7 +100,7 @@ const exampleNote = reactive<Misskey.entities.Note>({
|
|||
background: var(--divider);
|
||||
}
|
||||
|
||||
.image {
|
||||
.viewImage {
|
||||
max-width: 300px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
|
|
@ -22,7 +22,6 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<a href="https://misskey-hub.net/docs/for-users/features/timeline/" target="_blank" class="_link">{{ i18n.ts.help }}</a>
|
||||
</template>
|
||||
</I18n>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -42,7 +41,7 @@ import { i18n } from '@/i18n.js';
|
|||
background: var(--divider);
|
||||
}
|
||||
|
||||
.image {
|
||||
.viewImage {
|
||||
max-width: 300px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
|
|
@ -1,26 +0,0 @@
|
|||
<template>
|
||||
<div
|
||||
class="cell"
|
||||
:style="{ width: width + 'px' }"
|
||||
>
|
||||
<span>{{ cell.value }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { toRefs } from 'vue';
|
||||
import { GridCell } from '@/components/grid/types.js';
|
||||
|
||||
const props = defineProps<{ cell: GridCell }>();
|
||||
const { cell } = toRefs(props);
|
||||
const width = cell.value.columnSetting.width;
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.cell {
|
||||
display: inline-block;
|
||||
padding: 4px 8px;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,304 @@
|
|||
<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,
|
||||
} from '@/components/grid/types.js';
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'edit:begin', sender: GridCell): void;
|
||||
(ev: 'edit:end', sender: GridCell): void;
|
||||
(ev: 'selection:move', sender: GridCell, next: CellAddress): 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(cellWidth, updateContentSize);
|
||||
watch(() => [cell, cell.value.value], () => {
|
||||
// 中身がセットされた直後はサイズが分からないので、次のタイミングで更新する
|
||||
nextTick(updateContentSize);
|
||||
});
|
||||
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) {
|
||||
switch (ev.code) {
|
||||
case 'Enter':
|
||||
case 'F2': {
|
||||
beginEditing();
|
||||
break;
|
||||
}
|
||||
case 'ArrowRight': {
|
||||
const next = {
|
||||
col: cell.value.address.col + 1,
|
||||
row: cell.value.address.row,
|
||||
};
|
||||
emit('selection:move', cell.value, next);
|
||||
break;
|
||||
}
|
||||
case 'ArrowLeft': {
|
||||
const next = {
|
||||
col: cell.value.address.col - 1,
|
||||
row: cell.value.address.row,
|
||||
};
|
||||
emit('selection:move', cell.value, next);
|
||||
break;
|
||||
}
|
||||
case 'ArrowUp': {
|
||||
const next = {
|
||||
col: cell.value.address.col,
|
||||
row: cell.value.address.row - 1,
|
||||
};
|
||||
emit('selection:move', cell.value, next);
|
||||
break;
|
||||
}
|
||||
case 'ArrowDown': {
|
||||
const next = {
|
||||
col: cell.value.address.col,
|
||||
row: cell.value.address.row + 1,
|
||||
};
|
||||
emit('selection:move', cell.value, next);
|
||||
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('edit:begin', cell.value);
|
||||
|
||||
nextTick(() => {
|
||||
if (inputAreaEl.value) {
|
||||
(inputAreaEl.value.querySelector('*') as HTMLElement).focus();
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'boolean': {
|
||||
// とくに特殊なUIは設けず、トグルするだけ
|
||||
cell.value.value = !cell.value.value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function endEditing(applyValue: boolean) {
|
||||
if (!editing.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
emit('edit:end', cell.value);
|
||||
unregisterOutsideMouseDown();
|
||||
|
||||
if (applyValue) {
|
||||
cell.value.value = editingValue.value;
|
||||
}
|
||||
editing.value = false;
|
||||
|
||||
rootEl.value?.focus();
|
||||
}
|
||||
|
||||
function updateContentSize() {
|
||||
cell.value.contentSize = {
|
||||
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>
|
|
@ -0,0 +1,48 @@
|
|||
<template>
|
||||
<tr :class="$style.row">
|
||||
<MkNumberCell
|
||||
:content="(row.index + 1).toString()"
|
||||
:selectable="true"
|
||||
:row="row"
|
||||
@selection:row="(sender) => emit('selection:row', sender)"
|
||||
/>
|
||||
<MkDataCell
|
||||
v-for="cell in cells"
|
||||
:key="cell.address.col"
|
||||
:cell="cell"
|
||||
:bus="bus"
|
||||
@edit:begin="(sender) => emit('edit:begin', sender)"
|
||||
@edit:end="(sender) => emit('edit:end', sender)"
|
||||
@selection:move="(sender, next) => emit('selection:move', sender, next)"
|
||||
/>
|
||||
</tr>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, toRefs } from 'vue';
|
||||
import { CellAddress, GridCell, GridEventEmitter, GridRow } from '@/components/grid/types.js';
|
||||
import MkDataCell from '@/components/grid/MkDataCell.vue';
|
||||
import MkNumberCell from '@/components/grid/MkNumberCell.vue';
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'edit:begin', sender: GridCell): void;
|
||||
(ev: 'edit:end', sender: GridCell): void;
|
||||
(ev: 'selection:move', sender: GridCell, next: CellAddress): void;
|
||||
(ev: 'selection:row', sender: GridRow): void;
|
||||
}>();
|
||||
const props = defineProps<{
|
||||
row: GridRow,
|
||||
cells: GridCell[],
|
||||
bus: GridEventEmitter,
|
||||
}>();
|
||||
|
||||
const { cells } = toRefs(props);
|
||||
const last = computed(() => cells.value[cells.value.length - 1]);
|
||||
|
||||
</script>
|
||||
|
||||
<style module lang="scss">
|
||||
.row {
|
||||
|
||||
}
|
||||
</style>
|
|
@ -1,23 +1,56 @@
|
|||
<template>
|
||||
<div :style="$style.grid">
|
||||
<div :class="$style.header">
|
||||
<div
|
||||
v-for="column in columns"
|
||||
:key="column.index"
|
||||
:class="$style.cell"
|
||||
:style="{ width: column.setting.width + 'px'}"
|
||||
>
|
||||
{{ column.setting.title ?? column.setting.bindTo }}
|
||||
</div>
|
||||
</div>
|
||||
<MkRow v-for="row in rows" :key="row.index" :row="row"/>
|
||||
</div>
|
||||
<table
|
||||
:class="$style.grid"
|
||||
@mousedown="onMouseDown"
|
||||
@mouseup="onMouseUp"
|
||||
@mousemove="onMouseMove"
|
||||
>
|
||||
<thead>
|
||||
<MkHeaderRow
|
||||
:columns="columns"
|
||||
:bus="bus"
|
||||
@width:beginChange="onHeaderCellWidthBeginChange"
|
||||
@width:endChange="onHeaderCellWidthEndChange"
|
||||
@width:changing="onHeaderCellWidthChanging"
|
||||
@width:largest="onHeaderCellWidthLargest"
|
||||
@selection:column="onSelectionColumn"
|
||||
/>
|
||||
</thead>
|
||||
<tbody>
|
||||
<MkDataRow
|
||||
v-for="row in rows"
|
||||
:key="row.index"
|
||||
:row="row"
|
||||
:cells="cells[row.index]"
|
||||
:bus="bus"
|
||||
@edit:begin="onCellEditBegin"
|
||||
@edit:end="onCellEditEnd"
|
||||
@selection:move="onSelectionMove"
|
||||
@selection:row="onSelectionRow"
|
||||
/>
|
||||
</tbody>
|
||||
</table>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, toRefs, watch } from 'vue';
|
||||
import MkRow from '@/components/grid/MkRow.vue';
|
||||
import { ColumnSetting, DataSource, GridCell, GridColumn, GridRow } from '@/components/grid/types.js';
|
||||
import { computed, ref, toRefs, watch } from 'vue';
|
||||
import {
|
||||
calcCellWidth,
|
||||
CELL_ADDRESS_NONE,
|
||||
CellAddress,
|
||||
ColumnSetting,
|
||||
DataSource,
|
||||
equalCellAddress,
|
||||
getCellAddress,
|
||||
GridCell,
|
||||
GridColumn,
|
||||
GridEventEmitter,
|
||||
GridRow,
|
||||
GridState,
|
||||
isCellElement,
|
||||
} from '@/components/grid/types.js';
|
||||
import MkDataRow from '@/components/grid/MkDataRow.vue';
|
||||
import MkHeaderRow from '@/components/grid/MkHeaderRow.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
columnSettings: ColumnSetting[],
|
||||
|
@ -25,11 +58,230 @@ const props = defineProps<{
|
|||
}>();
|
||||
|
||||
const { columnSettings, data } = toRefs(props);
|
||||
const columns = ref<GridColumn[]>();
|
||||
const rows = ref<GridRow[]>();
|
||||
|
||||
const columns = ref<GridColumn[]>([]);
|
||||
const rows = ref<GridRow[]>([]);
|
||||
const cells = ref<GridCell[][]>([]);
|
||||
const rangedCells = computed(() => cells.value.flat().filter(it => it.ranged));
|
||||
const previousCellAddress = ref<CellAddress>(CELL_ADDRESS_NONE);
|
||||
const editingCellAddress = ref<CellAddress>(CELL_ADDRESS_NONE);
|
||||
|
||||
const state = ref<GridState>('normal');
|
||||
const bus = new GridEventEmitter();
|
||||
|
||||
watch(columnSettings, refreshColumnsSetting);
|
||||
watch(data, refreshData);
|
||||
watch(rangedCells, () => {
|
||||
if (rangedCells.value.length <= 1) {
|
||||
// 範囲セルが1以下の場合、以下の計算が無駄になるのでやらないようにする
|
||||
return;
|
||||
}
|
||||
|
||||
const min = rangedCells.value.reduce(
|
||||
(acc, value) => {
|
||||
return {
|
||||
col: Math.min(acc.col, value.address.col),
|
||||
row: Math.min(acc.row, value.address.row),
|
||||
};
|
||||
},
|
||||
rangedCells.value[0].address,
|
||||
);
|
||||
|
||||
const max = rangedCells.value.reduce(
|
||||
(acc, value) => {
|
||||
return {
|
||||
col: Math.max(acc.col, value.address.col),
|
||||
row: Math.max(acc.row, value.address.row),
|
||||
};
|
||||
},
|
||||
rangedCells.value[0].address,
|
||||
);
|
||||
|
||||
expandRange(min, max);
|
||||
});
|
||||
|
||||
if (_DEV_) {
|
||||
watch(state, (value) => {
|
||||
console.log(`state: ${value}`);
|
||||
});
|
||||
}
|
||||
|
||||
function onMouseDown(ev: MouseEvent) {
|
||||
const cellAddress = getCellAddress(ev.target as HTMLElement);
|
||||
switch (state.value) {
|
||||
case 'cellEditing': {
|
||||
if (availableCellAddress(cellAddress) && !equalCellAddress(editingCellAddress.value, cellAddress)) {
|
||||
selectionCell(cellAddress);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'normal': {
|
||||
if (availableCellAddress(cellAddress)) {
|
||||
selectionCell(cellAddress);
|
||||
state.value = 'cellSelecting';
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function onMouseUp() {
|
||||
switch (state.value) {
|
||||
case 'cellSelecting': {
|
||||
state.value = 'normal';
|
||||
previousCellAddress.value = CELL_ADDRESS_NONE;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function onMouseMove(ev: MouseEvent) {
|
||||
switch (state.value) {
|
||||
case 'cellSelecting': {
|
||||
if (isCellElement(ev.target)) {
|
||||
const address = getCellAddress(ev.target);
|
||||
if (!equalCellAddress(previousCellAddress.value, address)) {
|
||||
if (isCellElement(ev.target)) {
|
||||
selectionRange(getCellAddress(ev.target));
|
||||
}
|
||||
|
||||
previousCellAddress.value = address;
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function onCellEditBegin(sender: GridCell) {
|
||||
state.value = 'cellEditing';
|
||||
editingCellAddress.value = sender.address;
|
||||
for (const cell of cells.value.flat()) {
|
||||
if (cell.address.col !== sender.address.col || cell.address.row !== sender.address.row) {
|
||||
// 編集状態となったセル以外は全部選択解除
|
||||
cell.selected = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function onCellEditEnd() {
|
||||
editingCellAddress.value = CELL_ADDRESS_NONE;
|
||||
state.value = 'normal';
|
||||
}
|
||||
|
||||
function onSelectionMove(_: GridCell, next: CellAddress) {
|
||||
if (availableCellAddress(next)) {
|
||||
selectionCell(next);
|
||||
}
|
||||
}
|
||||
|
||||
function onHeaderCellWidthBeginChange(_: GridColumn) {
|
||||
switch (state.value) {
|
||||
case 'normal': {
|
||||
state.value = 'colResizing';
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function onHeaderCellWidthEndChange(_: GridColumn) {
|
||||
switch (state.value) {
|
||||
case 'colResizing': {
|
||||
state.value = 'normal';
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function onHeaderCellWidthChanging(sender: GridColumn, width: string) {
|
||||
switch (state.value) {
|
||||
case 'colResizing': {
|
||||
const column = columns.value[sender.index];
|
||||
column.width = width;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function onHeaderCellWidthLargest(sender: GridColumn) {
|
||||
switch (state.value) {
|
||||
case 'normal': {
|
||||
const column = columns.value[sender.index];
|
||||
const _cells = cells.value;
|
||||
const largestColumnWidth = columns.value.reduce(
|
||||
(acc, value) => Math.max(acc, value.contentSize.width),
|
||||
columns.value[sender.index].contentSize.width,
|
||||
);
|
||||
const largestCellWidth = _cells
|
||||
.map(row => row[column.index])
|
||||
.reduce(
|
||||
(acc, value) => Math.max(acc, value.contentSize.width),
|
||||
_cells[0][column.index].contentSize.width,
|
||||
);
|
||||
column.width = `${Math.max(largestColumnWidth, largestCellWidth)}px`;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function onSelectionColumn(sender: GridColumn) {
|
||||
unSelectionRange();
|
||||
|
||||
const targets = cells.value.map(row => row[sender.index].address);
|
||||
selectionRange(...targets);
|
||||
}
|
||||
|
||||
function onSelectionRow(sender: GridRow) {
|
||||
unSelectionRange();
|
||||
|
||||
const targets = cells.value[sender.index].map(cell => cell.address);
|
||||
selectionRange(...targets);
|
||||
}
|
||||
|
||||
function selectionCell(target: CellAddress) {
|
||||
const _cells = cells.value;
|
||||
|
||||
for (const row of cells.value) {
|
||||
for (const cell of row) {
|
||||
cell.selected = false;
|
||||
cell.ranged = false;
|
||||
}
|
||||
}
|
||||
|
||||
_cells[target.row][target.col].selected = true;
|
||||
_cells[target.row][target.col].ranged = true;
|
||||
}
|
||||
|
||||
function selectionRange(...targets: CellAddress[]) {
|
||||
const _cells = cells.value;
|
||||
for (const target of targets) {
|
||||
_cells[target.row][target.col].ranged = true;
|
||||
}
|
||||
}
|
||||
|
||||
function unSelectionRange() {
|
||||
const _cells = cells.value;
|
||||
for (const row of _cells) {
|
||||
for (const cell of row) {
|
||||
cell.selected = false;
|
||||
cell.ranged = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function expandRange(min: CellAddress, max: CellAddress) {
|
||||
const targetRows = cells.value.slice(min.row, max.row + 1);
|
||||
for (const row of targetRows) {
|
||||
for (const cell of row.slice(min.col, max.col + 1)) {
|
||||
cell.ranged = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function availableCellAddress(cellAddress: CellAddress): boolean {
|
||||
return cellAddress.row >= 0 && cellAddress.col >= 0 && cellAddress.row < rows.value.length && cellAddress.col < columns.value.length;
|
||||
}
|
||||
|
||||
function refreshColumnsSetting() {
|
||||
const bindToList = columnSettings.value.map(it => it.bindTo);
|
||||
|
@ -41,31 +293,44 @@ function refreshColumnsSetting() {
|
|||
}
|
||||
|
||||
function refreshData() {
|
||||
const _settings = columnSettings.value;
|
||||
const _data = data.value;
|
||||
const _rows = _data.map((_, index) => ({ index, cells: Array.of<GridCell>() }));
|
||||
const _columns = columnSettings.value.map((setting, index) => ({ index, setting, cells: Array.of<GridCell>() }));
|
||||
const _data: DataSource[] = data.value;
|
||||
const _rows: GridRow[] = _data.map((_, index) => ({
|
||||
index,
|
||||
}));
|
||||
const _columns: GridColumn[] = columnSettings.value.map((setting, index) => ({
|
||||
index,
|
||||
setting,
|
||||
width: calcCellWidth(setting.width),
|
||||
contentSize: { width: 0, height: 0 },
|
||||
}));
|
||||
const _cells = Array.of<GridCell[]>();
|
||||
|
||||
for (const [rowIndex, row] of _rows.entries()) {
|
||||
const rowCells = Array.of<GridCell>();
|
||||
for (const [colIndex, column] of _columns.entries()) {
|
||||
if (!(column.setting.bindTo in _data[rowIndex])) {
|
||||
continue;
|
||||
}
|
||||
const value = _data[rowIndex][column.setting.bindTo];
|
||||
const value = (column.setting.bindTo in _data[rowIndex])
|
||||
? _data[rowIndex][column.setting.bindTo]
|
||||
: undefined;
|
||||
|
||||
const cell = {
|
||||
const cell: GridCell = {
|
||||
address: { col: colIndex, row: rowIndex },
|
||||
value,
|
||||
columnSetting: _settings[colIndex],
|
||||
column: column,
|
||||
row: row,
|
||||
selected: false,
|
||||
ranged: false,
|
||||
contentSize: { width: 0, height: 0 },
|
||||
};
|
||||
|
||||
row.cells.push(cell);
|
||||
column.cells.push(cell);
|
||||
rowCells.push(cell);
|
||||
}
|
||||
|
||||
_cells.push(rowCells);
|
||||
}
|
||||
|
||||
rows.value = _rows;
|
||||
columns.value = _columns;
|
||||
cells.value = _cells;
|
||||
}
|
||||
|
||||
refreshColumnsSetting();
|
||||
|
@ -75,24 +340,13 @@ refreshData();
|
|||
|
||||
<style module lang="scss">
|
||||
.grid {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
flex-direction: row;
|
||||
overflow: scroll;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
table-layout: fixed;
|
||||
width: fit-content;
|
||||
user-select: none;
|
||||
|
||||
> .cell {
|
||||
display: block;
|
||||
padding: 4px 8px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
text-align: center;
|
||||
}
|
||||
border: solid 0.5px var(--divider);
|
||||
border-spacing: 0;
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -0,0 +1,195 @@
|
|||
<template>
|
||||
<th
|
||||
ref="rootEl"
|
||||
:class="$style.cell"
|
||||
:style="[{ width: column.width }]"
|
||||
>
|
||||
<div :class="$style.root">
|
||||
<div :class="$style.left"/>
|
||||
<div :class="$style.wrapper" @mouseup="onContentMouseUp">
|
||||
<div ref="contentEl" :class="$style.contentArea">
|
||||
{{ text }}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
:class="$style.right"
|
||||
@mousedown="onHandleMouseDown"
|
||||
@dblclick="onHandleDoubleClick"
|
||||
/>
|
||||
</div>
|
||||
</th>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, ref, toRefs, watch } from 'vue';
|
||||
import { GridColumn, GridEventEmitter } from '@/components/grid/types.js';
|
||||
|
||||
const emit = defineEmits<{
|
||||
// ヘッダのサイズ変更系イベントはセル全体の横幅設定に影響するので上位コンポーネントにリレーする必要あり
|
||||
(ev: 'width:begin-change', sender: GridColumn): void;
|
||||
(ev: 'width:end-change', sender: GridColumn): void;
|
||||
(ev: 'width:changing', sender: GridColumn, width: string): void;
|
||||
(ev: 'width:largest', sender: GridColumn): void;
|
||||
(ev: 'selection:column', sender: GridColumn): 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(updateContentSize);
|
||||
});
|
||||
|
||||
function onContentMouseUp(ev: MouseEvent) {
|
||||
switch (ev.type) {
|
||||
case 'mouseup': {
|
||||
emit('selection:column', column.value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function onHandleDoubleClick(ev: MouseEvent) {
|
||||
switch (ev.type) {
|
||||
case 'dblclick': {
|
||||
emit('width:largest', column.value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function onHandleMouseDown(ev: MouseEvent) {
|
||||
switch (ev.type) {
|
||||
case 'mousedown': {
|
||||
if (!resizing.value) {
|
||||
registerHandleMouseUp();
|
||||
registerHandleMouseMove();
|
||||
resizing.value = true;
|
||||
emit('width:begin-change', 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('width:changing', column.value, `${nextWidth}px`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function onHandleMouseUp(ev: MouseEvent) {
|
||||
switch (ev.type) {
|
||||
case 'mouseup': {
|
||||
if (resizing.value) {
|
||||
unregisterHandleMouseUp();
|
||||
unregisterHandleMouseMove();
|
||||
resizing.value = false;
|
||||
emit('width:end-change', column.value);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function registerHandleMouseMove() {
|
||||
unregisterHandleMouseMove();
|
||||
addEventListener('mousemove', onHandleMouseMove);
|
||||
}
|
||||
|
||||
function unregisterHandleMouseMove() {
|
||||
removeEventListener('mousemove', onHandleMouseMove);
|
||||
}
|
||||
|
||||
function registerHandleMouseUp() {
|
||||
unregisterHandleMouseUp();
|
||||
addEventListener('mouseup', onHandleMouseUp);
|
||||
}
|
||||
|
||||
function unregisterHandleMouseUp() {
|
||||
removeEventListener('mouseup', onHandleMouseUp);
|
||||
}
|
||||
|
||||
function updateContentSize() {
|
||||
const clientWidth = contentEl.value?.clientWidth ?? 0;
|
||||
const clientHeight = contentEl.value?.clientHeight ?? 0;
|
||||
column.value.contentSize = {
|
||||
// バーの横幅も考慮したいので、+3px
|
||||
width: clientWidth + 3 + 3,
|
||||
height: clientHeight,
|
||||
};
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style module lang="scss">
|
||||
$handleWidth: 3px;
|
||||
|
||||
.cell {
|
||||
border-left: solid 0.5px var(--divider);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.root {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
height: 100%;
|
||||
|
||||
.wrapper {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
overflow: hidden;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.contentArea {
|
||||
display: flex;
|
||||
padding: 4px 0;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.left {
|
||||
margin-right: auto;
|
||||
width: $handleWidth;
|
||||
min-width: $handleWidth;
|
||||
}
|
||||
|
||||
.right {
|
||||
margin-left: auto;
|
||||
width: $handleWidth;
|
||||
min-width: $handleWidth;
|
||||
cursor: w-resize;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,45 @@
|
|||
<template>
|
||||
<tr :class="$style.header">
|
||||
<MkNumberCell
|
||||
content="#"
|
||||
:selectable="false"
|
||||
:top="true"
|
||||
/>
|
||||
<MkHeaderCell
|
||||
v-for="column in columns"
|
||||
:key="column.index"
|
||||
:column="column"
|
||||
:bus="bus"
|
||||
@width:beginChange="(sender) => emit('width:begin-change', sender)"
|
||||
@width:endChange="(sender) => emit('width:end-change', sender)"
|
||||
@width:changing="(sender, width) => emit('width:changing', sender, width)"
|
||||
@width:largest="(sender) => emit('width:largest', sender)"
|
||||
@selection:column="(sender) => emit('selection:column', sender)"
|
||||
/>
|
||||
</tr>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { GridColumn, GridEventEmitter } from '@/components/grid/types.js';
|
||||
import MkHeaderCell from '@/components/grid/MkHeaderCell.vue';
|
||||
import MkNumberCell from '@/components/grid/MkNumberCell.vue';
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'width:begin-change', sender: GridColumn): void;
|
||||
(ev: 'width:end-change', sender: GridColumn): void;
|
||||
(ev: 'width:changing', sender: GridColumn, width: string): void;
|
||||
(ev: 'width:largest', sender: GridColumn): void;
|
||||
(ev: 'selection:column', sender: GridColumn): void;
|
||||
}>();
|
||||
defineProps<{
|
||||
columns: GridColumn[],
|
||||
bus: GridEventEmitter,
|
||||
}>();
|
||||
|
||||
</script>
|
||||
|
||||
<style module lang="scss">
|
||||
.header {
|
||||
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,46 @@
|
|||
<template>
|
||||
<th :class="[$style.num, [top ? {} : $style.border]]" @mouseup="onMouseUp">
|
||||
{{ content }}
|
||||
</th>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { toRefs } from 'vue';
|
||||
import { GridRow } from '@/components/grid/types.js';
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'selection:row', sender: GridRow): void;
|
||||
}>();
|
||||
|
||||
const props = defineProps<{
|
||||
content: string,
|
||||
row?: GridRow,
|
||||
selectable: boolean,
|
||||
top?: boolean,
|
||||
}>();
|
||||
|
||||
const { content, row, selectable } = toRefs(props);
|
||||
|
||||
function onMouseUp(ev: MouseEvent) {
|
||||
switch (ev.type) {
|
||||
case 'mouseup': {
|
||||
if (selectable.value && row.value) {
|
||||
emit('selection:row', row.value);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style module lang="scss">
|
||||
.num {
|
||||
padding: 0 8px;
|
||||
min-width: 30px;
|
||||
width: 30px;
|
||||
}
|
||||
|
||||
.border {
|
||||
border-top: solid 0.5px var(--divider);
|
||||
}
|
||||
</style>
|
|
@ -1,23 +0,0 @@
|
|||
<template>
|
||||
<div :class="$style.row">
|
||||
<div v-for="cell in row.cells" :key="JSON.stringify(cell.address)">
|
||||
<MkCell :cell="cell"/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import MkCell from '@/components/grid/MkCell.vue';
|
||||
import { GridRow } from '@/components/grid/types.js';
|
||||
|
||||
defineProps<{ row: GridRow }>();
|
||||
|
||||
</script>
|
||||
|
||||
<style module lang="scss">
|
||||
.row {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
flex-direction: row;
|
||||
}
|
||||
</style>
|
|
@ -1,14 +1,38 @@
|
|||
import { EventEmitter } from 'eventemitter3';
|
||||
|
||||
export type CellValue = string | boolean | number | undefined | null
|
||||
|
||||
export type DataSource = Record<string, CellValue>;
|
||||
|
||||
export type GridState = 'normal' | 'cellSelecting' | 'cellEditing' | 'colResizing'
|
||||
|
||||
export type RowState = 'normal' | 'added' | 'deleted'
|
||||
|
||||
export type Size = {
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
export type SizeStyle = number | 'auto' | undefined;
|
||||
|
||||
export type CellAddress = {
|
||||
row: number;
|
||||
col: number;
|
||||
}
|
||||
|
||||
export const CELL_ADDRESS_NONE: CellAddress = {
|
||||
row: -1,
|
||||
col: -1,
|
||||
};
|
||||
|
||||
export type GridCell = {
|
||||
address: CellAddress;
|
||||
value: CellValue;
|
||||
columnSetting: ColumnSetting;
|
||||
column: GridColumn;
|
||||
row: GridRow;
|
||||
selected: boolean;
|
||||
ranged: boolean;
|
||||
contentSize: Size;
|
||||
}
|
||||
|
||||
export type ColumnType = 'text' | 'number' | 'date' | 'boolean' | 'image';
|
||||
|
@ -17,19 +41,70 @@ export type ColumnSetting = {
|
|||
bindTo: string;
|
||||
title?: string;
|
||||
type: ColumnType;
|
||||
width?: number | 'auto';
|
||||
width: SizeStyle;
|
||||
editable?: boolean;
|
||||
};
|
||||
|
||||
export type DataSource = Record<string, CellValue>;
|
||||
|
||||
export type GridColumn = {
|
||||
index: number;
|
||||
setting: ColumnSetting;
|
||||
cells: GridCell[];
|
||||
width: string;
|
||||
contentSize: Size;
|
||||
}
|
||||
|
||||
export type GridRow = {
|
||||
index: number;
|
||||
cells: GridCell[];
|
||||
}
|
||||
|
||||
export class GridEventEmitter extends EventEmitter<{}> {
|
||||
}
|
||||
|
||||
export function isElement(elem: any): elem is HTMLElement {
|
||||
return elem instanceof HTMLElement;
|
||||
}
|
||||
|
||||
export function isCellElement(elem: any): elem is HTMLTableCellElement {
|
||||
return elem instanceof HTMLTableCellElement;
|
||||
}
|
||||
|
||||
export function isRowElement(elem: any): elem is HTMLTableRowElement {
|
||||
return elem instanceof HTMLTableRowElement;
|
||||
}
|
||||
|
||||
export function getCellAddress(elem: HTMLElement, parentNodeCount = 5): CellAddress {
|
||||
let node = elem;
|
||||
for (let i = 0; i < parentNodeCount; i++) {
|
||||
if (isCellElement(node) && isRowElement(node.parentElement)) {
|
||||
return {
|
||||
// ヘッダ行ぶんを除く
|
||||
row: node.parentElement.rowIndex - 1,
|
||||
// 数値列ぶんを除く
|
||||
col: node.cellIndex - 1,
|
||||
};
|
||||
}
|
||||
|
||||
if (!node.parentElement) {
|
||||
break;
|
||||
}
|
||||
|
||||
node = node.parentElement;
|
||||
}
|
||||
|
||||
throw new Error('Cannot get cell address');
|
||||
}
|
||||
|
||||
export function equalCellAddress(a: CellAddress, b: CellAddress): boolean {
|
||||
return a.row === b.row && a.col === b.col;
|
||||
}
|
||||
|
||||
export function calcCellWidth(widthSetting: SizeStyle): string {
|
||||
switch (widthSetting) {
|
||||
case undefined:
|
||||
case 'auto': {
|
||||
return 'auto';
|
||||
}
|
||||
default: {
|
||||
return `${widthSetting}px`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -43,7 +43,18 @@ export class GridItem {
|
|||
}
|
||||
|
||||
static ofEmojiDetailed(it: Misskey.entities.EmojiDetailed): GridItem {
|
||||
return new GridItem(it.id, it.url, undefined, it.name, it.category ?? '', it.aliases.join(', '), it.license ?? '', it.isSensitive, it.localOnly, it.roleIdsThatCanBeUsedThisEmojiAsReaction.join(', '));
|
||||
return new GridItem(
|
||||
it.id,
|
||||
it.url,
|
||||
undefined,
|
||||
it.name,
|
||||
it.category ?? '',
|
||||
it.aliases.join(', '),
|
||||
it.license ?? '',
|
||||
it.isSensitive,
|
||||
it.localOnly,
|
||||
it.roleIdsThatCanBeUsedThisEmojiAsReaction.join(', '),
|
||||
);
|
||||
}
|
||||
|
||||
public get edited(): boolean {
|
||||
|
@ -51,7 +62,7 @@ export class GridItem {
|
|||
return JSON.stringify(_this) !== origin;
|
||||
}
|
||||
|
||||
public asRecord(): Record<string, unknown> {
|
||||
return this as Record<string, unknown>;
|
||||
public asRecord(): Record<string, never> {
|
||||
return this as Record<string, never>;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,14 +5,14 @@
|
|||
<MkPageHeader/>
|
||||
</template>
|
||||
<div class="_gaps" :class="$style.root">
|
||||
<MkGrid :data="gridItems" :columnSettings="columnSettings"/>
|
||||
<MkGrid :data="convertedGridItems" :columnSettings="columnSettings"/>
|
||||
</div>
|
||||
</MkStickyContainer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref, watch } from 'vue';
|
||||
import { computed, onMounted, ref, watch } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { GridItem } from '@/pages/admin/custom-emojis-grid.impl.js';
|
||||
|
@ -20,19 +20,19 @@ import MkGrid from '@/components/grid/MkGrid.vue';
|
|||
import { ColumnSetting } from '@/components/grid/types.js';
|
||||
|
||||
const columnSettings: ColumnSetting[] = [
|
||||
{ bindTo: 'url', title: ' ', type: 'image', width: 50 },
|
||||
{ bindTo: 'name', title: 'name', type: 'text', width: 140 },
|
||||
{ bindTo: 'category', title: 'category', type: 'text', width: 140 },
|
||||
{ bindTo: 'aliases', title: 'aliases', type: 'text', width: 140 },
|
||||
{ bindTo: 'license', title: 'license', type: 'text', width: 140 },
|
||||
{ bindTo: 'isSensitive', title: 'sensitive', type: 'text', width: 90 },
|
||||
{ bindTo: 'localOnly', title: 'localOnly', type: 'text', width: 90 },
|
||||
{ bindTo: 'roleIdsThatCanBeUsedThisEmojiAsReaction', title: 'role', type: 'text', width: 140 },
|
||||
{ bindTo: 'url', title: '🎨', type: 'image', editable: false, width: 50 },
|
||||
{ bindTo: 'name', title: 'name', type: 'text', editable: true, width: 140 },
|
||||
{ bindTo: 'category', title: 'category', type: 'text', editable: true, width: 140 },
|
||||
{ bindTo: 'aliases', title: 'aliases', type: 'text', editable: true, width: 140 },
|
||||
{ bindTo: 'license', title: 'license', type: 'text', editable: true, width: 140 },
|
||||
{ bindTo: 'isSensitive', title: 'sensitive', type: 'boolean', editable: true, width: 90 },
|
||||
{ bindTo: 'localOnly', title: 'localOnly', type: 'boolean', editable: true, width: 90 },
|
||||
{ bindTo: 'roleIdsThatCanBeUsedThisEmojiAsReaction', title: 'role', type: 'text', editable: true, width: 140 },
|
||||
];
|
||||
|
||||
const customEmojis = ref<Misskey.entities.EmojiDetailed[]>([]);
|
||||
const gridItems = ref<GridItem[]>([]);
|
||||
// const convertedGridItems = computed<DataSource[]>(() => gridItems.value.map(it => it.asRecord()));
|
||||
const convertedGridItems = computed(() => gridItems.value.map(it => it.asRecord()));
|
||||
|
||||
const refreshCustomEmojis = async () => {
|
||||
customEmojis.value = await misskeyApi('emojis', { detail: true }).then(it => it.emojis);
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
* version: 2024.2.0-beta.3
|
||||
* generatedAt: 2024-01-23T01:22:13.177Z
|
||||
* version: 2024.2.0-beta.4
|
||||
* generatedAt: 2024-01-23T07:38:51.367Z
|
||||
*/
|
||||
|
||||
import type { SwitchCaseResponseType } from '../api.js';
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
* version: 2024.2.0-beta.3
|
||||
* generatedAt: 2024-01-23T01:22:13.175Z
|
||||
* version: 2024.2.0-beta.4
|
||||
* generatedAt: 2024-01-23T07:38:51.365Z
|
||||
*/
|
||||
|
||||
import type {
|
||||
|
@ -366,6 +366,7 @@ import type {
|
|||
InviteLimitResponse,
|
||||
MetaRequest,
|
||||
MetaResponse,
|
||||
EmojisRequest,
|
||||
EmojisResponse,
|
||||
EmojiRequest,
|
||||
EmojiResponse,
|
||||
|
@ -805,7 +806,7 @@ export type Endpoints = {
|
|||
'invite/list': { req: InviteListRequest; res: InviteListResponse };
|
||||
'invite/limit': { req: EmptyRequest; res: InviteLimitResponse };
|
||||
'meta': { req: MetaRequest; res: MetaResponse };
|
||||
'emojis': { req: EmptyRequest; res: EmojisResponse };
|
||||
'emojis': { req: EmojisRequest; res: EmojisResponse };
|
||||
'emoji': { req: EmojiRequest; res: EmojiResponse };
|
||||
'miauth/gen-token': { req: MiauthGenTokenRequest; res: MiauthGenTokenResponse };
|
||||
'mute/create': { req: MuteCreateRequest; res: EmptyResponse };
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
* version: 2024.2.0-beta.3
|
||||
* generatedAt: 2024-01-23T01:22:13.173Z
|
||||
* version: 2024.2.0-beta.4
|
||||
* generatedAt: 2024-01-23T07:38:51.364Z
|
||||
*/
|
||||
|
||||
import { operations } from './types.js';
|
||||
|
@ -368,6 +368,7 @@ export type InviteListResponse = operations['invite/list']['responses']['200']['
|
|||
export type InviteLimitResponse = operations['invite/limit']['responses']['200']['content']['application/json'];
|
||||
export type MetaRequest = operations['meta']['requestBody']['content']['application/json'];
|
||||
export type MetaResponse = operations['meta']['responses']['200']['content']['application/json'];
|
||||
export type EmojisRequest = operations['emojis']['requestBody']['content']['application/json'];
|
||||
export type EmojisResponse = operations['emojis']['responses']['200']['content']['application/json'];
|
||||
export type EmojiRequest = operations['emoji']['requestBody']['content']['application/json'];
|
||||
export type EmojiResponse = operations['emoji']['responses']['200']['content']['application/json'];
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
* version: 2024.2.0-beta.3
|
||||
* generatedAt: 2024-01-23T01:22:13.172Z
|
||||
* version: 2024.2.0-beta.4
|
||||
* generatedAt: 2024-01-23T07:38:51.363Z
|
||||
*/
|
||||
|
||||
import { components } from './types.js';
|
||||
|
|
|
@ -2,8 +2,8 @@
|
|||
/* eslint @typescript-eslint/no-explicit-any: 0 */
|
||||
|
||||
/*
|
||||
* version: 2024.2.0-beta.3
|
||||
* generatedAt: 2024-01-23T01:22:13.093Z
|
||||
* version: 2024.2.0-beta.4
|
||||
* generatedAt: 2024-01-23T07:38:51.282Z
|
||||
*/
|
||||
|
||||
/**
|
||||
|
@ -19027,12 +19027,19 @@ export type operations = {
|
|||
* **Credential required**: *No*
|
||||
*/
|
||||
emojis: {
|
||||
requestBody: {
|
||||
content: {
|
||||
'application/json': {
|
||||
detail?: boolean | null;
|
||||
};
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description OK (with results) */
|
||||
200: {
|
||||
content: {
|
||||
'application/json': {
|
||||
emojis: components['schemas']['EmojiSimple'][];
|
||||
emojis: (components['schemas']['EmojiSimple'] | components['schemas']['EmojiDetailed'])[];
|
||||
};
|
||||
};
|
||||
};
|
||||
|
|
|
@ -14,8 +14,7 @@
|
|||
},
|
||||
"scripts": {
|
||||
"build": "node ./build.js",
|
||||
"build:tsc": "npm run tsc",
|
||||
"tsc": "npm run tsc-esm && npm run tsc-dts",
|
||||
"build:tsc": "pnpm tsc-esm && pnpm run tsc-dts",
|
||||
"tsc-esm": "tsc --outDir built/esm",
|
||||
"tsc-dts": "tsc --outDir built/dts --declaration true --emitDeclarationOnly true --declarationMap true",
|
||||
"watch": "nodemon -w src -e ts,js,cjs,mjs,json --exec \"pnpm run build:tsc\"",
|
||||
|
|
|
@ -698,8 +698,8 @@ importers:
|
|||
specifier: 15.0.0
|
||||
version: 15.0.0
|
||||
'@vitejs/plugin-vue':
|
||||
specifier: 5.0.2
|
||||
version: 5.0.2(vite@5.0.12)(vue@3.4.15)
|
||||
specifier: 5.0.3
|
||||
version: 5.0.3(vite@5.0.12)(vue@3.4.15)
|
||||
'@vue/compiler-sfc':
|
||||
specifier: 3.4.3
|
||||
version: 3.4.3
|
||||
|
@ -905,6 +905,9 @@ importers:
|
|||
'@testing-library/vue':
|
||||
specifier: 8.0.1
|
||||
version: 8.0.1(@vue/compiler-sfc@3.4.3)(vue@3.4.15)
|
||||
'@types/blueimp-load-image':
|
||||
specifier: ^5.16.6
|
||||
version: 5.16.6
|
||||
'@types/escape-regexp':
|
||||
specifier: 0.0.3
|
||||
version: 0.0.3
|
||||
|
@ -7873,6 +7876,10 @@ packages:
|
|||
resolution: {integrity: sha512-9xlo6R2qDs5uixm0bcIqCeMCE6HiQsIyel9KQySStiyqNl2tnj2mP3DX1Nf56MD6KMenNNlBBsy3LJ7gUEQPXQ==}
|
||||
dev: true
|
||||
|
||||
/@types/blueimp-load-image@5.16.6:
|
||||
resolution: {integrity: sha512-e7s6CdDCUoBQdCe62Q6OS+DF68M8+ABxCEMh2Isjt4Fl3xuddljCHMN8mak48AMSVGGwUUtNRaZbkzgL5PEWew==}
|
||||
dev: true
|
||||
|
||||
/@types/body-parser@1.19.5:
|
||||
resolution: {integrity: sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==}
|
||||
dependencies:
|
||||
|
@ -8725,8 +8732,8 @@ packages:
|
|||
vue: 3.4.15(typescript@5.3.3)
|
||||
dev: true
|
||||
|
||||
/@vitejs/plugin-vue@5.0.2(vite@5.0.12)(vue@3.4.15):
|
||||
resolution: {integrity: sha512-kEjJHrLb5ePBvjD0SPZwJlw1QTRcjjCA9sB5VyfonoXVBxTS7TMnqL6EkLt1Eu61RDeiuZ/WN9Hf6PxXhPI2uA==}
|
||||
/@vitejs/plugin-vue@5.0.3(vite@5.0.12)(vue@3.4.15):
|
||||
resolution: {integrity: sha512-b8S5dVS40rgHdDrw+DQi/xOM9ed+kSRZzfm1T74bMmBDCd8XO87NKlFYInzCtwvtWwXZvo1QxE2OSspTATWrbA==}
|
||||
engines: {node: ^18.0.0 || >=20.0.0}
|
||||
peerDependencies:
|
||||
vite: ^5.0.0
|
||||
|
|
Loading…
Reference in New Issue