mirror of
https://github.com/liubinfighter/csv-lite.git
synced 2026-07-22 05:43:52 +00:00
commit
4a28663ebd
9 changed files with 170 additions and 7 deletions
|
|
@ -27,6 +27,7 @@ A plugin designed to view and edit `CSV files` directly within Obsidian.
|
|||
- **Toggle** between the table view and raw source-mode.
|
||||
- **Edit** cells directly by clicking and typing.
|
||||
- **Manage** rows and columns (add, delete, move) with a simple right-click on the header.
|
||||
- **Create** new CSV files: use the command palette or right-click in the File Explorer to quickly create a new `.csv` file. A unique name like `new.csv` or `new-1.csv` will be generated automatically.
|
||||
- **Switch Delimiter Non‑Destructively**: Auto‑detects the file delimiter (comma, semicolon, tab, etc.). Changing the delimiter in the toolbar only re-parses the view; it does NOT rewrite your file. Your original delimiter is preserved when saving edits.
|
||||
- **Clickable URLs**: Plain-text URLs and Markdown-style links (`[text](url)`) in cells are automatically detected and rendered as clickable links. Click a link to open it in your browser, or click the edit button (✎) to edit the cell content.
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"id": "csv-lite",
|
||||
"name": "CSV Lite",
|
||||
"version": "1.6.1",
|
||||
"version": "1.7.0",
|
||||
"minAppVersion": "1.8.0",
|
||||
"description": "Just open and edit CSV files directly, no more. Keep it simple.",
|
||||
"author": "Jay Bridge",
|
||||
|
|
|
|||
4
package-lock.json
generated
4
package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "obsidian-csv",
|
||||
"version": "1.1.5",
|
||||
"version": "1.7.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "obsidian-csv",
|
||||
"version": "1.1.5",
|
||||
"version": "1.7.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/commands": "^6.8.1",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "obsidian-csv",
|
||||
"version": "1.6.1",
|
||||
"version": "1.7.0",
|
||||
"description": "CSV viewer and editor for Obsidian",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
|
|
|
|||
|
|
@ -45,8 +45,13 @@ export const enUS = {
|
|||
deleteCol: 'Delete this column',
|
||||
moveColLeft: 'Move column left',
|
||||
moveColRight: 'Move column right',
|
||||
createNewCsv: 'Create new CSV file'
|
||||
}
|
||||
,
|
||||
commands: {
|
||||
createNewCsv: 'Create new CSV file',
|
||||
fileExists: 'File already exists'
|
||||
},
|
||||
tableMessages: {
|
||||
atLeastOneRow: 'At least one row must remain',
|
||||
atLeastOneColumn: 'At least one column must remain'
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ export const zhCN = {
|
|||
sourceMode: '源码模式',
|
||||
tableMode: '表格模式',
|
||||
insertRowBefore: '上方插入行',
|
||||
insertRowAfter: '下方插入行',
|
||||
insertRowAfter: '下方插入行'
|
||||
},
|
||||
editBar: {
|
||||
placeholder: '编辑选中单元格...'
|
||||
|
|
@ -47,8 +47,13 @@ export const zhCN = {
|
|||
deleteCol: '删除本列',
|
||||
moveColLeft: '向左移动一列',
|
||||
moveColRight: '向右移动一列',
|
||||
createNewCsv: '创建新 CSV 文件'
|
||||
}
|
||||
,
|
||||
commands: {
|
||||
createNewCsv: '创建新 CSV 文件',
|
||||
fileExists: '文件已存在'
|
||||
},
|
||||
tableMessages: {
|
||||
atLeastOneRow: '至少需要保留一行',
|
||||
atLeastOneColumn: '至少需要保留一列'
|
||||
|
|
|
|||
63
src/main.ts
63
src/main.ts
|
|
@ -1,7 +1,8 @@
|
|||
import { Plugin, WorkspaceLeaf, moment } from "obsidian";
|
||||
import { Plugin, WorkspaceLeaf, moment, TFile, Notice } from "obsidian";
|
||||
import { CSVView, VIEW_TYPE_CSV } from "./view";
|
||||
import { SourceView, VIEW_TYPE_CSV_SOURCE } from "./source-view";
|
||||
import { i18n } from "./i18n";
|
||||
import { FileUtils } from "./utils/file-utils";
|
||||
|
||||
interface CSVPluginSettings {
|
||||
csvSettings: string;
|
||||
|
|
@ -43,6 +44,66 @@ export default class CSVPlugin extends Plugin {
|
|||
|
||||
// 将.csv文件扩展名与视图类型绑定
|
||||
this.registerExtensions(["csv"], VIEW_TYPE_CSV);
|
||||
|
||||
// Command: create new csv file (from command palette) — direct creation, no modal
|
||||
this.addCommand({
|
||||
id: 'csv-lite-create-new-csv-file',
|
||||
name: i18n.t('commands.createNewCsv'),
|
||||
callback: () => {
|
||||
this.createCsvInFolder('');
|
||||
}
|
||||
});
|
||||
|
||||
// File explorer context menu: create csv file inside folder or next to file — direct creation
|
||||
this.registerEvent(
|
||||
this.app.workspace.on('file-menu', (menu, file) => {
|
||||
menu.addItem((item) => {
|
||||
item.setTitle(i18n.t('contextMenu.createNewCsv') || i18n.t('commands.createNewCsv'))
|
||||
.setIcon('file-plus')
|
||||
.onClick(() => {
|
||||
let defaultFolder = '';
|
||||
if ((file as any).path) {
|
||||
const fp = (file as any).path as string;
|
||||
// if path has a slash, take parent folder
|
||||
const idx = fp.lastIndexOf('/');
|
||||
if (idx > 0) defaultFolder = fp.substring(0, idx);
|
||||
}
|
||||
this.createCsvInFolder(defaultFolder);
|
||||
});
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// Create a new CSV in the given folder with an auto-incremented name if necessary
|
||||
private async createCsvInFolder(folder: string) {
|
||||
const baseName = 'new.csv';
|
||||
let name = baseName;
|
||||
let idx = 0;
|
||||
|
||||
while (this.app.vault.getAbstractFileByPath(folder ? `${folder}/${name}` : name)) {
|
||||
idx++;
|
||||
name = `new-${idx}.csv`;
|
||||
if (idx > 1000) {
|
||||
new Notice(i18n.t('modal.errors.createFailed') || 'Failed to create file');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const path = folder ? `${folder}/${name}` : name;
|
||||
|
||||
try {
|
||||
await FileUtils.withRetry(() => this.app.vault.create(path, ''));
|
||||
const created = this.app.vault.getAbstractFileByPath(path) as TFile | null;
|
||||
if (created) {
|
||||
await this.app.workspace.getLeaf(true).openFile(created);
|
||||
}
|
||||
return created;
|
||||
} catch (err) {
|
||||
console.error('CreateCsv: failed to create file', err);
|
||||
new Notice(i18n.t('modal.errors.createFailed') || 'Failed to create file');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
onunload() {
|
||||
|
|
|
|||
90
src/utils/create-csv-modal.ts
Normal file
90
src/utils/create-csv-modal.ts
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
import { App, Modal, Setting, Notice } from 'obsidian';
|
||||
import { i18n } from '../i18n';
|
||||
|
||||
export interface CreateCsvModalOptions {
|
||||
defaultFolder?: string;
|
||||
onSubmit: (path: string) => Promise<void>;
|
||||
}
|
||||
|
||||
export class CreateCsvModal extends Modal {
|
||||
private defaultFolder: string;
|
||||
private onSubmit: (path: string) => Promise<void>;
|
||||
private fileNameInput!: HTMLInputElement;
|
||||
private folderInput!: HTMLInputElement;
|
||||
|
||||
constructor(app: App, options: CreateCsvModalOptions) {
|
||||
super(app);
|
||||
this.defaultFolder = options.defaultFolder ?? '';
|
||||
this.onSubmit = options.onSubmit;
|
||||
this.titleEl.setText(i18n.t('commands.createNewCsv'));
|
||||
}
|
||||
|
||||
onOpen() {
|
||||
const { contentEl } = this;
|
||||
|
||||
// filename setting
|
||||
new Setting(contentEl)
|
||||
.setName(i18n.t('modal.fileName') || 'File name')
|
||||
.setDesc(i18n.t('modal.fileNameDesc') || 'File name (will have .csv appended if missing)')
|
||||
.addText((text) => {
|
||||
this.fileNameInput = text.inputEl;
|
||||
text.setPlaceholder('new-file.csv');
|
||||
});
|
||||
|
||||
// folder setting
|
||||
new Setting(contentEl)
|
||||
.setName(i18n.t('modal.folder') || 'Folder')
|
||||
.setDesc(i18n.t('modal.folderDesc') || 'Relative folder inside vault (leave empty for root)')
|
||||
.addText((text) => {
|
||||
this.folderInput = text.inputEl;
|
||||
this.folderInput.value = this.defaultFolder ?? '';
|
||||
text.setPlaceholder('Folder/Subfolder');
|
||||
});
|
||||
|
||||
// buttons
|
||||
const btnContainer = contentEl.createDiv({ cls: 'mod-cta-container' });
|
||||
const submitBtn = btnContainer.createEl('button', { text: i18n.t('buttons.create') || 'Create' });
|
||||
submitBtn.addEventListener('click', async (e) => {
|
||||
e.preventDefault();
|
||||
await this.handleSubmit();
|
||||
});
|
||||
|
||||
const cancelBtn = btnContainer.createEl('button', { text: i18n.t('buttons.cancel') || 'Cancel' });
|
||||
cancelBtn.addEventListener('click', () => this.close());
|
||||
}
|
||||
|
||||
async handleSubmit() {
|
||||
const rawName = this.fileNameInput?.value.trim() ?? '';
|
||||
let folder = this.folderInput?.value.trim() ?? '';
|
||||
|
||||
if (!rawName) {
|
||||
new Notice(i18n.t('modal.errors.missingFileName') || 'Please provide a file name');
|
||||
return;
|
||||
}
|
||||
|
||||
let name = rawName;
|
||||
if (!name.toLowerCase().endsWith('.csv')) {
|
||||
name = `${name}.csv`;
|
||||
}
|
||||
|
||||
// normalize folder path: remove leading/trailing slashes
|
||||
folder = folder.replace(/^\/+|\/+$/g, '');
|
||||
|
||||
const path = folder ? `${folder}/${name}` : name;
|
||||
|
||||
// prevent overwriting existing files
|
||||
const existing = this.app.vault.getAbstractFileByPath(path);
|
||||
if (existing) {
|
||||
new Notice(i18n.t('commands.fileExists') || 'File already exists');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.onSubmit(path);
|
||||
this.close();
|
||||
} catch (err) {
|
||||
console.error('CreateCsvModal: failed to create file', err);
|
||||
new Notice(i18n.t('modal.errors.createFailed') || 'Failed to create file');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
{
|
||||
"1.0.0": "0.15.0",
|
||||
"1.1.5": "1.8.0"
|
||||
"1.1.5": "1.8.0",
|
||||
"1.7.0": "1.8.0"
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue