Skip to content

Commit

Permalink
I would call it a usable CSV editor
Browse files Browse the repository at this point in the history
  • Loading branch information
J-Cake committed Aug 3, 2024
1 parent 835c78d commit c2c3c33
Show file tree
Hide file tree
Showing 6 changed files with 97 additions and 57 deletions.
28 changes: 27 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -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.
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
47 changes: 35 additions & 12 deletions src/data.ts
Original file line number Diff line number Diff line change
@@ -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";

Expand All @@ -9,21 +9,22 @@ export const typeDefList: Record<string, ((value: string) => any)> = {

export type Row<FM extends FrontMatter> = string[];

export default class DataSource<FM extends FrontMatter> {
export default class DataSource<FM extends Partial<FrontMatter>> {
private onChange: () => void = () => void 0;

public data: Row<FM>[] = [];
private raw: Row<FM>[] = [];

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();
}

Expand All @@ -42,7 +43,7 @@ export default class DataSource<FM extends FrontMatter> {
if (!this.frontMatter.columnTitles)
this.frontMatter.columnTitles = rows.shift()?.split(separator) ?? [];

this.data = rows
this.raw = rows
.map(line => {
const column = [];

Expand All @@ -59,7 +60,11 @@ export default class DataSource<FM extends FrontMatter> {
}

private parseFrontMatter(frontMatter: string): number {
this.frontMatter = parseYaml(frontMatter);
console.log(this.frontMatter, parseYaml(frontMatter));
this.frontMatter = {
...this.frontMatter,
...parseYaml(frontMatter)
};
return frontMatter.length;
}

Expand All @@ -71,12 +76,29 @@ export default class DataSource<FM extends FrontMatter> {
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();
}
}
Expand All @@ -86,9 +108,10 @@ export class InfiniteIterator<T> {
return this.iter();
}

constructor(private readonly data: T) {}
constructor(private readonly data: T) {
}

private *iter(): Generator<T> {
private* iter(): Generator<T> {
while (true)
yield this.data;
}
Expand Down
10 changes: 10 additions & 0 deletions src/range.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
3 changes: 2 additions & 1 deletion src/spreadsheet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ export interface FrontMatter extends Record<string, any> {
columnTitles?: string[],
rowTitles?: string[],
allowedTypes?: string[],
columnSeparator?: string
columnSeparator?: string,
urlEscaped?: boolean
}

export default class Spreadsheet extends TextFileView {
Expand Down
56 changes: 14 additions & 42 deletions src/table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,47 +7,24 @@ interface TableProps {
data: DataSource<any>,
}

interface TableState {
data: DataSource<any>,

selected: Range[],

columnWidths: number[];
rowHeights: number[];

dragStart: Cell | null,

cellRef: React.RefObject<HTMLTableCellElement>[],
tableBodyRef: React.RefObject<HTMLTableSectionElement>
tableHeadRef: React.RefObject<HTMLTableSectionElement>

formula: React.RefObject<HTMLTextAreaElement>,
formulaContent: string,

manualSelection: string
}

type State<T> = {
state: T,
setState: React.Dispatch<React.SetStateAction<T>>
};

const _if = function <Condition>(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) {

const [selected, setSelected] = React.useState<SelectionState>({
selected: [],
dragStart: null,
cell: null
cell: null,
});
const [dimensions, setDimensions] = React.useState<DimensionState>({
columns: [],
Expand Down Expand Up @@ -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();
Expand All @@ -107,7 +83,10 @@ export default function Table({data}: TableProps) {

<textarea
ref={references.formula}
onChange={e => selected.cell ? data.setValueAt(selected.cell!, e.target.value) : null}
onChange={function (e) {
if (selected.cell)
data.setValueAt(selected.cell!, e.target.value);
}}
value={selected.cell ? data.valueAt(selected.cell!) : ''}
disabled={!selected.cell}
autoFocus={true}
Expand All @@ -123,7 +102,8 @@ export default function Table({data}: TableProps) {
{data.data.map((cells, row) => <tr
data-row-number={row}
key={`table-row-${row}`}>
{cells.map((value, col) => <td
{cells.map((cell, col) => <td
className={selected.cell?.eq(new Cell(row, col)) ? "editing" : ""}
key={`table-cell-${new Cell(row, col).toString()}`}
ref={getCell(new Cell(row, col))}

Expand All @@ -137,7 +117,7 @@ export default function Table({data}: TableProps) {
setState: setSelected
})}
data-address={new Cell(row, col).toString()}
>{value}</td>)}
>{data.valueAt(cell)}</td>)}
</tr>)}
<Selection
ranges={selected.selected}
Expand Down Expand Up @@ -210,13 +190,5 @@ export function finishSelection(e: React.MouseEvent, cell: Cell, selection: Stat
export function getActiveCell(selected: Range[]): Cell | null {
if (selected.reduce((a, j) => a + j.area, 0) > 0)
return selected[0].topLeft;
else return null;

// if (selected.reduce((a, i) => a + i.area, 0) != 1)
// return null;
//
// return new Cell(
// Math.min(...selected.map(i => [i.to.row, i.from.row]).flat()),
// Math.min(...selected.map(i => [i.to.col, i.from.col]).flat())
// );
else return null
}
10 changes: 9 additions & 1 deletion styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -69,13 +69,21 @@ thead {
td {
line-height: var(--table-line-height);
font-size: var(--table-text-size);
color: var(--table-text-color);
color: var(--text-muted);

white-space: var(--table-white-space);

padding: 4px 8px;

border: var(--table-border-width) solid var(--table-border-color);

box-sizing: border-box;
}

td.editing {
/*border: 4px solid hsl(var(--accent-h), var(--accent-s), var(--accent-l));*/
background: var(--code-background);
color: var(--table-text-color);
}

.table-widget, tbody {
Expand Down

0 comments on commit c2c3c33

Please sign in to comment.