├── .eslintrc.json ├── .github └── workflows │ ├── release.yml │ └── version.yml ├── .gitignore ├── .npmrc ├── README.md ├── manifest.json ├── package.json ├── rollup.config.js ├── screenshot.png ├── src ├── main.ts └── styles.scss └── tsconfig.json /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true 5 | }, 6 | "extends": [ 7 | "eslint:recommended", 8 | "plugin:@typescript-eslint/recommended" 9 | ], 10 | "parser": "@typescript-eslint/parser", 11 | "parserOptions": { 12 | "ecmaVersion": "latest", 13 | "sourceType": "module" 14 | }, 15 | "plugins": [ 16 | "@typescript-eslint" 17 | ], 18 | "rules": { 19 | "indent": [ 20 | "error", 21 | "tab" 22 | ], 23 | "linebreak-style": [ 24 | "warn", 25 | "unix" 26 | ], 27 | "quotes": [ 28 | "error", 29 | "double" 30 | ], 31 | "semi": [ 32 | "error", 33 | "always" 34 | ], 35 | "@typescript-eslint/ban-ts-comment": "warn", 36 | "@typescript-eslint/no-empty-function": "warn" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release Obsidian Plugin 2 | on: 3 | push: 4 | # Sequence of patterns matched against refs/tags 5 | tags: 6 | - '*' # Push events to matching any tag format, i.e. 1.0, 20.15.10 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | with: 13 | fetch-depth: 0 # otherwise, you will failed to push refs to dest repo 14 | - name: Use Node.js 15 | uses: actions/setup-node@v1 16 | with: 17 | node-version: '17.x' # You might need to adjust this value to your own version 18 | # Get the version number and put it in a variable 19 | - name: Get Version 20 | id: version 21 | run: | 22 | echo "::set-output name=tag::$(git describe --abbrev=0)" 23 | # Build the plugin 24 | - name: Build 25 | id: build 26 | run: | 27 | npm install 28 | npm run build --if-present 29 | # Package the required files into a zip 30 | - name: Package 31 | run: | 32 | mkdir ${{ github.event.repository.name }} 33 | cp main.js manifest.json styles.css README.md ${{ github.event.repository.name }} 34 | zip -r ${{ github.event.repository.name }}.zip ${{ github.event.repository.name }} 35 | # Create the release on github 36 | - name: Create Release 37 | id: create_release 38 | uses: actions/create-release@v1 39 | env: 40 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 41 | VERSION: ${{ github.ref }} 42 | with: 43 | tag_name: ${{ github.ref }} 44 | release_name: ${{ github.ref }} 45 | draft: false 46 | prerelease: false 47 | # Upload the packaged release file 48 | - name: Upload zip file 49 | id: upload-zip 50 | uses: actions/upload-release-asset@v1 51 | env: 52 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 53 | with: 54 | upload_url: ${{ steps.create_release.outputs.upload_url }} 55 | asset_path: ./${{ github.event.repository.name }}.zip 56 | asset_name: ${{ github.event.repository.name }}-${{ steps.version.outputs.tag }}.zip 57 | asset_content_type: application/zip 58 | # Upload the main.js 59 | - name: Upload main.js 60 | id: upload-main 61 | uses: actions/upload-release-asset@v1 62 | env: 63 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 64 | with: 65 | upload_url: ${{ steps.create_release.outputs.upload_url }} 66 | asset_path: ./main.js 67 | asset_name: main.js 68 | asset_content_type: text/javascript 69 | # Upload the manifest.json 70 | - name: Upload manifest.json 71 | id: upload-manifest 72 | uses: actions/upload-release-asset@v1 73 | env: 74 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 75 | with: 76 | upload_url: ${{ steps.create_release.outputs.upload_url }} 77 | asset_path: ./manifest.json 78 | asset_name: manifest.json 79 | asset_content_type: application/json 80 | # Upload the style.css 81 | - name: Upload styles.css 82 | id: upload-css 83 | uses: actions/upload-release-asset@v1 84 | env: 85 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 86 | with: 87 | upload_url: ${{ steps.create_release.outputs.upload_url }} 88 | asset_path: ./styles.css 89 | asset_name: styles.css 90 | asset_content_type: text/css 91 | -------------------------------------------------------------------------------- /.github/workflows/version.yml: -------------------------------------------------------------------------------- 1 | name: Bump Version 2 | on: 3 | workflow_dispatch: 4 | inputs: 5 | type: 6 | description: 'Type of version (`major`, `minor`, `patch`)' 7 | required: true 8 | default: 'patch' 9 | jobs: 10 | bump: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | with: 15 | persist-credentials: false # otherwise, the token used is the GITHUB_TOKEN, instead of your personal token 16 | fetch-depth: 0 # otherwise, you will fail to push refs to dest repo 17 | token: ${{secrets.PAT}} # use a personal access token so that other actions can trigger 18 | # Bump the version number 19 | - name: Update Version 20 | uses: MCKanpolat/auto-semver-action@1.0.5 21 | id: version 22 | with: 23 | releaseType: ${{ github.event.inputs.type }} 24 | github_token: ${{ secrets.PAT }} 25 | # update the manifest.json with the new version 26 | - name: Update manifest version 27 | uses: jossef/action-set-json-field@v1 28 | with: 29 | file: manifest.json 30 | field: version 31 | value: ${{ steps.version.outputs.version }} 32 | # Commit the manifest.json and update the tag 33 | - name: Commit manifest 34 | run: | 35 | git config --local user.name "GitHub Action" 36 | git config --local user.email "action@github.com" 37 | git branch --show-current 38 | git add -u 39 | git commit -m "${{ steps.version.outputs.version }}" 40 | git tag -fa ${{ steps.version.outputs.version }} -m "${{ steps.version.outputs.version }}" 41 | # push the commit 42 | - name: Push changes 43 | uses: ad-m/github-push-action@v0.6.0 44 | with: 45 | github_token: ${{secrets.PAT}} 46 | tags: true 47 | branch: ${{ github.ref }} 48 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Intellij 2 | *.iml 3 | .idea 4 | 5 | # npm 6 | node_modules 7 | package-lock.json 8 | 9 | # build 10 | main.js 11 | styles.css 12 | *.js.map 13 | data.json 14 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | tag-version-prefix = "" -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This repository will be put as archived because this version doesn't work anymore either and as I don't use this extension anymore nor have I the time to do so, I prefer to put it as archived with this little note and let people wanting to see what little change I did see and use if it work for them. 2 | Good luck on your search for an alternative or to make this one work ! 3 | 4 | # CSV Editor Obsidian Plugin 5 | [![GitHub release (latest SemVer)](https://img.shields.io/github/v/release/Sayama3/csv-obsidian?style=for-the-badge&sort=semver)](https://github.com/Sayama3/csv-obsidian/releases/latest) 6 | ![GitHub All Releases](https://img.shields.io/github/downloads/Sayama3/csv-obsidian/total?style=for-the-badge) 7 | 8 | A plugin for [Obsidian](https://obsidian.md) which allows viewing and editing of CSV files in a spreadsheet-like table format. 9 | 10 | **Back up your CSV files!** This plugin is very new and therefore experimental. At this stage, data loss is a very real possibility! 11 | 12 | ![Screenshot](https://github.com/Sayama3/csv-obsidian/raw/main/screenshot.png) 13 | 14 | ### Original Repo 15 | 16 | Thanks to [deathau](https://github.com/deathau) for the [original repo](https://github.com/deathau/csv-obsidian). 17 | 18 | ### Features 19 | - Open (and edit) CSV files right from Obsidian! 20 | - Auto-saving 21 | - Per-file setting for including headers (persisted in local storage) 22 | - Markdown editing, a preview for each individual cell (internal links aren't working correctly, yet) 23 | - Sort the data by clicking on a column name 24 | - Filter by column values 25 | - Freeze columns 26 | - Insert new columns/rows 27 | 28 | ## Installation 29 | 30 | ### From GitHub 31 | - Download the latest main 32 | - Extract the files from the zip to your vault's plugins folder: `/.obsidian/plugins/csv-obsidian` 33 | Note: On some machines the `.obsidian` folder may be hidden. On MacOS you should be able to press `Command+Shift+Dot` to show the folder in Finder. 34 | - Reload Obsidian 35 | - If prompted about Safe Mode, you can disable safe mode and enable the plugin. 36 | Otherwise head to Settings, third-party plugins, make sure safe mode is off and 37 | enable the plugin from there. 38 | 39 | ## Development 40 | 41 | This project uses Typescript to provide type checking and documentation. 42 | The repo depends on the latest [plugin API](https://github.com/obsidianmd/obsidian-api) in Typescript Definition format, which contains TSDoc comments describing what it does. 43 | 44 | **Note:** The Obsidian API is still in early alpha and is subject to change at any time! 45 | 46 | If you want to contribute to development and/or just customize it with your own 47 | tweaks, you can do the following: 48 | - Clone this repo. 49 | - `npm i` or `yarn` to install dependencies 50 | - `npm run build` to compile. 51 | - Copy `manifest.json`, `main.js` and `styles.css` to a subfolder of your plugins 52 | folder (e.g, `/.obsidian/plugins//`) 53 | - Reload obsidian to see changes 54 | 55 | Alternately, you can clone the repo directly into your plugins folder and once 56 | dependencies are installed use `npm run dev` to start compilation in watch mode. 57 | You may have to reload obsidian (`ctrl+R`) to see changes. 58 | 59 | # Version History 60 | ## 0.0.1 61 | Initial release of csv-obsidian! See [Features](#Features) above 62 | ## 0.0.2 63 | Update of the source code and the package.json to use the latest version the api and each package. 64 | Add a button to create CSV from `File explorer` tab. 65 | ## 0.0.3 66 | Fix the error that was overriding the faulty csv. *(They are aside, the editor will just close)*. 67 | A notice will also show what went wrong with the csv. 68 | 69 | ### Known issue 70 | Sometimes, the headers won't move, when scrolling so the content will not entirely be displayed. 71 | Try reloading/restart obsidian or switching to a regular file back and forth. 72 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "csv-obsidian-second", 3 | "name": "CSV Editor 2", 4 | "author": "Sayama3", 5 | "authorUrl": "https://github.com/Sayama3", 6 | "description": "Edit CSV files in Obsidian", 7 | "isDesktopOnly": false, 8 | "version": "0.0.3", 9 | "minAppVersion": "0.14.5", 10 | "repo": "Sayama3/csv-obsidian", 11 | "branch": "main" 12 | } 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "csv-obsidian", 3 | "version": "0.0.2", 4 | "description": "Edit csv files", 5 | "main": "main.js", 6 | "scripts": { 7 | "install-deps": "npm install", 8 | "dev": "rollup --config rollup.config.js -w", 9 | "build": "rollup --config rollup.config.js" 10 | }, 11 | "keywords": [], 12 | "author": "", 13 | "license": "MIT", 14 | "devDependencies": { 15 | "@rollup/plugin-commonjs": "^21.1.0", 16 | "@rollup/plugin-node-resolve": "^13.2.1", 17 | "@rollup/plugin-typescript": "^8.3.2", 18 | "@types/node": "^17.0.24", 19 | "@typescript-eslint/eslint-plugin": "^5.19.0", 20 | "@typescript-eslint/parser": "^5.19.0", 21 | "eslint": "^8.13.0", 22 | "obsidian": "https://github.com/obsidianmd/obsidian-api/tarball/master", 23 | "rollup": "^2.70.2", 24 | "rollup-plugin-scss": "^3.0.0", 25 | "sass": "^1.50.0", 26 | "tslib": "^2.3.1", 27 | "typescript": "^4.6.3" 28 | }, 29 | "dependencies": { 30 | "@types/papaparse": "^5.3.2", 31 | "handsontable": "^11.1.0", 32 | "papaparse": "^5.3.2" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import typescript from '@rollup/plugin-typescript'; 2 | import {nodeResolve} from '@rollup/plugin-node-resolve'; 3 | import commonjs from '@rollup/plugin-commonjs'; 4 | import scss from 'rollup-plugin-scss'; 5 | 6 | export default { 7 | input: 'src/main.ts', 8 | output: { 9 | dir: '.', 10 | sourcemap: 'inline', 11 | format: 'cjs', 12 | exports: 'default' 13 | }, 14 | external: ['obsidian'], 15 | plugins: [ 16 | typescript(), 17 | nodeResolve({browser: true}), 18 | commonjs(), 19 | scss({ output: 'styles.css', sass: require('sass') }) 20 | ] 21 | }; -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sayama3/csv-obsidian/6cc2b11804201569186a308cae2699061089491f/screenshot.png -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { 2 | addIcon, ButtonComponent, debounce, 3 | MarkdownRenderer, 4 | MarkdownView, Notice, 5 | Plugin, 6 | Setting, 7 | TextFileView, TFile, TFolder, 8 | ToggleComponent, View, 9 | WorkspaceLeaf, 10 | } from "obsidian"; 11 | import * as Papa from "papaparse"; 12 | import Handsontable from "handsontable"; 13 | import "handsontable/dist/handsontable.full.min.css"; 14 | import "./styles.scss"; 15 | import {ParseError, ParseMeta, ParseResult} from "papaparse"; 16 | import {error} from "handsontable/helpers"; 17 | 18 | function CreateEmptyCSV(row = 1, col = 1): string{ 19 | let csv = ""; 20 | for (let x = 0; x < col; x++) { 21 | for (let y = 0; y < row; y++) { 22 | csv += "\"\""; 23 | if (y { 36 | if(file instanceof TFolder){ 37 | const folder = file as TFolder; 38 | menu.addItem((item) => { 39 | item 40 | .setTitle("New CSV file") 41 | .setIcon("document") 42 | .onClick(async () => { 43 | //Searching if there is not already csv files named "Untitled". 44 | let index = 0; 45 | for (const child of folder.children) { 46 | if (child instanceof TFile){ 47 | const file = child as TFile; 48 | if (file.extension === "csv" && file.basename.contains("Untitled")){ 49 | const split = file.basename.split(" "); 50 | if (split.length > 1 && !isNaN(parseInt(split[1]))){ 51 | const i = parseInt(split[1]); 52 | index = i >= index ? i+1:index; 53 | } else { 54 | index = index > 0 ? index : 1; 55 | } 56 | } 57 | } 58 | } 59 | //Creating the file. 60 | const fileName = `Untitled${index>0?` ${index}`:""}`; 61 | await this.app.vault.create(folder.path+`/${fileName}.csv`, CreateEmptyCSV(4,4)); 62 | new Notice(`The file "${fileName}" has been created in the folder "${folder.path}".`); 63 | 64 | // We're not opening the file as it cause error. 65 | // await this.app.workspace.activeLeaf.openFile(file); 66 | }); 67 | }); 68 | } 69 | }) 70 | ); 71 | 72 | // register a custom icon 73 | this.addDocumentIcon("csv"); 74 | 75 | // register the view and extensions 76 | this.registerView("csv", this.csvViewCreator); 77 | this.registerExtensions(["csv"], "csv"); 78 | } 79 | 80 | // function to create the view 81 | csvViewCreator = (leaf: WorkspaceLeaf) => { 82 | return new CsvView(leaf); 83 | }; 84 | 85 | // this function used the regular 'document' svg, 86 | // but adds the supplied extension into the icon as well 87 | addDocumentIcon = (extension: string) => { 88 | addIcon(`document-${extension}`, ` 89 | 90 | 91 | ${extension} 92 | 93 | `); 94 | }; 95 | } 96 | 97 | // This is the custom view 98 | class CsvView extends TextFileView { 99 | autoSaveToggle: ToggleComponent; 100 | saveButton: ButtonComponent; 101 | autoSaveValue: boolean; 102 | parseResult: ParseResult; 103 | headerToggle: ToggleComponent; 104 | headers: string[] = null; 105 | fileOptionsEl: HTMLElement; 106 | hot: Handsontable; 107 | hotSettings: Handsontable.GridSettings; 108 | hotExport: Handsontable.plugins.ExportFile; 109 | hotState: Handsontable.plugins.PersistentState; 110 | hotFilters: Handsontable.plugins.Filters; 111 | loadingBar: HTMLElement; 112 | 113 | // this.contentEl is not exposed, so cheat a bit. 114 | public get extContentEl(): HTMLElement { 115 | return this.contentEl; 116 | } 117 | 118 | // constructor 119 | constructor(leaf: WorkspaceLeaf) { 120 | //Calling the parent constructor 121 | super(leaf); 122 | this.autoSaveValue = true; 123 | this.onResize = () => { 124 | //@ts-ignore - this.hot.view not recognized. 125 | this.hot.view.wt.wtOverlays.updateMainScrollableElements(); 126 | this.hot.render(); 127 | }; 128 | this.loadingBar = document.createElement("div"); 129 | this.loadingBar.addClass("progress-bar"); 130 | this.loadingBar.innerHTML = "
Loading CSV...
"; 131 | this.extContentEl.appendChild(this.loadingBar); 132 | 133 | this.fileOptionsEl = document.createElement("div"); 134 | this.fileOptionsEl.classList.add("csv-controls"); 135 | this.extContentEl.appendChild(this.fileOptionsEl); 136 | 137 | //Creating a toggle to set the header 138 | new Setting(this.fileOptionsEl) 139 | .setName("File Includes Headers") 140 | .addToggle(toggle => { 141 | this.headerToggle = toggle; 142 | toggle.setValue(false).onChange(this.toggleHeaders); 143 | }); 144 | 145 | // //Creating a toggle to allow the toggle of the auto Save 146 | // new Setting(this.fileOptionsEl) 147 | // .setName("Auto Save") 148 | // .addToggle((toggle: ToggleComponent) => { 149 | // toggle 150 | // .setValue(this.autoSaveValue) 151 | // .onChange((value) => { 152 | // // Setting the autosave value 153 | // this.autoSaveValue = value; 154 | // 155 | // // Disabling/Enabling the save button 156 | // if(this.saveButton) { 157 | // this.saveButton.setDisabled(value); 158 | // // this.saveButton.buttonEl.disabled = value; 159 | // if (value && !this.saveButton.buttonEl.hasClass("element-disabled")){ 160 | // this.saveButton.buttonEl.addClass("element-disabled"); 161 | // } else if (!value && this.saveButton.buttonEl.hasClass("element-disabled")) { 162 | // this.saveButton.buttonEl.removeClass("element-disabled"); 163 | // } 164 | // } 165 | // }); 166 | // }); 167 | // 168 | // //Creating a Save button 169 | // new Setting(this.fileOptionsEl) 170 | // .addButton((button: ButtonComponent) => { 171 | // this.saveButton = button; 172 | // button.setButtonText("Save"); 173 | // button.setDisabled(this.autoSaveToggle?.getValue()??this.autoSaveValue); 174 | // if (button.disabled){ 175 | // button.buttonEl.addClass("element-disabled"); 176 | // } 177 | // button.onClick((e: MouseEvent) => { 178 | // this.requestManualSave(); 179 | // }); 180 | // }); 181 | 182 | const tableContainer = document.createElement("div"); 183 | tableContainer.classList.add("csv-table-wrapper"); 184 | this.extContentEl.appendChild(tableContainer); 185 | 186 | const hotContainer = document.createElement("div"); 187 | tableContainer.appendChild(hotContainer); 188 | 189 | 190 | Handsontable.renderers.registerRenderer("markdown", this.markdownCellRenderer); 191 | // Handsontable.editors.registerEditor("markdown", MarkdownCellEditor); 192 | this.hotSettings = { 193 | afterChange: this.hotChange, 194 | afterColumnSort: this.requestAutoSave, 195 | afterColumnMove: this.requestAutoSave, 196 | afterRowMove: this.requestAutoSave, 197 | afterCreateCol: this.requestAutoSave, 198 | afterCreateRow: this.requestAutoSave, 199 | afterRemoveCol: this.requestAutoSave, 200 | afterRemoveRow: this.requestAutoSave, 201 | licenseKey: "non-commercial-and-evaluation", 202 | colHeaders: true, 203 | rowHeaders: true, 204 | autoColumnSize: true, 205 | autoRowSize: true, 206 | renderer: "markdown", 207 | // editor: "markdown", 208 | className: "csv-table", 209 | contextMenu: true, 210 | currentRowClassName: "active-row", 211 | currentColClassName: "active-col", 212 | columnSorting: true, 213 | dropdownMenu: true, 214 | filters: true, 215 | manualColumnFreeze: true, 216 | manualColumnMove: false, // moving columns causes too many headaches for now 217 | manualColumnResize: true, 218 | manualRowMove: false, // moving rows causes too many headaches for now 219 | manualRowResize: true, 220 | persistentState: true, 221 | // preventOverflow: true, 222 | search: true, // TODO:290 Hijack the search ui from markdown views, 223 | height: "100%", 224 | width: "100%", 225 | // stretchH: 'last' 226 | }; 227 | this.hot = new ExtHandsontable(hotContainer, this.hotSettings, {leaf:this.leaf}); 228 | this.hotExport = this.hot.getPlugin("exportFile"); 229 | this.hotState = this.hot.getPlugin("persistentState"); 230 | this.hotFilters = this.hot.getPlugin("filters"); 231 | } 232 | 233 | requestAutoSave = (): void => { 234 | if(this.autoSaveValue){ 235 | this.requestSave(); 236 | } 237 | } 238 | 239 | requestManualSave = (): void => { 240 | if(!this.autoSaveValue) { 241 | this.requestSave(); 242 | } 243 | } 244 | 245 | hotChange = (changes: Handsontable.CellChange[], source: Handsontable.ChangeSource): void => { 246 | if (source === "loadData") { 247 | return; //don't save this change 248 | } 249 | 250 | if(this.requestAutoSave) { 251 | this.requestAutoSave(); 252 | } else { 253 | console.error("Couldn't auto save..."); 254 | } 255 | }; 256 | 257 | // get the new file contents 258 | override getViewData(): string { 259 | if(this.hot && !this.hot.isDestroyed) { 260 | // get the *source* data (i.e. unfiltered) 261 | const data = this.hot.getSourceDataArray(); 262 | if (this.hotSettings.colHeaders !== true) { 263 | data.unshift(this.hot.getColHeader()); 264 | } 265 | 266 | return Papa.unparse(data); 267 | } else { 268 | return this.data; 269 | } 270 | }; 271 | 272 | // Setting the view from the previously set data 273 | override setViewData(data: string, clear: boolean): void { 274 | this.data = data; 275 | this.loadingBar.show(); 276 | debounce(() => this.loadDataAsync(data) 277 | .then(() => { 278 | console.log("Loading data correctly."); 279 | this.loadingBar.hide(); 280 | }) 281 | .catch((e: any) => { 282 | const ErrorTimeout = 5000; 283 | this.loadingBar.hide(); 284 | if (Array.isArray(e)){ 285 | console.error(`Catch ${e.length > 1 ? "multiple errors" : "an error"} during the loading of the data from "${this.file.name}".`); 286 | for (const error of e) { 287 | if (error.hasOwnProperty("message")){ 288 | console.error(error["message"], error); 289 | new Notice(error["message"],ErrorTimeout); 290 | } else { 291 | console.error(JSON.stringify(error), error); 292 | new Notice(JSON.stringify(error),ErrorTimeout); 293 | } 294 | } 295 | } else { 296 | new Notice(JSON.stringify(e),ErrorTimeout); 297 | console.error(`Catch error during the loading of the data from ${this.file.name}\n`,e); 298 | } 299 | this.hot?.destroy(); 300 | this.hot = undefined; 301 | //Close the window 302 | this.app.workspace.activeLeaf.detach(); 303 | }) 304 | , 50, true).apply(this); 305 | return; 306 | }; 307 | 308 | loadDataAsync(data: string): Promise { 309 | return new Promise((resolve: (value: (PromiseLike | void)) => void, reject: ParseError[] | any) => { 310 | // for the sake of persistent settings we need to set the root element id 311 | this.hot.rootElement.id = this.file.path; 312 | this.hotSettings.colHeaders = true; 313 | 314 | // strip Byte Order Mark if necessary (damn you, Excel) 315 | if (data.charCodeAt(0) === 0xFEFF) data = data.slice(1); 316 | 317 | // parse the incoming data string 318 | Papa.parse(data,{ 319 | header:false, 320 | complete: (results: ParseResult) => { 321 | //Handle the errors 322 | if (results.errors !== undefined && results.errors.length !== 0) { 323 | reject(results.errors); 324 | return; 325 | } 326 | 327 | this.parseResult = results; 328 | 329 | // load the data into the table 330 | this.hot.loadData(this.parseResult.data); 331 | // we also need to update the settings so that the persistence will work 332 | this.hot.updateSettings(this.hotSettings); 333 | 334 | // load the persistent setting for headings 335 | const hasHeadings = { value: false }; 336 | this.hotState.loadValue("hasHeadings", hasHeadings); 337 | this.headerToggle.setValue(hasHeadings.value); 338 | 339 | // toggle the headers on or off based on the loaded value 340 | this.toggleHeaders(hasHeadings.value); 341 | resolve(); 342 | } 343 | }); 344 | }); 345 | }; 346 | 347 | override clear() { 348 | // clear the view content 349 | this.hot?.clear(); 350 | this.hot?.clearUndo(); 351 | }; 352 | 353 | //Unloading the data 354 | override async onUnloadFile(file: TFile): Promise{ 355 | await super.onUnloadFile(file); 356 | return; 357 | } 358 | 359 | override async save(clear?: boolean): Promise { 360 | const SaveNoticeTimeout = 1000; 361 | try { 362 | await super.save(clear); 363 | new Notice(`"${this.file.name}" was saved.`,SaveNoticeTimeout); 364 | } catch (e) { 365 | new Notice(`"${this.file.name}" couldn't be saved.`,SaveNoticeTimeout); 366 | throw e; 367 | } 368 | } 369 | 370 | // Arrow function because "this" can bug 371 | toggleHeaders = (value: boolean) => { 372 | value = value || false; // just in case it's undefined 373 | // turning headers on 374 | if (value) { 375 | // we haven't specified headers yet 376 | if (this.hotSettings.colHeaders === true) { 377 | // get the data 378 | const data = this.hot.getSourceDataArray(); 379 | // take the first row off the data to use as headers 380 | this.hotSettings.colHeaders = data.shift(); 381 | // reload the data without this first row 382 | this.hot.loadData(data); 383 | // update the settings 384 | this.hot.updateSettings(this.hotSettings); 385 | } 386 | } 387 | // turning headers off 388 | else { 389 | // we have headers 390 | if (this.hotSettings.colHeaders !== true) { 391 | // get the data 392 | const data = this.hot.getSourceDataArray(); 393 | // put the headings back in as a row 394 | data.unshift(this.hot.getColHeader()); 395 | // specify true to just display alphabetical headers 396 | this.hotSettings.colHeaders = true; 397 | // reload the data with this new first row 398 | this.hot.loadData(data); 399 | // update the settings 400 | this.hot.updateSettings(this.hotSettings); 401 | } 402 | } 403 | 404 | // set this value to the state 405 | this.hotState.saveValue("hasHeadings", value); 406 | }; 407 | 408 | // DO NOT TRANSFORM THIS INTO A REAL FUNCTION 409 | markdownCellRenderer = (instance: Handsontable, TD: HTMLTableCellElement, row: number, col: number, prop: string | number, value: Handsontable.CellValue, cellProperties: Handsontable.CellProperties): HTMLTableCellElement | void => { 410 | TD.innerHTML = ""; 411 | if (cellProperties.className){ 412 | const htmlClass: string[] = Array.isArray(cellProperties.className)?cellProperties.className:cellProperties.className.split(" "); 413 | TD.style.textAlign = ""; 414 | for (const c of htmlClass) { 415 | switch (c) { 416 | case "htLeft": 417 | TD.style.textAlign = "left"; 418 | break; 419 | case "htCenter": 420 | TD.style.textAlign = "center"; 421 | break; 422 | case "htRight": 423 | TD.style.textAlign = "right"; 424 | break; 425 | case "htJustify": 426 | TD.style.textAlign = "justify"; 427 | break; 428 | case "htTop": 429 | TD.style.verticalAlign = "top"; 430 | break; 431 | case "htMiddle": 432 | TD.style.verticalAlign = "middle"; 433 | break; 434 | case "htBottom": 435 | TD.style.verticalAlign = "bottom"; 436 | break; 437 | default: 438 | break; 439 | } 440 | } 441 | } 442 | MarkdownRenderer.renderMarkdown(value, TD, this.file.path || "", this || null); 443 | return TD; 444 | }; 445 | 446 | // gets the title of the document 447 | getDisplayText() { 448 | if (this.file) return this.file.basename; 449 | else return "csv (no file)"; 450 | } 451 | 452 | // confirms this view can accept csv extension 453 | canAcceptExtension(extension: string) { 454 | return extension == "csv"; 455 | } 456 | 457 | // the view type name 458 | getViewType() { 459 | return "csv"; 460 | } 461 | 462 | // icon for the view 463 | getIcon() { 464 | return "document-csv"; 465 | } 466 | } 467 | 468 | class ExtHandsontable extends Handsontable { 469 | extContext: any; 470 | 471 | constructor(element: Element, options: Handsontable.GridSettings, context:any) { 472 | super(element, options); 473 | this.extContext = context; 474 | } 475 | } 476 | 477 | // class MarkdownCellEditor extends Handsontable.editors.BaseEditor { 478 | // eGui: HTMLElement; 479 | // view: MarkdownView; 480 | // 481 | // override init(): void { 482 | // const extContext: any = (this.hot as ExtHandsontable).extContext; 483 | // if (extContext && extContext.leaf && !this.eGui) { 484 | // // create the container 485 | // this.eGui = this.hot.rootDocument.createElement("DIV"); 486 | // Handsontable.dom.addClass(this.eGui, "htMarkdownEditor"); 487 | // Handsontable.dom.addClass(this.eGui, "csv-cell-edit"); 488 | // 489 | // // create a markdown (editor) view 490 | // this.view = new MarkdownView(extContext.leaf); 491 | // 492 | // this.eGui.appendChild(this.view.contentEl); 493 | // // hide the container 494 | // this.eGui.style.display = "none"; 495 | // // add the container to the table root element 496 | // this.hot.rootElement.appendChild(this.eGui); 497 | // } 498 | // } 499 | // 500 | // override open(event?: Event): void { 501 | // this.refreshDimensions(); 502 | // this.eGui.show(); 503 | // this.view.editor.focus(); 504 | // this.view.editor.refresh(); 505 | // } 506 | // 507 | // refreshDimensions() { 508 | // this.TD = this.getEditedCell(); 509 | // 510 | // // TD is outside of the viewport. 511 | // if (!this.TD) { 512 | // this.close(); 513 | // 514 | // return; 515 | // } 516 | // //@ts-ignore - this.hot.view not recognized. 517 | // const { wtOverlays } = this.hot.view.wt; 518 | // const currentOffset = Handsontable.dom.offset(this.TD); 519 | // const containerOffset = Handsontable.dom.offset(this.hot.rootElement); 520 | // const scrollableContainer = wtOverlays.scrollableElement; 521 | // const editorSection = this.checkEditorSection(); 522 | // let width = Handsontable.dom.outerWidth(this.TD) + 1; 523 | // let height = Handsontable.dom.outerHeight(this.TD) + 1; 524 | // 525 | // let editTop = currentOffset.top - containerOffset.top - 1 - (scrollableContainer.scrollTop || 0); 526 | // let editLeft = currentOffset.left - containerOffset.left - 1 - (scrollableContainer.scrollLeft || 0); 527 | // 528 | // let cssTransformOffset; 529 | // 530 | // switch (editorSection) { 531 | // case "top": 532 | // cssTransformOffset = Handsontable.dom.getCssTransform(wtOverlays.topOverlay.clone.wtTable.holder.parentNode); 533 | // break; 534 | // case "left": 535 | // cssTransformOffset = Handsontable.dom.getCssTransform(wtOverlays.leftOverlay.clone.wtTable.holder.parentNode); 536 | // break; 537 | // case "top-left-corner": 538 | // cssTransformOffset = Handsontable.dom.getCssTransform(wtOverlays.topLeftCornerOverlay.clone.wtTable.holder.parentNode); 539 | // break; 540 | // case "bottom-left-corner": 541 | // cssTransformOffset = Handsontable.dom.getCssTransform(wtOverlays.bottomLeftCornerOverlay.clone.wtTable.holder.parentNode); 542 | // break; 543 | // case "bottom": 544 | // cssTransformOffset = Handsontable.dom.getCssTransform(wtOverlays.bottomOverlay.clone.wtTable.holder.parentNode); 545 | // break; 546 | // default: 547 | // break; 548 | // } 549 | // 550 | // if (this.hot.getSelectedLast()[0] === 0) { 551 | // editTop += 1; 552 | // } 553 | // if (this.hot.getSelectedLast()[1] === 0) { 554 | // editLeft += 1; 555 | // } 556 | // 557 | // const selectStyle = this.eGui.style; 558 | // 559 | // if (cssTransformOffset && cssTransformOffset !== -1) { 560 | // //@ts-ignore 561 | // selectStyle[cssTransformOffset[0]] = cssTransformOffset[1]; 562 | // } else { 563 | // Handsontable.dom.resetCssTransform(this.eGui); 564 | // } 565 | // 566 | // const cellComputedStyle = Handsontable.dom.getComputedStyle(this.TD, this.hot.rootWindow); 567 | // if (parseInt(cellComputedStyle.borderTopWidth, 10) > 0) { 568 | // height -= 1; 569 | // } 570 | // if (parseInt(cellComputedStyle.borderLeftWidth, 10) > 0) { 571 | // width -= 1; 572 | // } 573 | // 574 | // selectStyle.height = `${height}px`; 575 | // selectStyle.minWidth = `${width}px`; 576 | // selectStyle.maxWidth = `${width}px`; 577 | // selectStyle.top = `${editTop}px`; 578 | // selectStyle.left = `${editLeft}px`; 579 | // selectStyle.margin = "0px"; 580 | // } 581 | // 582 | // override getEditedCell(): HTMLTableCellElement | null { 583 | // //@ts-ignore - this.hot.view not recognized. 584 | // const { wtOverlays } = this.hot.view.wt; 585 | // const editorSection = this.checkEditorSection(); 586 | // let editedCell; 587 | // 588 | // switch (editorSection) { 589 | // case "top": 590 | // editedCell = wtOverlays.topOverlay.clone.wtTable.getCell({ 591 | // row: this.row, 592 | // col: this.col 593 | // }); 594 | // this.eGui.style.zIndex = "101"; 595 | // break; 596 | // case "top-left-corner": 597 | // case "bottom-left-corner": 598 | // editedCell = wtOverlays.topLeftCornerOverlay.clone.wtTable.getCell({ 599 | // row: this.row, 600 | // col: this.col 601 | // }); 602 | // this.eGui.style.zIndex = "103"; 603 | // break; 604 | // case "left": 605 | // editedCell = wtOverlays.leftOverlay.clone.wtTable.getCell({ 606 | // row: this.row, 607 | // col: this.col 608 | // }); 609 | // this.eGui.style.zIndex = "102"; 610 | // break; 611 | // default: 612 | // editedCell = this.hot.getCell(this.row, this.col); 613 | // this.eGui.style.zIndex = ""; 614 | // break; 615 | // } 616 | // 617 | // return editedCell < 0 ? void 0 : editedCell; 618 | // } 619 | // 620 | // override close(): void { 621 | // this.eGui.hide(); 622 | // } 623 | // override focus(): void { 624 | // this.view.editor.focus(); 625 | // this.view.editor.refresh(); 626 | // } 627 | // override getValue() { 628 | // return this.view.currentMode.get(); 629 | // } 630 | // override setValue(newValue?: any): void { 631 | // if(this) 632 | // this.view.currentMode.set(newValue, false); 633 | // } 634 | // } 635 | -------------------------------------------------------------------------------- /src/styles.scss: -------------------------------------------------------------------------------- 1 | .workspace-leaf-content[data-type="csv"] .view-content{ 2 | padding:0; 3 | display: flex; 4 | flex-direction: column; 5 | 6 | .progress-bar { 7 | z-index: 500; 8 | padding: 40px; 9 | } 10 | 11 | .htMarkdownEditor { 12 | position: absolute; 13 | width: auto; 14 | background: var(--background-primary); 15 | 16 | .markdown-source-view { 17 | padding:0; 18 | .CodeMirror-scroll { 19 | padding:0; 20 | margin:0; 21 | padding-bottom:8px; 22 | } 23 | .CodeMirror-sizer { 24 | padding: 0 !important; 25 | margin: 0 !important; 26 | border: none; 27 | } 28 | .CodeMirror-lines{ 29 | padding: 0; 30 | } 31 | .CodeMirror-code{ 32 | padding:0; 33 | } 34 | } 35 | } 36 | 37 | .csv-controls { 38 | margin: 20px 8px 8px 8px; 39 | padding: 8px; 40 | background-color: var(--background-primary-alt); 41 | min-width: 140px; 42 | max-width: 240px; 43 | border: 1px solid var(--background-modifier-border); 44 | border-radius: 6px; 45 | flex-grow: 0; 46 | overflow: hidden; 47 | } 48 | .csv-table-wrapper { 49 | position: relative; 50 | flex-grow: 1; 51 | width:100%; 52 | } 53 | 54 | td>p:first-child { margin-top: 0; } 55 | td>p:last-child { margin-bottom: 0; } 56 | 57 | .handsontable.csv-table{ 58 | tbody tr { 59 | background-color: var(--background-primary); 60 | } 61 | tbody tr:nth-child(2n) { 62 | background-color: var(--background-primary-alt); 63 | } 64 | thead tr, 65 | tbody th { 66 | background-color: var(--background-secondary); 67 | } 68 | tbody th.ht__highlight, 69 | thead th.ht__highlight { 70 | background-color: var(--background-secondary-alt); 71 | } 72 | tbody th.ht__active_highlight, 73 | thead th.ht__active_highlight { 74 | background-color: var(--interactive-accent); 75 | color:var(--text-on-accent) 76 | } 77 | 78 | span.colHeader.columnSorting.descending::before { 79 | background-image: none; 80 | font-size: 0.75em; 81 | content: "↓"; 82 | } 83 | span.colHeader.columnSorting.ascending::before { 84 | background-image: none; 85 | font-size: 0.75em; 86 | content: "↑"; 87 | } 88 | 89 | th, 90 | td { 91 | background-color: transparent; 92 | border-color: var(--background-modifier-border); 93 | color: var(--text-normal); 94 | } 95 | 96 | &.htRowHeaders thead tr th:nth-child(2), 97 | tr:first-child th, 98 | tr:first-child td, 99 | th:first-child, 100 | th:nth-child(2), 101 | td:first-of-type { 102 | border-color: var(--background-modifier-border); 103 | } 104 | 105 | .wtBorder, 106 | td.area:before, 107 | td.area-1:before, 108 | td.area-2:before, 109 | td.area-3:before, 110 | td.area-4:before, 111 | td.area-5:before, 112 | td.area-6:before, 113 | td.area-7:before { 114 | background-color: var(--interactive-accent) !important; 115 | } 116 | } 117 | } 118 | 119 | .htMenu.handsontable { 120 | 121 | .ht_master table td.htCustomMenuRenderer, 122 | tbody tr { 123 | background-color: var(--background-primary); 124 | } 125 | 126 | th, 127 | td, 128 | table tbody tr td { 129 | background-color: transparent; 130 | border-color: var(--background-modifier-border); 131 | color: var(--text-normal); 132 | } 133 | 134 | table tbody tr td.htDisabled { 135 | color: var(--text-faint); 136 | } 137 | 138 | table tbody tr td.htSeparator, 139 | .ht_master table.htCore { 140 | border-color: var(--background-modifier-border); 141 | } 142 | } 143 | 144 | .element-disabled { 145 | opacity: 0.1; 146 | cursor: default; 147 | } 148 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "inlineSourceMap": true, 5 | "inlineSources": true, 6 | "module": "ESNext", 7 | "target": "es6", 8 | "allowJs": true, 9 | "noImplicitAny": true, 10 | "moduleResolution": "node", 11 | "importHelpers": true, 12 | "lib": [ 13 | "dom", 14 | "es5", 15 | "scripthost", 16 | "es2015" 17 | ] 18 | }, 19 | "include": [ 20 | "**/*.ts" 21 | ] 22 | } 23 | --------------------------------------------------------------------------------