fix utils
This commit is contained in:
parent
e6ec32126f
commit
f9e866e733
|
@ -66,7 +66,7 @@ import { GridEventEmitter, Size } from '@/components/grid/grid.js';
|
||||||
import { useTooltip } from '@/scripts/use-tooltip.js';
|
import { useTooltip } from '@/scripts/use-tooltip.js';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
import { CellValue, GridCell } from '@/components/grid/cell.js';
|
import { CellValue, GridCell } from '@/components/grid/cell.js';
|
||||||
import { equalCellAddress, getCellAddress } from '@/components/grid/utils.js';
|
import { equalCellAddress, getCellAddress } from '@/components/grid/grid-utils.js';
|
||||||
import { cellValidation, ValidateViolation } from '@/components/grid/cell-validators.js';
|
import { cellValidation, ValidateViolation } from '@/components/grid/cell-validators.js';
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
|
|
|
@ -43,7 +43,7 @@ import MkDataRow from '@/components/grid/MkDataRow.vue';
|
||||||
import MkHeaderRow from '@/components/grid/MkHeaderRow.vue';
|
import MkHeaderRow from '@/components/grid/MkHeaderRow.vue';
|
||||||
import { cellValidation } from '@/components/grid/cell-validators.js';
|
import { cellValidation } from '@/components/grid/cell-validators.js';
|
||||||
import { CELL_ADDRESS_NONE, CellAddress, CellValue, createCell, GridCell } from '@/components/grid/cell.js';
|
import { CELL_ADDRESS_NONE, CellAddress, CellValue, createCell, GridCell } from '@/components/grid/cell.js';
|
||||||
import { equalCellAddress, getCellAddress } from '@/components/grid/utils.js';
|
import { equalCellAddress, getCellAddress } from '@/components/grid/grid-utils.js';
|
||||||
import { MenuItem } from '@/types/menu.js';
|
import { MenuItem } from '@/types/menu.js';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
import { GridCurrentState, GridEvent } from '@/components/grid/grid-event.js';
|
import { GridCurrentState, GridEvent } from '@/components/grid/grid-event.js';
|
||||||
|
@ -774,6 +774,10 @@ function emitCellValue(sender: GridCell | CellAddress, newValue: CellValue) {
|
||||||
oldValue: cell.value,
|
oldValue: cell.value,
|
||||||
newValue: newValue,
|
newValue: newValue,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (_DEV_) {
|
||||||
|
console.log(`[grid][cell-value] row:${cell.row}, col:${cell.column.index}, value:${newValue}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { CellValidator } from '@/components/grid/cell-validators.js';
|
import { CellValidator } from '@/components/grid/cell-validators.js';
|
||||||
import { Size, SizeStyle } from '@/components/grid/grid.js';
|
import { Size, SizeStyle } from '@/components/grid/grid.js';
|
||||||
import { calcCellWidth } from '@/components/grid/utils.js';
|
import { calcCellWidth } from '@/components/grid/grid-utils.js';
|
||||||
|
|
||||||
export type ColumnType = 'text' | 'number' | 'date' | 'boolean' | 'image';
|
export type ColumnType = 'text' | 'number' | 'date' | 'boolean' | 'image';
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { SizeStyle } from '@/components/grid/types.js';
|
import { SizeStyle } from '@/components/grid/grid.js';
|
||||||
import { CELL_ADDRESS_NONE, CellAddress } from '@/components/grid/cell.js';
|
import { CELL_ADDRESS_NONE, CellAddress } from '@/components/grid/cell.js';
|
||||||
|
|
||||||
export function isCellElement(elem: any): elem is HTMLTableCellElement {
|
export function isCellElement(elem: any): elem is HTMLTableCellElement {
|
|
@ -0,0 +1,146 @@
|
||||||
|
import { Ref } from 'vue';
|
||||||
|
import { GridCellValueChangeEvent, GridCurrentState, GridKeyDownEvent } from '@/components/grid/grid-event.js';
|
||||||
|
import copyToClipboard from '@/scripts/copy-to-clipboard.js';
|
||||||
|
import { ColumnSetting } from '@/components/grid/column.js';
|
||||||
|
import { CellValue } from '@/components/grid/cell.js';
|
||||||
|
import { DataSource } from '@/components/grid/grid.js';
|
||||||
|
|
||||||
|
class OptInGridUtils {
|
||||||
|
async applyCellValueFromEvent(gridItems: Ref<DataSource[]>, event: GridCellValueChangeEvent) {
|
||||||
|
const { row, column, newValue } = event;
|
||||||
|
gridItems.value[row.index][column.setting.bindTo] = newValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
async commonKeyDownHandler(gridItems: Ref<DataSource[]>, event: GridKeyDownEvent, currentState: GridCurrentState) {
|
||||||
|
const { ctrlKey, shiftKey, code } = event.event;
|
||||||
|
|
||||||
|
switch (true) {
|
||||||
|
case ctrlKey && shiftKey: {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case ctrlKey: {
|
||||||
|
switch (code) {
|
||||||
|
case 'KeyC': {
|
||||||
|
this.rangeCopyToClipboard(gridItems, currentState);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'KeyV': {
|
||||||
|
await this.pasteFromClipboard(gridItems, currentState);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case shiftKey: {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
switch (code) {
|
||||||
|
case 'Delete': {
|
||||||
|
this.deleteSelectionRange(gridItems, currentState);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rangeCopyToClipboard(gridItems: Ref<DataSource[]>, currentState: GridCurrentState) {
|
||||||
|
const lines = Array.of<string>();
|
||||||
|
const bounds = currentState.randedBounds;
|
||||||
|
|
||||||
|
for (let row = bounds.leftTop.row; row <= bounds.rightBottom.row; row++) {
|
||||||
|
const items = Array.of<string>();
|
||||||
|
for (let col = bounds.leftTop.col; col <= bounds.rightBottom.col; col++) {
|
||||||
|
const bindTo = currentState.columns[col].setting.bindTo;
|
||||||
|
const cell = gridItems.value[row][bindTo];
|
||||||
|
items.push(cell?.toString() ?? '');
|
||||||
|
}
|
||||||
|
lines.push(items.join('\t'));
|
||||||
|
}
|
||||||
|
|
||||||
|
const text = lines.join('\n');
|
||||||
|
copyToClipboard(text);
|
||||||
|
|
||||||
|
if (_DEV_) {
|
||||||
|
console.log(`Copied to clipboard: ${text}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async pasteFromClipboard(
|
||||||
|
gridItems: Ref<DataSource[]>,
|
||||||
|
currentState: GridCurrentState,
|
||||||
|
) {
|
||||||
|
function parseValue(value: string, type: ColumnSetting['type']): CellValue {
|
||||||
|
switch (type) {
|
||||||
|
case 'number': {
|
||||||
|
return Number(value);
|
||||||
|
}
|
||||||
|
case 'boolean': {
|
||||||
|
return value === 'true';
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const clipBoardText = await navigator.clipboard.readText();
|
||||||
|
if (_DEV_) {
|
||||||
|
console.log(`Paste from clipboard: ${clipBoardText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const bounds = currentState.randedBounds;
|
||||||
|
const lines = clipBoardText.replace(/\r/g, '')
|
||||||
|
.split('\n')
|
||||||
|
.map(it => it.split('\t'));
|
||||||
|
|
||||||
|
if (lines.length === 1 && lines[0].length === 1) {
|
||||||
|
// 単独文字列の場合は選択範囲全体に同じテキストを貼り付ける
|
||||||
|
const ranges = currentState.rangedCells;
|
||||||
|
for (const cell of ranges) {
|
||||||
|
gridItems.value[cell.row.index][cell.column.setting.bindTo] = parseValue(lines[0][0], cell.column.setting.type);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 表形式文字列の場合は表形式にパースし、選択範囲に合うように貼り付ける
|
||||||
|
const offsetRow = bounds.leftTop.row;
|
||||||
|
const offsetCol = bounds.leftTop.col;
|
||||||
|
const columns = currentState.columns;
|
||||||
|
for (let row = bounds.leftTop.row; row <= bounds.rightBottom.row; row++) {
|
||||||
|
const rowIdx = row - offsetRow;
|
||||||
|
if (lines.length <= rowIdx) {
|
||||||
|
// クリップボードから読んだ二次元配列よりも選択範囲の方が大きい場合、貼り付け操作を打ち切る
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const items = lines[rowIdx];
|
||||||
|
for (let col = bounds.leftTop.col; col <= bounds.rightBottom.col; col++) {
|
||||||
|
const colIdx = col - offsetCol;
|
||||||
|
if (items.length <= colIdx) {
|
||||||
|
// クリップボードから読んだ二次元配列よりも選択範囲の方が大きい場合、貼り付け操作を打ち切る
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
gridItems.value[row][columns[col].setting.bindTo] = parseValue(items[colIdx], columns[col].setting.type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteSelectionRange(gridItems: Ref<DataSource[]>, currentState: GridCurrentState) {
|
||||||
|
if (currentState.rangedRows.length > 0) {
|
||||||
|
const deletedIndexes = currentState.rangedRows.map(it => it.index);
|
||||||
|
gridItems.value = gridItems.value.filter((_, index) => !deletedIndexes.includes(index));
|
||||||
|
} else {
|
||||||
|
const ranges = currentState.rangedCells;
|
||||||
|
for (const cell of ranges) {
|
||||||
|
if (cell.column.setting.editable) {
|
||||||
|
gridItems.value[cell.row.index][cell.column.setting.bindTo] = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const optInGridUtils = new OptInGridUtils();
|
|
@ -56,8 +56,8 @@
|
||||||
</div>
|
</div>
|
||||||
<ul>
|
<ul>
|
||||||
<li>この枠に画像ファイルまたはディレクトリをドラッグ&ドロップ</li>
|
<li>この枠に画像ファイルまたはディレクトリをドラッグ&ドロップ</li>
|
||||||
<li><a @click="onFileSelectClicked">このリンクをクリックしてPCから選択する</a></li>
|
<li><a @click.prevent="onFileSelectClicked">このリンクをクリックしてPCから選択する</a></li>
|
||||||
<li><a @click="onDriveSelectClicked">このリンクをクリックしてドライブから選択する</a></li>
|
<li><a @click.prevent="onDriveSelectClicked">このリンクをクリックしてドライブから選択する</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -88,7 +88,7 @@
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||||
import { getCurrentInstance, nextTick, onMounted, reactive, ref, watch } from 'vue';
|
import { onMounted, reactive, ref } from 'vue';
|
||||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||||
import { fromDriveFile, GridItem } from '@/pages/admin/custom-emojis-grid.impl.js';
|
import { fromDriveFile, GridItem } from '@/pages/admin/custom-emojis-grid.impl.js';
|
||||||
import MkGrid from '@/components/grid/MkGrid.vue';
|
import MkGrid from '@/components/grid/MkGrid.vue';
|
||||||
|
@ -103,6 +103,7 @@ import { required } from '@/components/grid/cell-validators.js';
|
||||||
import { chooseFileFromDrive, chooseFileFromPc } from '@/scripts/select-file.js';
|
import { chooseFileFromDrive, chooseFileFromPc } from '@/scripts/select-file.js';
|
||||||
import { uploadFile } from '@/scripts/upload.js';
|
import { uploadFile } from '@/scripts/upload.js';
|
||||||
import {
|
import {
|
||||||
|
GridCellContextMenuEvent,
|
||||||
GridCellValidationEvent,
|
GridCellValidationEvent,
|
||||||
GridCellValueChangeEvent,
|
GridCellValueChangeEvent,
|
||||||
GridCurrentState,
|
GridCurrentState,
|
||||||
|
@ -113,7 +114,8 @@ import {
|
||||||
import copyToClipboard from '@/scripts/copy-to-clipboard.js';
|
import copyToClipboard from '@/scripts/copy-to-clipboard.js';
|
||||||
import { CellValue } from '@/components/grid/cell.js';
|
import { CellValue } from '@/components/grid/cell.js';
|
||||||
import { ColumnSetting } from '@/components/grid/column.js';
|
import { ColumnSetting } from '@/components/grid/column.js';
|
||||||
import { GridRow } from '@/components/grid/row.js';
|
import { DroppedFile, readDataTransferItems, flattenDroppedFiles, extractDroppedItems } from '@/scripts/file-drop.js';
|
||||||
|
import { optInGridUtils } from '@/components/grid/optin-utils.js';
|
||||||
|
|
||||||
type FolderItem = {
|
type FolderItem = {
|
||||||
id?: string;
|
id?: string;
|
||||||
|
@ -134,20 +136,6 @@ type RegisterLogItem = {
|
||||||
error?: string;
|
error?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type DroppedItem = DroppedFile | DroppedDirectory;
|
|
||||||
|
|
||||||
type DroppedFile = {
|
|
||||||
isFile: true;
|
|
||||||
path: string;
|
|
||||||
file: File;
|
|
||||||
};
|
|
||||||
|
|
||||||
type DroppedDirectory = {
|
|
||||||
isFile: false;
|
|
||||||
path: string;
|
|
||||||
children: DroppedItem[];
|
|
||||||
}
|
|
||||||
|
|
||||||
const columnSettings: ColumnSetting[] = [
|
const columnSettings: ColumnSetting[] = [
|
||||||
{ bindTo: 'url', icon: 'ti-icons', type: 'image', editable: false, width: 'auto', validators: [required] },
|
{ bindTo: 'url', icon: 'ti-icons', type: 'image', editable: false, width: 'auto', validators: [required] },
|
||||||
{ bindTo: 'name', title: 'name', type: 'text', editable: true, width: 140, validators: [required] },
|
{ bindTo: 'name', title: 'name', type: 'text', editable: true, width: 140, validators: [required] },
|
||||||
|
@ -248,35 +236,7 @@ async function onClearClicked() {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onDrop(ev: DragEvent) {
|
async function onDrop(ev: DragEvent) {
|
||||||
const dropItems = ev.dataTransfer?.items;
|
const droppedFiles = await extractDroppedItems(ev).then(it => flattenDroppedFiles(it));
|
||||||
if (!dropItems || dropItems.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const droppedFiles = Array.of<DroppedFile>();
|
|
||||||
const apiTestItem = dropItems[0];
|
|
||||||
if ('webkitGetAsEntry' in apiTestItem) {
|
|
||||||
const droppedItems = await eachDroppedItems(dropItems);
|
|
||||||
droppedFiles.push(...flattenDroppedItems(droppedItems).filter(it => it.isFile));
|
|
||||||
} else {
|
|
||||||
// webkitGetAsEntryに対応していない場合はfilesから取得する(ディレクトリのサポートは出来ない)
|
|
||||||
const dropFiles = ev.dataTransfer.files;
|
|
||||||
if (dropFiles.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = 0; i < dropFiles.length; i++) {
|
|
||||||
const file = dropFiles.item(i);
|
|
||||||
if (file) {
|
|
||||||
droppedFiles.push({
|
|
||||||
isFile: true,
|
|
||||||
path: file.name,
|
|
||||||
file,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const uploadedItems = await Promise.all(
|
const uploadedItems = await Promise.all(
|
||||||
droppedFiles.map(async (it) => ({
|
droppedFiles.map(async (it) => ({
|
||||||
droppedFile: it,
|
droppedFile: it,
|
||||||
|
@ -303,7 +263,6 @@ async function onDrop(ev: DragEvent) {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onFileSelectClicked(ev: MouseEvent) {
|
async function onFileSelectClicked(ev: MouseEvent) {
|
||||||
ev.preventDefault();
|
|
||||||
const driveFiles = await chooseFileFromPc(
|
const driveFiles = await chooseFileFromPc(
|
||||||
true,
|
true,
|
||||||
{
|
{
|
||||||
|
@ -317,16 +276,10 @@ async function onFileSelectClicked(ev: MouseEvent) {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onDriveSelectClicked(ev: MouseEvent) {
|
async function onDriveSelectClicked(ev: MouseEvent) {
|
||||||
ev.preventDefault();
|
|
||||||
const driveFiles = await chooseFileFromDrive(true);
|
const driveFiles = await chooseFileFromDrive(true);
|
||||||
gridItems.value.push(...driveFiles.map(fromDriveFile));
|
gridItems.value.push(...driveFiles.map(fromDriveFile));
|
||||||
}
|
}
|
||||||
|
|
||||||
function onRowDeleting(rows: GridRow[]) {
|
|
||||||
const deletedIndexes = rows.map(it => it.index);
|
|
||||||
gridItems.value = gridItems.value.filter((_, index) => !deletedIndexes.includes(index));
|
|
||||||
}
|
|
||||||
|
|
||||||
function onGridEvent(event: GridEvent, currentState: GridCurrentState) {
|
function onGridEvent(event: GridEvent, currentState: GridCurrentState) {
|
||||||
switch (event.type) {
|
switch (event.type) {
|
||||||
case 'cell-validation':
|
case 'cell-validation':
|
||||||
|
@ -335,6 +288,9 @@ function onGridEvent(event: GridEvent, currentState: GridCurrentState) {
|
||||||
case 'row-context-menu':
|
case 'row-context-menu':
|
||||||
onGridRowContextMenu(event, currentState);
|
onGridRowContextMenu(event, currentState);
|
||||||
break;
|
break;
|
||||||
|
case 'cell-context-menu':
|
||||||
|
onGridCellContextMenu(event, currentState);
|
||||||
|
break;
|
||||||
case 'cell-value-change':
|
case 'cell-value-change':
|
||||||
onGridCellValueChange(event, currentState);
|
onGridCellValueChange(event, currentState);
|
||||||
break;
|
break;
|
||||||
|
@ -352,170 +308,42 @@ function onGridRowContextMenu(event: GridRowContextMenuEvent, currentState: Grid
|
||||||
event.menuItems.push(
|
event.menuItems.push(
|
||||||
{
|
{
|
||||||
type: 'button',
|
type: 'button',
|
||||||
text: '行を削除',
|
text: '選択行をコピー',
|
||||||
|
icon: 'ti ti-copy',
|
||||||
|
action: () => optInGridUtils.rangeCopyToClipboard(gridItems, currentState),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'button',
|
||||||
|
text: '選択行を削除',
|
||||||
icon: 'ti ti-trash',
|
icon: 'ti ti-trash',
|
||||||
action: () => onRowDeleting(currentState.rangedRows),
|
action: () => optInGridUtils.deleteSelectionRange(gridItems, currentState),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onGridCellContextMenu(event: GridCellContextMenuEvent, currentState: GridCurrentState) {
|
||||||
|
event.menuItems.push(
|
||||||
|
{
|
||||||
|
type: 'button',
|
||||||
|
text: '選択範囲をコピー',
|
||||||
|
icon: 'ti ti-copy',
|
||||||
|
action: () => optInGridUtils.rangeCopyToClipboard(gridItems, currentState),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'button',
|
||||||
|
text: '選択行を削除',
|
||||||
|
icon: 'ti ti-trash',
|
||||||
|
action: () => optInGridUtils.deleteSelectionRange(gridItems, currentState),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onGridCellValueChange(event: GridCellValueChangeEvent, currentState: GridCurrentState) {
|
function onGridCellValueChange(event: GridCellValueChangeEvent, currentState: GridCurrentState) {
|
||||||
gridItems.value[event.row.index][event.column.setting.bindTo] = event.newValue;
|
optInGridUtils.applyCellValueFromEvent(gridItems, event);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onGridKeyDown(event: GridKeyDownEvent, currentState: GridCurrentState) {
|
function onGridKeyDown(event: GridKeyDownEvent, currentState: GridCurrentState) {
|
||||||
switch (event.event.code) {
|
optInGridUtils.commonKeyDownHandler(gridItems, event, currentState);
|
||||||
case 'KeyC': {
|
|
||||||
rangeCopyToClipboard(currentState);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'KeyV': {
|
|
||||||
pasteFromClipboard(currentState);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'Delete': {
|
|
||||||
if (currentState.rangedRows.length > 0) {
|
|
||||||
onRowDeleting(currentState.rangedRows);
|
|
||||||
} else {
|
|
||||||
const ranges = currentState.rangedCells;
|
|
||||||
for (const cell of ranges) {
|
|
||||||
if (cell.column.setting.editable) {
|
|
||||||
gridItems.value[cell.row.index][cell.column.setting.bindTo] = undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function eachDroppedItems(itemList: DataTransferItemList): Promise<DroppedItem[]> {
|
|
||||||
async function readEntry(entry: FileSystemEntry): Promise<DroppedItem> {
|
|
||||||
if (entry.isFile) {
|
|
||||||
return {
|
|
||||||
isFile: true,
|
|
||||||
path: entry.fullPath,
|
|
||||||
file: await readFile(entry as FileSystemFileEntry),
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
return {
|
|
||||||
isFile: false,
|
|
||||||
path: entry.fullPath,
|
|
||||||
children: await readDirectory(entry as FileSystemDirectoryEntry),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function readFile(fileSystemFileEntry: FileSystemFileEntry): Promise<File> {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
fileSystemFileEntry.file(resolve, reject);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function readDirectory(fileSystemDirectoryEntry: FileSystemDirectoryEntry): Promise<DroppedItem[]> {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
fileSystemDirectoryEntry.createReader().readEntries(
|
|
||||||
async (entries) => resolve(await Promise.all(entries.map(readEntry))),
|
|
||||||
reject,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 扱いにくいので配列に変換
|
|
||||||
const items = Array.of<DataTransferItem>();
|
|
||||||
for (let i = 0; i < itemList.length; i++) {
|
|
||||||
items.push(itemList[i]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Promise.all(
|
|
||||||
items
|
|
||||||
.map(it => it.webkitGetAsEntry())
|
|
||||||
.filter(it => it)
|
|
||||||
.map(it => readEntry(it!)),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function flattenDroppedItems(items: DroppedItem[]): DroppedFile[] {
|
|
||||||
const result = Array.of<DroppedFile>();
|
|
||||||
for (const item of items) {
|
|
||||||
if (item.isFile) {
|
|
||||||
result.push(item);
|
|
||||||
} else {
|
|
||||||
result.push(...flattenDroppedItems(item.children));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
function rangeCopyToClipboard(currentState: GridCurrentState) {
|
|
||||||
const lines = Array.of<string>();
|
|
||||||
const bounds = currentState.randedBounds;
|
|
||||||
|
|
||||||
for (let row = bounds.leftTop.row; row <= bounds.rightBottom.row; row++) {
|
|
||||||
const items = Array.of<string>();
|
|
||||||
for (let col = bounds.leftTop.col; col <= bounds.rightBottom.col; col++) {
|
|
||||||
const cell = gridItems.value[row][col];
|
|
||||||
items.push(cell.value?.toString() ?? '');
|
|
||||||
}
|
|
||||||
lines.push(items.join('\t'));
|
|
||||||
}
|
|
||||||
|
|
||||||
const text = lines.join('\n');
|
|
||||||
copyToClipboard(text);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function pasteFromClipboard(currentState: GridCurrentState) {
|
|
||||||
function parseValue(value: string, type: ColumnSetting['type']): CellValue {
|
|
||||||
switch (type) {
|
|
||||||
case 'number': {
|
|
||||||
return Number(value);
|
|
||||||
}
|
|
||||||
case 'boolean': {
|
|
||||||
return value === 'true';
|
|
||||||
}
|
|
||||||
default: {
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const cells = currentState.rangedCells;
|
|
||||||
const clipBoardText = await navigator.clipboard.readText();
|
|
||||||
|
|
||||||
const bounds = currentState.randedBounds;
|
|
||||||
const lines = clipBoardText.replace(/\r/g, '')
|
|
||||||
.split('\n')
|
|
||||||
.map(it => it.split('\t'));
|
|
||||||
|
|
||||||
if (lines.length === 1 && lines[0].length === 1) {
|
|
||||||
// 単独文字列の場合は選択範囲全体に同じテキストを貼り付ける
|
|
||||||
const ranges = currentState.rangedCells;
|
|
||||||
for (const cell of ranges) {
|
|
||||||
gridItems.value[cell.row.index][cell.column.setting.bindTo] = parseValue(lines[0][0], cell.column.setting.type);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 表形式文字列の場合は表形式にパースし、選択範囲に合うように貼り付ける
|
|
||||||
const offsetRow = bounds.leftTop.row;
|
|
||||||
const offsetCol = bounds.leftTop.col;
|
|
||||||
for (let row = bounds.leftTop.row; row <= bounds.rightBottom.row; row++) {
|
|
||||||
const rowIdx = row - offsetRow;
|
|
||||||
if (lines.length <= rowIdx) {
|
|
||||||
// クリップボードから読んだ二次元配列よりも選択範囲の方が大きい場合、貼り付け操作を打ち切る
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
const items = lines[rowIdx];
|
|
||||||
for (let col = bounds.leftTop.col; col <= bounds.rightBottom.col; col++) {
|
|
||||||
const colIdx = col - offsetCol;
|
|
||||||
if (items.length <= colIdx) {
|
|
||||||
// クリップボードから読んだ二次元配列よりも選択範囲の方が大きい場合、貼り付け操作を打ち切る
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
gridItems.value[row][col] = parseValue(items[colIdx], cells[row][col].column.setting.type);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function refreshUploadFolders() {
|
async function refreshUploadFolders() {
|
||||||
|
|
|
@ -0,0 +1,114 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type DroppedItem = DroppedFile | DroppedDirectory;
|
||||||
|
|
||||||
|
export type DroppedFile = {
|
||||||
|
isFile: true;
|
||||||
|
path: string;
|
||||||
|
file: File;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DroppedDirectory = {
|
||||||
|
isFile: false;
|
||||||
|
path: string;
|
||||||
|
children: DroppedItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function extractDroppedItems(ev: DragEvent): Promise<DroppedItem[]> {
|
||||||
|
const dropItems = ev.dataTransfer?.items;
|
||||||
|
if (!dropItems || dropItems.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const apiTestItem = dropItems[0];
|
||||||
|
if ('webkitGetAsEntry' in apiTestItem) {
|
||||||
|
return readDataTransferItems(dropItems);
|
||||||
|
} else {
|
||||||
|
// webkitGetAsEntryに対応していない場合はfilesから取得する(ディレクトリのサポートは出来ない)
|
||||||
|
const dropFiles = ev.dataTransfer.files;
|
||||||
|
if (dropFiles.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const droppedFiles = Array.of<DroppedFile>();
|
||||||
|
for (let i = 0; i < dropFiles.length; i++) {
|
||||||
|
const file = dropFiles.item(i);
|
||||||
|
if (file) {
|
||||||
|
droppedFiles.push({
|
||||||
|
isFile: true,
|
||||||
|
path: file.name,
|
||||||
|
file,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return droppedFiles;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ドラッグ&ドロップされたファイルのリストからディレクトリ構造とファイルへの参照({@link File})を取得する。
|
||||||
|
*/
|
||||||
|
export async function readDataTransferItems(itemList: DataTransferItemList): Promise<DroppedItem[]> {
|
||||||
|
async function readEntry(entry: FileSystemEntry): Promise<DroppedItem> {
|
||||||
|
if (entry.isFile) {
|
||||||
|
return {
|
||||||
|
isFile: true,
|
||||||
|
path: entry.fullPath,
|
||||||
|
file: await readFile(entry as FileSystemFileEntry),
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
isFile: false,
|
||||||
|
path: entry.fullPath,
|
||||||
|
children: await readDirectory(entry as FileSystemDirectoryEntry),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function readFile(fileSystemFileEntry: FileSystemFileEntry): Promise<File> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
fileSystemFileEntry.file(resolve, reject);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function readDirectory(fileSystemDirectoryEntry: FileSystemDirectoryEntry): Promise<DroppedItem[]> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
fileSystemDirectoryEntry.createReader().readEntries(
|
||||||
|
async (entries) => resolve(await Promise.all(entries.map(readEntry))),
|
||||||
|
reject,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 扱いにくいので配列に変換
|
||||||
|
const items = Array.of<DataTransferItem>();
|
||||||
|
for (let i = 0; i < itemList.length; i++) {
|
||||||
|
items.push(itemList[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.all(
|
||||||
|
items
|
||||||
|
.map(it => it.webkitGetAsEntry())
|
||||||
|
.filter(it => it)
|
||||||
|
.map(it => readEntry(it!)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link DroppedItem}のリストからディレクトリを再帰的に検索し、ファイルのリストを取得する。
|
||||||
|
*/
|
||||||
|
export function flattenDroppedFiles(items: DroppedItem[]): DroppedFile[] {
|
||||||
|
const result = Array.of<DroppedFile>();
|
||||||
|
for (const item of items) {
|
||||||
|
if (item.isFile) {
|
||||||
|
result.push(item);
|
||||||
|
} else {
|
||||||
|
result.push(...flattenDroppedFiles(item.children));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
Loading…
Reference in New Issue