From c2c3c33f2f36d2b61f2d924e29656f09ddfefcb2 Mon Sep 17 00:00:00 2001 From: JCake Date: Sat, 3 Aug 2024 23:11:10 +0200 Subject: [PATCH] I would call it a usable CSV editor --- README.md | 28 ++++++++++++++++++++++- src/data.ts | 47 +++++++++++++++++++++++++++---------- src/range.ts | 10 ++++++++ src/spreadsheet.tsx | 3 ++- src/table.tsx | 56 ++++++++++++--------------------------------- styles.css | 10 +++++++- 6 files changed, 97 insertions(+), 57 deletions(-) diff --git a/README.md b/README.md index d1f16d6..df1c8d9 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,30 @@ # Spreadsheet Spreadsheet is an Obsidian plugin which follows the Obsidian mindset of open-standards and supports rich spreadsheet -handling through CSV. \ No newline at end of file +handling through CSV. + +## Usage + +A spreadsheet is any CSV document in your vault. Front matter is also supported natively. The following properties are +understood: + +| property | type | function | +|-----------------------------|:------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `columnTypes` | typeName[] | Allows constraining a column to a particular type. [All types](#types) | +| `columnTitles` | string[] | Gives each column a name. If this property is omitted, it will be inferred to be the first row in the CSV document. Upon saving, this value will be moved into the frontmatter of the CSV document. | +| `constrainToDefinedColumns` | 'true' or 'false' | If true, discards values without a `columnTitle` | +| `allowedTypes` | typeName[] | A list of [custom types](#custom-types) which are permitted in the document | +| `columnSeparator` | string | A string which separates columns. If not defined, will attempt to detect either `,` or `;`. This value is written to the front matter when the document is saved. | + +## Types + +A spreadsheet is designed to allow working with various data formats. This spreadsheet handler natively supports the following types: + +* raw text +* rich text (markdown) +* formulas +* dates +* values with units +* + +### Custom Types \ No newline at end of file diff --git a/src/data.ts b/src/data.ts index ca2c13a..176c095 100644 --- a/src/data.ts +++ b/src/data.ts @@ -1,5 +1,5 @@ import {FrontMatter} from "./spreadsheet.js"; -import {editorLivePreviewField, parseYaml, stringifyYaml} from "obsidian"; +import {parseYaml, stringifyYaml} from "obsidian"; import * as iter from "@j-cake/jcake-utils/iter"; import {Cell} from "./range.js"; @@ -9,21 +9,22 @@ export const typeDefList: Record any)> = { export type Row = string[]; -export default class DataSource { +export default class DataSource> { private onChange: () => void = () => void 0; - public data: Row[] = []; + private raw: Row[] = []; - frontMatter: FM = {} as any; + // If not specified, assume the following defaults. + frontMatter: FM = { urlEscaped: true } as FM; parsers = typeDefList; serialise(): string { - return `---\n${stringifyYaml(this.frontMatter)}\n---\n${this.data.map(i => i.join(this.frontMatter.columnSeparator ??= ';')).join('\n')}`; + return `---\n${stringifyYaml(this.frontMatter)}\n---\n${this.raw.map(i => i.join(this.frontMatter.columnSeparator ??= ';')).join('\n')}`; } clear() { - this.data = []; + this.raw = []; this.onChange(); } @@ -42,7 +43,7 @@ export default class DataSource { if (!this.frontMatter.columnTitles) this.frontMatter.columnTitles = rows.shift()?.split(separator) ?? []; - this.data = rows + this.raw = rows .map(line => { const column = []; @@ -59,7 +60,11 @@ export default class DataSource { } private parseFrontMatter(frontMatter: string): number { - this.frontMatter = parseYaml(frontMatter); + console.log(this.frontMatter, parseYaml(frontMatter)); + this.frontMatter = { + ...this.frontMatter, + ...parseYaml(frontMatter) + }; return frontMatter.length; } @@ -71,12 +76,29 @@ export default class DataSource { return this.frontMatter.columnTitles ?? []; } + public get data(): Cell[][] { + return this.raw.map((i, a) => i.map((j, b) => new Cell(a, b))); + } + public valueAt(cell: Cell): string { - return this.data[cell.row][cell.col]; + if (!this.raw[cell.row]) + this.raw[cell.row] = new Array(Math.max(cell.col, this.columnNames.length)).fill("").map(_ => ""); + + if (this.frontMatter.urlEscaped) + return decodeURIComponent(this.raw[cell.row][cell.col]); + else + return this.raw[cell.row][cell.col]; } public setValueAt(cell: Cell, value: string) { - this.data[cell.row][cell.col] = value; + if (!this.raw[cell.row]) + this.raw[cell.row] = new Array(Math.max(cell.col, this.columnNames.length)).fill("").map(_ => ""); + + if (this.frontMatter.urlEscaped) + this.raw[cell.row][cell.col] = encodeURIComponent(value); + else + this.raw[cell.row][cell.col] = value; + this.onChange(); } } @@ -86,9 +108,10 @@ export class InfiniteIterator { return this.iter(); } - constructor(private readonly data: T) {} + constructor(private readonly data: T) { + } - private *iter(): Generator { + private* iter(): Generator { while (true) yield this.data; } diff --git a/src/range.ts b/src/range.ts index 95d7e58..ad166ff 100644 --- a/src/range.ts +++ b/src/range.ts @@ -69,4 +69,14 @@ export class Cell { return `${[...this.col.toString(26).toUpperCase()].map(i => alphabet[alphabet2.indexOf(i)])}${this.row + 1}`; } + + public moveVertically(amount: number = 1): Cell { + this.row = Math.max(0, this.row + amount); + return this; + } + + public moveHorizontally(amount: number = 1): Cell { + this.col = Math.max(0, this.col + amount); + return this; + } } \ No newline at end of file diff --git a/src/spreadsheet.tsx b/src/spreadsheet.tsx index cb149ea..f9510fd 100644 --- a/src/spreadsheet.tsx +++ b/src/spreadsheet.tsx @@ -14,7 +14,8 @@ export interface FrontMatter extends Record { columnTitles?: string[], rowTitles?: string[], allowedTypes?: string[], - columnSeparator?: string + columnSeparator?: string, + urlEscaped?: boolean } export default class Spreadsheet extends TextFileView { diff --git a/src/table.tsx b/src/table.tsx index 9556a2c..76d074a 100644 --- a/src/table.tsx +++ b/src/table.tsx @@ -7,39 +7,16 @@ interface TableProps { data: DataSource, } -interface TableState { - data: DataSource, - - selected: Range[], - - columnWidths: number[]; - rowHeights: number[]; - - dragStart: Cell | null, - - cellRef: React.RefObject[], - tableBodyRef: React.RefObject - tableHeadRef: React.RefObject - - formula: React.RefObject, - formulaContent: string, - - manualSelection: string -} - type State = { state: T, setState: React.Dispatch> }; -const _if = function (condition: Condition, callback: () => any): boolean { - if (condition) - return (void callback()) || true; - else - return false; -} - -export type SelectionState = { selected: Range[], dragStart: Cell | null, cell: Cell | null }; +export type SelectionState = { + selected: Range[], + dragStart: Cell | null, + cell: Cell | null, +}; export type DimensionState = { columns: number[], rows: number[] }; export default function Table({data}: TableProps) { @@ -47,7 +24,7 @@ export default function Table({data}: TableProps) { const [selected, setSelected] = React.useState({ selected: [], dragStart: null, - cell: null + cell: null, }); const [dimensions, setDimensions] = React.useState({ columns: [], @@ -82,7 +59,6 @@ export default function Table({data}: TableProps) { } else return references.cells[addr]; } - // useEffect(() => void _if(selected.cell, () => data.setValueAt(selected.cell!, formula)), [formula]); useEffect(() => { references.formula.current?.focus(); references.formula.current?.select(); @@ -107,7 +83,10 @@ export default function Table({data}: TableProps) {